From d7c7c3e1df07cf4efb8e56ebe600459b47205ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Fri, 26 Jul 2024 20:02:59 +0200 Subject: [PATCH] Initial CryptoService impl --- Cargo.lock | 78 ++ crates/bitwarden-core/src/lib.rs | 116 +++ crates/bitwarden-crypto/Cargo.toml | 1 + crates/bitwarden-crypto/src/error.rs | 2 + crates/bitwarden-crypto/src/lib.rs | 1 + .../src/service/crypto_engine/mod.rs | 102 +++ .../src/service/crypto_engine/rust_impl.rs | 250 ++++++ .../src/service/encryptable.rs | 130 ++++ .../bitwarden-crypto/src/service/key_ref.rs | 86 +++ .../key_store/linux_memfd_secret_impl.rs | 122 +++ .../src/service/key_store/mod.rs | 27 + .../src/service/key_store/rust_impl.rs | 129 ++++ .../src/service/key_store/util.rs | 719 ++++++++++++++++++ crates/bitwarden-crypto/src/service/mod.rs | 82 ++ 14 files changed, 1845 insertions(+) create mode 100644 crates/bitwarden-crypto/src/service/crypto_engine/mod.rs create mode 100644 crates/bitwarden-crypto/src/service/crypto_engine/rust_impl.rs create mode 100644 crates/bitwarden-crypto/src/service/encryptable.rs create mode 100644 crates/bitwarden-crypto/src/service/key_ref.rs create mode 100644 crates/bitwarden-crypto/src/service/key_store/linux_memfd_secret_impl.rs create mode 100644 crates/bitwarden-crypto/src/service/key_store/mod.rs create mode 100644 crates/bitwarden-crypto/src/service/key_store/rust_impl.rs create mode 100644 crates/bitwarden-crypto/src/service/key_store/util.rs create mode 100644 crates/bitwarden-crypto/src/service/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e9ed72fee..c201b243c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,7 @@ dependencies = [ "generic-array", "hkdf", "hmac", + "memsec", "num-bigint", "num-traits", "pbkdf2", @@ -2282,6 +2283,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "memsec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" +dependencies = [ + "getrandom", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "mime" version = "0.3.17" @@ -4616,6 +4628,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4643,6 +4664,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4674,6 +4710,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4686,6 +4728,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4698,6 +4746,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4716,6 +4770,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4728,6 +4788,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4740,6 +4806,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4752,6 +4824,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/crates/bitwarden-core/src/lib.rs b/crates/bitwarden-core/src/lib.rs index 12b0df3c3..5f900a468 100644 --- a/crates/bitwarden-core/src/lib.rs +++ b/crates/bitwarden-core/src/lib.rs @@ -19,3 +19,119 @@ mod util; pub use bitwarden_crypto::ZeroizingAllocator; pub use client::{Client, ClientSettings, DeviceType}; + +#[allow(warnings)] +#[cfg(test)] +mod testcrypto { + + // TEST IMPL OF THE NEW ENCRYPTABLE/DECRYPTABLE TRAITS + // Note that these never touch the keys at all, they just use the context and key references to + // encrypt/decrypt + + use bitwarden_crypto::{ + key_refs, service::*, AsymmetricCryptoKey, CryptoError, EncString, KeyEncryptable, + SymmetricCryptoKey, + }; + + key_refs! { + #[symmetric] + pub enum MySymmKeyRef { + User, + Organization(uuid::Uuid), + #[local] + Local(&'static str), + } + + #[asymmetric] + pub enum MyAsymmKeyRef { + UserPrivateKey, + #[local] + Local(&'static str), + } + } + + #[derive(Clone)] + struct Cipher { + key: Option, + name: EncString, + } + + #[derive(Clone)] + struct CipherView { + key: Option, + name: String, + } + + const CIPHER_KEY: MySymmKeyRef = MySymmKeyRef::Local("cipher_key"); + + impl Encryptable for CipherView { + fn encrypt( + &self, + ctx: &mut CryptoServiceContext, + key: MySymmKeyRef, + ) -> Result { + let cipher_key = match &self.key { + Some(cipher_key) => { + ctx.decrypt_and_store_symmetric_key(key, CIPHER_KEY, cipher_key)? + } + None => key, + }; + + Ok(Cipher { + key: self.key.clone(), + name: self.name.encrypt(ctx, cipher_key)?, + }) + } + } + + impl Decryptable for Cipher { + fn decrypt( + &self, + ctx: &mut CryptoServiceContext, + key: MySymmKeyRef, + ) -> Result { + let cipher_key = match &self.key { + Some(cipher_key) => { + ctx.decrypt_and_store_symmetric_key(key, CIPHER_KEY, cipher_key)? + } + None => key, + }; + + Ok(CipherView { + key: self.key.clone(), + name: self.name.decrypt(ctx, cipher_key)?, + }) + } + } + + #[test] + fn test_cipher() { + let user_key = SymmetricCryptoKey::generate(rand::thread_rng()); + + let org_id = uuid::Uuid::parse_str("91b000b6-81ce-47f4-9802-3390e0b895ed").unwrap(); + let org_key = SymmetricCryptoKey::generate(rand::thread_rng()); + + let cipher_key = SymmetricCryptoKey::generate(rand::thread_rng()); + let cipher_key_user_enc = cipher_key.to_vec().encrypt_with_key(&user_key).unwrap(); + let cipher_view = CipherView { + key: Some(cipher_key_user_enc.clone()), + name: "test".to_string(), + }; + + let service: CryptoService = CryptoService::new(); + // Ideally we'd decrypt the keys directly into the service, but that's not implemented yet + #[allow(deprecated)] + { + service.insert_symmetric_key(MySymmKeyRef::User, user_key); + service.insert_symmetric_key(MySymmKeyRef::Organization(org_id), org_key); + } + + let cipher_enc2 = service + .encrypt(MySymmKeyRef::User, cipher_view.clone()) + .unwrap(); + + let cipher_view2 = service.decrypt(MySymmKeyRef::User, &cipher_enc2).unwrap(); + + assert_eq!(cipher_view.name, cipher_view2.name); + } +} diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index f2bbc6f7e..aa683dc7f 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -30,6 +30,7 @@ cbc = { version = ">=0.1.2, <0.2", features = ["alloc", "zeroize"] } generic-array = { version = ">=0.14.7, <1.0", features = ["zeroize"] } hkdf = ">=0.12.3, <0.13" hmac = ">=0.12.1, <0.13" +memsec = { version = "0.7.0", features = ["alloc_ext"] } num-bigint = ">=0.4, <0.5" num-traits = ">=0.2.15, <0.3" pbkdf2 = { version = ">=0.12.1, <0.13", default-features = false } diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index 2fc4d2591..f2ac92e19 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -21,6 +21,8 @@ pub enum CryptoError { MissingKey(Uuid), #[error("The item was missing a required field: {0}")] MissingField(&'static str), + #[error("Missing Key for Ref. {0}")] + MissingKey2(String), #[error("EncString error, {0}")] EncString(#[from] EncStringParseError), diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 4c71b0029..04c441e47 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -81,6 +81,7 @@ mod wordlist; pub use wordlist::EFF_LONG_WORD_LIST; mod allocator; pub use allocator::ZeroizingAllocator; +pub mod service; #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); diff --git a/crates/bitwarden-crypto/src/service/crypto_engine/mod.rs b/crates/bitwarden-crypto/src/service/crypto_engine/mod.rs new file mode 100644 index 000000000..404ad201d --- /dev/null +++ b/crates/bitwarden-crypto/src/service/crypto_engine/mod.rs @@ -0,0 +1,102 @@ +use crate::{ + service::{AsymmetricKeyRef, SymmetricKeyRef}, + AsymmetricEncString, EncString, SymmetricCryptoKey, +}; + +mod rust_impl; + +pub(crate) use rust_impl::RustCryptoEngine; + +// This trait represents a service that can store cryptographic keys and perform operations with +// them, ideally within a Secure Enclave or HSM. Users of this trait will not handle the keys +// directly but will use references to them. +// +// For the cases where a secure element capable of doing cryptographic operations is not available, +// but there is a secure way to store keys, `KeyStore` can be implemented and then used in +// conjunction with `RustCryptoEngine`. +pub(crate) trait CryptoEngine { + /// Create a new context for this service. This allows the user to perform cryptographic + // operations with keys that are only relevant to the current context. + /// + /// NOTE: This is an advanced API, and should be used with care. Particularly, it's important + /// to ensure the context is dropped when it's no longer needed, to avoid holding a reference to + /// the RwLock for too long. It's also important to ensure that the context is cleared of + /// keys after every use if it's being reused, to avoid it growing indefinitely. + fn context(&'_ self) -> Box + '_>; + + #[deprecated(note = "We should be generating/decrypting the keys into the service directly")] + fn insert_symmetric_key(&self, key_ref: SymmKeyRef, key: SymmetricCryptoKey); + + fn clear(&self); +} + +// This trait represents a context for a `CryptoEngine`. It allows the user to perform cryptographic +// operations with keys that are only relevant to the current context. +#[allow(dead_code)] +pub(crate) trait CryptoEngineContext<'a, SymmKeyRef: SymmetricKeyRef, AsymmKeyRef: AsymmetricKeyRef> +{ + fn clear(&mut self); + + // Symmetric key operations + + fn decrypt_data_with_symmetric_key( + &self, + key: SymmKeyRef, + data: &EncString, + ) -> Result, crate::CryptoError>; + + fn encrypt_data_with_symmetric_key( + &self, + key: SymmKeyRef, + data: &[u8], + ) -> Result; + + fn decrypt_and_store_symmetric_key( + &mut self, + encryption_key: SymmKeyRef, + new_key_ref: SymmKeyRef, + encrypted_key: &EncString, + ) -> Result; + + fn encrypt_symmetric_key( + &self, + encryption_key: SymmKeyRef, + key_to_encrypt: SymmKeyRef, + ) -> Result; + + // Asymmetric key operations + + fn decrypt_data_with_asymmetric_key( + &self, + key: AsymmKeyRef, + data: &AsymmetricEncString, + ) -> Result, crate::CryptoError>; + + fn encrypt_data_with_asymmetric_key( + &self, + key: AsymmKeyRef, + data: &[u8], + ) -> Result; + + fn decrypt_and_store_asymmetric_key( + &mut self, + encryption_key: AsymmKeyRef, + new_key_ref: AsymmKeyRef, + encrypted_key: &AsymmetricEncString, + ) -> Result; + + fn encrypt_asymmetric_key( + &self, + encryption_key: AsymmKeyRef, + key_to_encrypt: AsymmKeyRef, + ) -> Result; +} + +fn _ensure_that_traits_are_object_safe< + SymmKeyRef: SymmetricKeyRef, + AsymmKeyRef: AsymmetricKeyRef, +>( + _: Box>, + _: Box>, +) { +} diff --git a/crates/bitwarden-crypto/src/service/crypto_engine/rust_impl.rs b/crates/bitwarden-crypto/src/service/crypto_engine/rust_impl.rs new file mode 100644 index 000000000..69164b361 --- /dev/null +++ b/crates/bitwarden-crypto/src/service/crypto_engine/rust_impl.rs @@ -0,0 +1,250 @@ +use std::sync::RwLock; + +use rsa::Oaep; + +use crate::{ + service::{ + crypto_engine::{CryptoEngine, CryptoEngineContext}, + key_ref::KeyRef, + key_store::KeyStore, + AsymmetricKeyRef, SymmetricKeyRef, + }, + AsymmetricCryptoKey, AsymmetricEncString, CryptoError, EncString, SymmetricCryptoKey, +}; + +fn create_key_store() -> Box> { + #[cfg(target_os = "linux")] + if let Some(key_store) = crate::service::key_store::LinuxMemfdSecretKeyStore::::new() { + return Box::new(key_store); + } + + Box::new(crate::service::key_store::RustKeyStore::new()) +} + +pub(crate) struct RustCryptoEngine { + key_stores: RwLock>, +} + +// This is just a wrapper around the keys so we only deal with one RwLock +struct RustCryptoEngineKeys { + symmetric_keys: Box>, + asymmetric_keys: Box>, +} + +impl + RustCryptoEngine +{ + pub(crate) fn new() -> Self { + Self { + key_stores: RwLock::new(RustCryptoEngineKeys { + symmetric_keys: create_key_store(), + asymmetric_keys: create_key_store(), + }), + } + } +} + +impl + CryptoEngine for RustCryptoEngine +{ + fn context(&'_ self) -> Box + '_> { + // TODO: Cache these?, or maybe initialize them lazily? or both? + Box::new(RustCryptoEngineContext { + global_keys: self.key_stores.read().expect("RwLock is poisoned"), + local_symmetric_keys: create_key_store(), + local_asymmetric_keys: create_key_store(), + }) + } + + fn insert_symmetric_key(&self, key_ref: SymmKeyRef, key: SymmetricCryptoKey) { + self.key_stores + .write() + .expect("RwLock is poisoned") + .symmetric_keys + .insert(key_ref, key); + } + + fn clear(&self) { + let mut keys = self.key_stores.write().expect("RwLock is poisoned"); + keys.symmetric_keys.clear(); + keys.asymmetric_keys.clear(); + } +} + +pub(crate) struct RustCryptoEngineContext< + 'a, + SymmKeyRef: SymmetricKeyRef, + AsymmKeyRef: AsymmetricKeyRef, +> { + // We hold a RwLock read guard to avoid having any nested + //calls locking it again and potentially causing a deadlock + global_keys: std::sync::RwLockReadGuard<'a, RustCryptoEngineKeys>, + + local_symmetric_keys: Box>, + local_asymmetric_keys: Box>, +} + +impl<'a, SymmKeyRef: SymmetricKeyRef, AsymmKeyRef: AsymmetricKeyRef> + RustCryptoEngineContext<'a, SymmKeyRef, AsymmKeyRef> +{ + fn get_symmetric_key( + &self, + key_ref: SymmKeyRef, + ) -> Result<&SymmetricCryptoKey, crate::CryptoError> { + if key_ref.is_local() { + self.local_symmetric_keys.get(key_ref) + } else { + self.global_keys.symmetric_keys.get(key_ref) + } + .ok_or_else(|| crate::CryptoError::MissingKey2(format!("{key_ref:?}"))) + } + + fn get_asymmetric_key( + &self, + key_ref: AsymmKeyRef, + ) -> Result<&AsymmetricCryptoKey, crate::CryptoError> { + if key_ref.is_local() { + self.local_asymmetric_keys.get(key_ref) + } else { + self.global_keys.asymmetric_keys.get(key_ref) + } + .ok_or_else(|| crate::CryptoError::MissingKey2(format!("{key_ref:?}"))) + } +} + +impl<'a, SymmKeyRef: SymmetricKeyRef, AsymmKeyRef: AsymmetricKeyRef> + CryptoEngineContext<'a, SymmKeyRef, AsymmKeyRef> + for RustCryptoEngineContext<'a, SymmKeyRef, AsymmKeyRef> +{ + fn clear(&mut self) { + self.local_symmetric_keys.clear(); + self.local_asymmetric_keys.clear(); + } + + fn decrypt_data_with_symmetric_key( + &self, + key: SymmKeyRef, + data: &EncString, + ) -> Result, crate::CryptoError> { + let key = self.get_symmetric_key(key)?; + + match data { + EncString::AesCbc256_B64 { iv, data } => { + let dec = crate::aes::decrypt_aes256(iv, data.clone(), &key.key)?; + Ok(dec) + } + EncString::AesCbc128_HmacSha256_B64 { iv, mac, data } => { + // TODO: SymmetricCryptoKey is designed to handle 32 byte keys only, but this + // variant uses a 16 byte key This means the key+mac are going to be + // parsed as a single 32 byte key, at the moment we split it manually + // When refactoring the key handling, this should be fixed. + let enc_key = (&key.key[0..16]).into(); + let mac_key = (&key.key[16..32]).into(); + let dec = crate::aes::decrypt_aes128_hmac(iv, mac, data.clone(), mac_key, enc_key)?; + Ok(dec) + } + EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { + let mac_key = key.mac_key.as_ref().ok_or(CryptoError::InvalidMac)?; + let dec = + crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), mac_key, &key.key)?; + Ok(dec) + } + } + } + + fn decrypt_and_store_symmetric_key( + &mut self, + encryption_key: SymmKeyRef, + new_key_ref: SymmKeyRef, + encrypted_key: &EncString, + ) -> Result { + let mut new_key_material = + self.decrypt_data_with_symmetric_key(encryption_key, encrypted_key)?; + + let new_key = SymmetricCryptoKey::try_from(new_key_material.as_mut_slice())?; + self.local_symmetric_keys.insert(new_key_ref, new_key); + Ok(new_key_ref) + } + + fn encrypt_data_with_symmetric_key( + &self, + key: SymmKeyRef, + data: &[u8], + ) -> Result { + let key = self.get_symmetric_key(key)?; + EncString::encrypt_aes256_hmac( + data, + key.mac_key.as_ref().ok_or(CryptoError::InvalidMac)?, + &key.key, + ) + } + + fn encrypt_symmetric_key( + &self, + encryption_key: SymmKeyRef, + key_to_encrypt: SymmKeyRef, + ) -> Result { + let key_to_encrypt = self.get_symmetric_key(key_to_encrypt)?; + self.encrypt_data_with_symmetric_key(encryption_key, &key_to_encrypt.to_vec()) + } + + fn decrypt_data_with_asymmetric_key( + &self, + key: AsymmKeyRef, + data: &AsymmetricEncString, + ) -> Result, crate::CryptoError> { + let key = self.get_asymmetric_key(key)?; + + use AsymmetricEncString::*; + match data { + Rsa2048_OaepSha256_B64 { data } => key.key.decrypt(Oaep::new::(), data), + Rsa2048_OaepSha1_B64 { data } => key.key.decrypt(Oaep::new::(), data), + #[allow(deprecated)] + Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => { + key.key.decrypt(Oaep::new::(), data) + } + #[allow(deprecated)] + Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => { + key.key.decrypt(Oaep::new::(), data) + } + } + .map_err(|_| CryptoError::KeyDecrypt) + } + + fn decrypt_and_store_asymmetric_key( + &mut self, + encryption_key: AsymmKeyRef, + new_key_ref: AsymmKeyRef, + encrypted_key: &AsymmetricEncString, + ) -> Result { + let new_key_material = + self.decrypt_data_with_asymmetric_key(encryption_key, encrypted_key)?; + + let new_key = AsymmetricCryptoKey::from_der(&new_key_material)?; + self.local_asymmetric_keys.insert(new_key_ref, new_key); + Ok(new_key_ref) + } + + fn encrypt_data_with_asymmetric_key( + &self, + key: AsymmKeyRef, + data: &[u8], + ) -> Result { + let key = self.get_asymmetric_key(key)?; + AsymmetricEncString::encrypt_rsa2048_oaep_sha1(data, key) + } + + fn encrypt_asymmetric_key( + &self, + encryption_key: AsymmKeyRef, + key_to_encrypt: AsymmKeyRef, + ) -> Result { + let encryption_key = self.get_asymmetric_key(encryption_key)?; + let key_to_encrypt = self.get_asymmetric_key(key_to_encrypt)?; + + AsymmetricEncString::encrypt_rsa2048_oaep_sha1( + key_to_encrypt.to_der()?.as_slice(), + encryption_key, + ) + } +} diff --git a/crates/bitwarden-crypto/src/service/encryptable.rs b/crates/bitwarden-crypto/src/service/encryptable.rs new file mode 100644 index 000000000..98fc24500 --- /dev/null +++ b/crates/bitwarden-crypto/src/service/encryptable.rs @@ -0,0 +1,130 @@ +use super::{ + key_ref::{AsymmetricKeyRef, KeyRef, SymmetricKeyRef}, + CryptoServiceContext, +}; + +pub trait Encryptable< + SymmKeyRef: SymmetricKeyRef, + AsymmKeyRef: AsymmetricKeyRef, + Key: KeyRef, + Output, +> +{ + fn encrypt( + &self, + ctx: &mut CryptoServiceContext, + key: Key, + ) -> Result; +} + +pub trait Decryptable< + SymmKeyRef: SymmetricKeyRef, + AsymmKeyRef: AsymmetricKeyRef, + Key: KeyRef, + Output, +> +{ + fn decrypt( + &self, + ctx: &mut CryptoServiceContext, + key: Key, + ) -> Result; +} + +impl + Decryptable> for EncString +{ + fn decrypt( + &self, + ctx: &mut CryptoServiceContext, + key: SymmKeyRef, + ) -> Result, crate::CryptoError> { + ctx.engine.decrypt_data_with_symmetric_key(key, self) + } +} + +impl + Decryptable> for AsymmetricEncString +{ + fn decrypt( + &self, + ctx: &mut CryptoServiceContext, + key: AsymmKeyRef, + ) -> Result, crate::CryptoError> { + ctx.engine.decrypt_data_with_asymmetric_key(key, self) + } +} + +impl + Encryptable for [u8] +{ + fn encrypt( + &self, + ctx: &mut CryptoServiceContext, + key: SymmKeyRef, + ) -> Result { + ctx.engine.encrypt_data_with_symmetric_key(key, self) + } +} + +impl + Encryptable for [u8] +{ + fn encrypt( + &self, + ctx: &mut CryptoServiceContext, + key: AsymmKeyRef, + ) -> Result { + ctx.engine.encrypt_data_with_asymmetric_key(key, self) + } +} + +impl + Decryptable for EncString +{ + fn decrypt( + &self, + ctx: &mut CryptoServiceContext, + key: SymmKeyRef, + ) -> Result { + let bytes: Vec = self.decrypt(ctx, key)?; + String::from_utf8(bytes).map_err(|_| CryptoError::InvalidUtf8String) + } +} + +impl + Decryptable for AsymmetricEncString +{ + fn decrypt( + &self, + ctx: &mut CryptoServiceContext, + key: AsymmKeyRef, + ) -> Result { + let bytes: Vec = self.decrypt(ctx, key)?; + String::from_utf8(bytes).map_err(|_| CryptoError::InvalidUtf8String) + } +} + +impl + Encryptable for str +{ + fn encrypt( + &self, + ctx: &mut CryptoServiceContext, + key: SymmKeyRef, + ) -> Result { + self.as_bytes().encrypt(ctx, key) + } +} + +impl + Encryptable for str +{ + fn encrypt( + &self, + ctx: &mut CryptoServiceContext, + key: AsymmKeyRef, + ) -> Result { + self.as_bytes().encrypt(ctx, key) + } +} diff --git a/crates/bitwarden-crypto/src/service/key_ref.rs b/crates/bitwarden-crypto/src/service/key_ref.rs new file mode 100644 index 000000000..f70f25fad --- /dev/null +++ b/crates/bitwarden-crypto/src/service/key_ref.rs @@ -0,0 +1,86 @@ +use std::{fmt::Debug, hash::Hash}; + +use zeroize::ZeroizeOnDrop; + +use crate::{AsymmetricCryptoKey, CryptoKey, SymmetricCryptoKey}; + +/// This trait represents a key reference that can be used to identify cryptographic keys in the key +/// store. It is used to abstract over the different types of keys that can be used in the system, +/// an end user would not implement this trait directly, and would instead use the `SymmetricKeyRef` +/// and `AsymmetricKeyRef` traits. +pub trait KeyRef: + Debug + Clone + Copy + Hash + Eq + PartialEq + Ord + PartialOrd + 'static +{ + type KeyValue: Debug + CryptoKey + ZeroizeOnDrop; + + /// Returns whether the key is local to the current context or shared globally by the service. + fn is_local(&self) -> bool; +} + +// These traits below are just basic aliases of the `KeyRef` trait, but they allow us to have two +// separate trait bounds + +pub trait SymmetricKeyRef: KeyRef {} +pub trait AsymmetricKeyRef: KeyRef {} + +// Hide the `KeyRef` trait from the public API, to avoid confusion +#[doc(hidden)] +pub mod __macro_internal { + pub use super::KeyRef; +} + +// Just a test of a derive_like macro that can be used to generate the key reference enums. +// Example usage: +// ```rust +// key_refs! { +// #[symmetric] +// pub enum KeyRef { +// User, +// Org(Uuid), +// #[local] +// Local(String), +// } +// } +#[macro_export] +macro_rules! key_refs { + ( $( + #[$meta_type:tt] + $(pub)? enum $name:ident { + $( + $( #[$variant_tag:tt] )? + $variant:ident $( ( $inner:ty ) )? + ,)+ + } + )+ ) => { $( + #[derive(std::fmt::Debug, Clone, Copy, std::hash::Hash, Eq, PartialEq, Ord, PartialOrd)] + pub enum $name { $( + $variant $( ($inner) )? + ,)+ } + + impl $crate::service::key_ref::__macro_internal::KeyRef for $name { + type KeyValue = key_refs!(@key_type $meta_type); + + fn is_local(&self) -> bool { + use $name::*; + match self { $( + key_refs!(@variant_match $variant $( ( $inner ) )?) => + key_refs!(@variant_tag $( $variant_tag )? ), + )+ } + } + } + + key_refs!(@key_trait $meta_type $name); + )+ }; + + ( @key_type symmetric ) => { $crate::SymmetricCryptoKey }; + ( @key_type asymmetric ) => { $crate::AsymmetricCryptoKey }; + + ( @key_trait symmetric $name:ident ) => { impl $crate::service::key_ref::SymmetricKeyRef for $name {} }; + ( @key_trait asymmetric $name:ident ) => { impl $crate::service::key_ref::AsymmetricKeyRef for $name {} }; + + ( @variant_match $variant:ident ( $inner:ty ) ) => { $variant (_) }; + ( @variant_match $variant:ident ) => { $variant }; + + ( @variant_tag local ) => { true }; + ( @variant_tag ) => { false }; +} diff --git a/crates/bitwarden-crypto/src/service/key_store/linux_memfd_secret_impl.rs b/crates/bitwarden-crypto/src/service/key_store/linux_memfd_secret_impl.rs new file mode 100644 index 000000000..23f335f0c --- /dev/null +++ b/crates/bitwarden-crypto/src/service/key_store/linux_memfd_secret_impl.rs @@ -0,0 +1,122 @@ +use std::{mem::MaybeUninit, ptr::NonNull}; + +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use super::{ + util::{MemPtr, SliceKeyContainer}, + KeyRef, KeyStore, +}; + +// This is an in-memory key store that is protected by memfd_secret on Linux 5.14+. +// This should be secure against memory dumps from anything except a malicious kernel driver. +// Note that not all 5.14+ systems have support for memfd_secret enabled, so +// LinuxMemfdSecretKeyStore::new returns an Option. +pub(crate) struct LinuxMemfdSecretKeyStore { + container: SliceKeyContainer, + + _key: std::marker::PhantomData, +} + +impl LinuxMemfdSecretKeyStore { + pub(crate) fn new() -> Option { + // This might not be exactly correct in all platforms, but it's a good enough approximation + const PAGE_SIZE: usize = 4096; + let entry_size = std::mem::size_of::>(); + let elements_per_page = PAGE_SIZE / entry_size; + + // We're using mlock APIs to protect the memory, so allocating less than a page is a waste + let capacity = std::cmp::max(32, elements_per_page); + + Self::with_capacity(capacity) + } + + pub(crate) fn with_capacity(capacity: usize) -> Option { + let entry_size = std::mem::size_of::>(); + + let memory = unsafe { + let ptr: NonNull<[u8]> = memsec::memfd_secret_sized(capacity * entry_size)?; + MemPtr::new(ptr, capacity) + }; + + let container = SliceKeyContainer::new(memory); + + // Validate that the entry size is correct + debug_assert_eq!(container.entry_size(), entry_size); + + Some(Self { + container, + _key: std::marker::PhantomData, + }) + } +} + +impl ZeroizeOnDrop for LinuxMemfdSecretKeyStore {} + +impl Drop for LinuxMemfdSecretKeyStore { + fn drop(&mut self) { + // Freeing the memory should clear all the secrets, but to ensure all the Drop impls + // are called correctly we clear the container first + self.container.clear(); + unsafe { + memsec::free_memfd_secret(self.container.inner_mut().as_ptr()); + } + } +} + +impl KeyStore for LinuxMemfdSecretKeyStore { + fn insert(&mut self, key_ref: Key, key: Key::KeyValue) { + if let Err(new_capacity) = self.container.ensure_capacity(1) { + // Create a new store with the correct capacity and replace self with it + let mut new_self = + Self::with_capacity(new_capacity).expect("Failed to allocate new memfd_secret"); + new_self.container.copy_from(&mut self.container); + *self = new_self; + }; + + let ok = self.container.insert(key_ref, key); + debug_assert!(ok, "insert failed"); + } + + fn get(&self, key_ref: Key) -> Option<&Key::KeyValue> { + self.container.get(key_ref) + } + fn remove(&mut self, key_ref: Key) { + self.container.remove(key_ref) + } + fn clear(&mut self) { + self.container.clear() + } + fn retain(&mut self, f: fn(Key) -> bool) { + self.container.retain(f); + } +} + +#[cfg(test)] +mod tests { + use super::{super::util::tests::*, *}; + + #[test] + fn test_resize() { + let mut store = super::LinuxMemfdSecretKeyStore::::with_capacity(1).unwrap(); + + for (idx, key) in [ + TestKey::A, + TestKey::B(10), + TestKey::C, + TestKey::B(7), + TestKey::A, + TestKey::C, + ] + .into_iter() + .enumerate() + { + store.insert(key, TestKeyValue::new(idx)); + } + + assert_eq!(store.get(TestKey::A), Some(&TestKeyValue::new(4))); + assert_eq!(store.get(TestKey::B(10)), Some(&TestKeyValue::new(1))); + assert_eq!(store.get(TestKey::C), Some(&TestKeyValue::new(5))); + assert_eq!(store.get(TestKey::B(7)), Some(&TestKeyValue::new(3))); + assert_eq!(store.get(TestKey::B(20)), None); + } +} diff --git a/crates/bitwarden-crypto/src/service/key_store/mod.rs b/crates/bitwarden-crypto/src/service/key_store/mod.rs new file mode 100644 index 000000000..9a4d75f46 --- /dev/null +++ b/crates/bitwarden-crypto/src/service/key_store/mod.rs @@ -0,0 +1,27 @@ +use zeroize::ZeroizeOnDrop; + +use crate::service::KeyRef; + +#[cfg(target_os = "linux")] +mod linux_memfd_secret_impl; +mod rust_impl; +mod util; + +#[cfg(target_os = "linux")] +pub(crate) use linux_memfd_secret_impl::LinuxMemfdSecretKeyStore; +pub(crate) use rust_impl::RustKeyStore; + +/// This trait represents a platform that can securely store and return keys. The `RustKeyStore` +/// implementation is a simple in-memory store without any security guarantees. Other +/// implementations could use secure enclaves or HSMs, or OS provided keychains. +#[allow(dead_code)] +pub(crate) trait KeyStore: ZeroizeOnDrop { + fn insert(&mut self, key_ref: Key, key: Key::KeyValue); + fn get(&self, key_ref: Key) -> Option<&Key::KeyValue>; + fn remove(&mut self, key_ref: Key); + fn clear(&mut self); + + fn retain(&mut self, f: fn(Key) -> bool); +} + +fn _ensure_that_trait_is_object_safe(_: Box>) {} diff --git a/crates/bitwarden-crypto/src/service/key_store/rust_impl.rs b/crates/bitwarden-crypto/src/service/key_store/rust_impl.rs new file mode 100644 index 000000000..1944a58cd --- /dev/null +++ b/crates/bitwarden-crypto/src/service/key_store/rust_impl.rs @@ -0,0 +1,129 @@ +use zeroize::ZeroizeOnDrop; + +use super::{util::SliceKeyContainer, KeyRef, KeyStore}; + +// This is a basic in-memory key store for the cases where we don't have a secure key store +// available. We still make use mlock to protect the memory from being swapped to disk, and we +// zeroize the values when dropped. +pub(crate) struct RustKeyStore { + #[allow(clippy::type_complexity)] + container: SliceKeyContainer]>>, +} + +const ENABLE_MLOCK: bool = true; + +impl RustKeyStore { + pub(crate) fn new() -> Self { + // This might not be exactly correct in all platforms, but it's a good enough approximation + const PAGE_SIZE: usize = 4096; + let entry_size = std::mem::size_of::>(); + + let entries_per_page = PAGE_SIZE / entry_size; + + // We're using mlock APIs to protect the memory, so allocating less than a page is a waste + let capacity = std::cmp::max(32, entries_per_page); + + Self::with_capacity(capacity) + } + + pub(crate) fn with_capacity(capacity: usize) -> Self { + let entry_size = std::mem::size_of::>(); + + // This is a bit awkward, but we need to fill the entire slice with None, and we can't just + // use vec![None; capacity] because that requires adding a Clone bound to the key + // value + let mut keys: Box<_> = std::iter::repeat_with(|| None).take(capacity).collect(); + + if ENABLE_MLOCK { + unsafe { + memsec::mlock(keys.as_mut_ptr() as *mut u8, capacity * entry_size); + } + } + + let container = SliceKeyContainer::new(keys); + + // Validate that the entry size is correct + debug_assert_eq!(container.entry_size(), entry_size); + + Self { container } + } +} + +impl ZeroizeOnDrop for RustKeyStore {} + +impl Drop for RustKeyStore { + fn drop(&mut self) { + if ENABLE_MLOCK { + // We need to ensure the values get dropped and zeroized _before_ + // the mlock gets removed, to avoid any last minute swaps to disk + self.container.clear(); + + unsafe { + memsec::munlock( + self.container.inner_mut().as_mut_ptr() as *mut u8, + self.container.byte_len(), + ); + } + } + } +} + +impl KeyStore for RustKeyStore { + fn insert(&mut self, key_ref: Key, key: Key::KeyValue) { + if let Err(new_capacity) = self.container.ensure_capacity(1) { + // Create a new store with the correct capacity and replace self with it + let mut new_self = Self::with_capacity(new_capacity); + new_self.container.copy_from(&mut self.container); + *self = new_self; + }; + + let ok = self.container.insert(key_ref, key); + debug_assert!(ok, "insert failed"); + } + + fn get(&self, key_ref: Key) -> Option<&Key::KeyValue> { + self.container.get(key_ref) + } + + fn remove(&mut self, key_ref: Key) { + self.container.remove(key_ref); + } + + fn clear(&mut self) { + self.container.clear(); + } + + fn retain(&mut self, f: fn(Key) -> bool) { + self.container.retain(f); + } +} + +#[cfg(test)] +mod tests { + use super::{super::util::tests::*, *}; + + #[test] + fn test_resize() { + let mut store = super::RustKeyStore::::with_capacity(1); + + for (idx, key) in [ + TestKey::A, + TestKey::B(10), + TestKey::C, + TestKey::B(7), + TestKey::A, + TestKey::C, + ] + .into_iter() + .enumerate() + { + store.insert(key, TestKeyValue::new(idx)); + } + + assert_eq!(store.get(TestKey::A), Some(&TestKeyValue::new(4))); + assert_eq!(store.get(TestKey::B(10)), Some(&TestKeyValue::new(1))); + assert_eq!(store.get(TestKey::C), Some(&TestKeyValue::new(5))); + assert_eq!(store.get(TestKey::B(7)), Some(&TestKeyValue::new(3))); + assert_eq!(store.get(TestKey::B(20)), None); + } +} diff --git a/crates/bitwarden-crypto/src/service/key_store/util.rs b/crates/bitwarden-crypto/src/service/key_store/util.rs new file mode 100644 index 000000000..2b527d14a --- /dev/null +++ b/crates/bitwarden-crypto/src/service/key_store/util.rs @@ -0,0 +1,719 @@ +use crate::service::key_ref::KeyRef; + +pub(crate) trait AsSlice { + fn as_slice(&self) -> &[Option<(Key, Key::KeyValue)>]; + fn as_mut_slice(&mut self) -> &mut [Option<(Key, Key::KeyValue)>]; +} + +impl AsSlice for Box<[Option<(Key, Key::KeyValue)>]> { + fn as_slice(&self) -> &[Option<(Key, Key::KeyValue)>] { + self.as_ref() + } + + fn as_mut_slice(&mut self) -> &mut [Option<(Key, Key::KeyValue)>] { + self.as_mut() + } +} + +#[cfg(target_os = "linux")] +pub(crate) struct MemPtr { + ptr: std::ptr::NonNull<[u8]>, + capacity: usize, +} + +#[cfg(target_os = "linux")] +impl MemPtr { + /// SAFETY: The caller must ensure that the pointer is valid, correctly aligned + pub unsafe fn new(ptr: std::ptr::NonNull<[u8]>, capacity: usize) -> MemPtr { + MemPtr { ptr, capacity } + } + + pub unsafe fn as_ptr(&self) -> std::ptr::NonNull<[u8]> { + self.ptr + } +} + +#[cfg(target_os = "linux")] +impl AsSlice for MemPtr { + fn as_slice(&self) -> &[Option<(Key, Key::KeyValue)>] { + let ptr = self.ptr.as_ptr() as *const Option<(Key, Key::KeyValue)>; + // SAFETY: The pointer is valid and points to a valid slice of the correct size. + unsafe { std::slice::from_raw_parts(ptr, self.capacity) } + } + + fn as_mut_slice(&mut self) -> &mut [Option<(Key, Key::KeyValue)>] { + let ptr = self.ptr.as_ptr() as *mut Option<(Key, Key::KeyValue)>; + // SAFETY: The pointer is valid and points to a valid slice of the correct size. + unsafe { std::slice::from_raw_parts_mut(ptr, self.capacity) } + } +} + +/// This represents a container over an arbitrary fixed size slice. +/// This is meant to abstract over the different ways to store keys in memory, +/// whether we're using a Vec, a Box<[u8]> or a NonNull. +pub(crate) struct SliceKeyContainer> { + data: Data, + + // This represents the number of elements in the container, it's always less than or equal to + // the length of `data`. + length: usize, + + // This represents the maximum number of elements that can be stored in the container. + capacity: usize, + + _key: std::marker::PhantomData, +} + +#[allow(dead_code)] +impl> SliceKeyContainer { + pub(crate) fn new(data: S) -> Self { + let capacity = data.as_slice().len(); + + debug_assert!( + capacity > 0, + "The container should have a capacity of at least 1" + ); + + let mut container = Self { + data, + length: 0, + capacity, + _key: std::marker::PhantomData, + }; + + // Ensure the container is properly initialized + container.clear(); + + container + } + + pub(crate) const fn entry_size(&self) -> usize { + std::mem::size_of::>() + } + + pub(crate) unsafe fn inner_mut(&mut self) -> &mut S { + &mut self.data + } + + pub(crate) fn len(&self) -> usize { + self.length + } + + pub(crate) fn byte_len(&self) -> usize { + self.length * self.entry_size() + } + + /// Check if the container has enough capacity to store `new_elements` more elements. + /// If the result is Ok, the container has enough capacity. + /// If it's Err, the container needs to be resized. + /// The error value returns a suggested new capacity. + pub(crate) fn ensure_capacity(&self, new_elements: usize) -> Result<(), usize> { + let new_size = self.length + new_elements; + + if new_size > self.capacity { + // We want to increase the capacity by a multiple to be mostly aligned with page size, + // we also need to make sure that we have enough space for the new elements, so we round + // up + let increase_factor = usize::div_ceil(new_size, self.capacity); + Err(self.capacity * increase_factor) + } else { + Ok(()) + } + } + + fn find_by_key_ref(&self, key_ref: &Key) -> Result { + // Because we know all the None's are at the end and all the Some values are at the + // beginning, we only need to search for the key in the first `size` elements. + let slice = &self.data.as_slice()[..self.length]; + + // This structure is almost always used for reads instead of writes, so we can use a binary + // search to optimize for the read case. + slice.binary_search_by(|k| { + debug_assert!( + k.is_some(), + "We should never have a None value in the middle of the slice" + ); + + match k { + Some((k, _)) => k.cmp(key_ref), + None => std::cmp::Ordering::Greater, + } + }) + } + + pub(crate) fn clear(&mut self) { + self.data.as_mut_slice().fill_with(|| None); + self.length = 0; + } + + pub(crate) fn remove(&mut self, key_ref: Key) { + if let Ok(idx) = self.find_by_key_ref(&key_ref) { + let slice = self.data.as_mut_slice(); + slice[idx] = None; + slice[idx..self.length].rotate_left(1); + self.length -= 1; + } + } + + pub(crate) fn insert(&mut self, key_ref: Key, key: ::KeyValue) -> bool { + match self.find_by_key_ref(&key_ref) { + Ok(idx) => { + // Key already exists, we just need to replace the value + let slice = self.data.as_mut_slice(); + slice[idx] = Some((key_ref, key)); + } + Err(idx) => { + // We need to insert the key, check if we have enough space + if self.length >= self.capacity { + return false; + } + + let slice = self.data.as_mut_slice(); + if idx < self.length { + // If we're not right at the end, we have to shift all the following elements + // one position to the right + slice[idx..=self.length].rotate_right(1); + } + slice[idx] = Some((key_ref, key)); + self.length += 1; + } + } + + true + } + + pub(crate) fn get(&self, key_ref: Key) -> Option<&::KeyValue> { + self.find_by_key_ref(&key_ref) + .ok() + .and_then(|idx| self.data.as_slice().get(idx)) + .and_then(|f| f.as_ref().map(|f| &f.1)) + } + + pub(crate) fn retain(&mut self, f: fn(Key) -> bool) { + let slice = self.data.as_mut_slice(); + + let mut removed_elements = 0; + + for value in slice.iter_mut().take(self.length) { + let key = value + .as_ref() + .map(|e| e.0) + .expect("Values in a slice are always Some"); + + if !f(key) { + *value = None; + removed_elements += 1; + } + } + + // If we haven't removed any elements, we don't need to compact the slice + if removed_elements == 0 { + return; + } + + // Remove all the None values from the middle of the slice + + for idx in 0..self.length { + if slice[idx].is_none() { + slice[idx..self.length].rotate_left(1); + } + } + + self.length -= removed_elements; + } + + pub(crate) fn copy_from(&mut self, other: &mut Self) -> bool { + if other.capacity > self.capacity { + return false; + } + + // Empty the current container + self.clear(); + + // Move the data from the other container + let this = self.data.as_mut_slice(); + let that = other.data.as_mut_slice(); + for idx in 0..other.length { + std::mem::swap(&mut this[idx], &mut that[idx]); + } + + // Update the length + self.length = other.length; + + true + } +} + +#[cfg(test)] +pub(crate) mod tests { + use zeroize::Zeroize; + + use super::*; + use crate::{service::key_ref::KeyRef, CryptoKey}; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub enum TestKey { + A, + B(u8), + C, + } + #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] + pub struct TestKeyValue([u8; 16]); + impl zeroize::ZeroizeOnDrop for TestKeyValue {} + impl CryptoKey for TestKeyValue {} + impl TestKeyValue { + pub fn new(value: usize) -> Self { + // Just fill the array with some values + let mut key = [0; 16]; + key[0..8].copy_from_slice(&value.to_le_bytes()); + key[8..16].copy_from_slice(&value.to_be_bytes()); + Self(key) + } + } + + impl Drop for TestKeyValue { + fn drop(&mut self) { + self.0.as_mut().zeroize(); + } + } + + impl KeyRef for TestKey { + type KeyValue = TestKeyValue; + + fn is_local(&self) -> bool { + false + } + } + + #[test] + fn test_slice_container_insertion() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + + // Insert one key, which should be at the beginning + assert!(container.insert(TestKey::B(10), TestKeyValue::new(110))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + None, + None, + None, + None + ] + ); + + // Insert a key that should be right after the first one + assert!(container.insert(TestKey::C, TestKeyValue::new(1000))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::C, TestKeyValue::new(1000))), + None, + None, + None + ] + ); + + // Insert a key in the middle + assert!(container.insert(TestKey::B(20), TestKeyValue::new(210))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::C, TestKeyValue::new(1000))), + None, + None + ] + ); + + // Insert a key right at the start + assert!(container.insert(TestKey::A, TestKeyValue::new(0))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(0))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::C, TestKeyValue::new(1000))), + None + ] + ); + + // Insert a key in the middle, which fills the container + assert!(container.insert(TestKey::B(30), TestKeyValue::new(310))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(0))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + // Replacing an existing value at the start + assert!(container.insert(TestKey::A, TestKeyValue::new(1))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + // Replacing an existing value at the middle + assert!(container.insert(TestKey::B(20), TestKeyValue::new(211))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(211))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + // Replacing an existing value at the end + assert!(container.insert(TestKey::C, TestKeyValue::new(1001))); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(211))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1001))), + ] + ); + } + + #[test] + fn test_slice_container_get() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + assert_eq!(container.get(TestKey::A), Some(&TestKeyValue::new(1))); + assert_eq!(container.get(TestKey::B(10)), Some(&TestKeyValue::new(110))); + assert_eq!(container.get(TestKey::B(20)), None); + assert_eq!(container.get(TestKey::C), Some(&TestKeyValue::new(1000))); + } + + #[test] + fn test_slice_container_clear() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + + container.clear(); + + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + } + + #[test] + fn test_slice_container_ensure_capacity() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + assert_eq!(container.capacity, 5); + assert_eq!(container.length, 0); + + assert_eq!(container.ensure_capacity(0), Ok(())); + assert_eq!(container.ensure_capacity(6), Err(10)); + assert_eq!(container.ensure_capacity(10), Err(10)); + assert_eq!(container.ensure_capacity(11), Err(15)); + assert_eq!(container.ensure_capacity(51), Err(55)); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + assert_eq!(container.ensure_capacity(0), Ok(())); + assert_eq!(container.ensure_capacity(6), Err(15)); + assert_eq!(container.ensure_capacity(10), Err(15)); + assert_eq!(container.ensure_capacity(11), Err(20)); + assert_eq!(container.ensure_capacity(51), Err(60)); + } + + #[test] + fn test_slice_container_removal() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + // Remove the last element + container.remove(TestKey::C); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + ] + ); + + // Remove the first element + container.remove(TestKey::A); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove a non-existing element + container.remove(TestKey::A); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove an element in the middle + container.remove(TestKey::B(20)); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None, + None + ] + ); + + // Remove all the remaining elements + container.remove(TestKey::B(30)); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + None, + None, + None, + None + ] + ); + container.remove(TestKey::B(10)); + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + + // Remove from an empty container + container.remove(TestKey::B(10)); + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + } + + #[test] + fn test_slice_container_retain_removes_one() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + // Remove the last element + container.retain(|k| k != TestKey::C); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + ] + ); + + // Remove the first element + container.retain(|k| k != TestKey::A); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove a non-existing element + container.retain(|k| k != TestKey::A); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None + ] + ); + + // Remove an element in the middle + container.retain(|k| k != TestKey::B(20)); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(30), TestKeyValue::new(310))), + None, + None, + None + ] + ); + + // Remove all the remaining elements + container.retain(|k| k != TestKey::B(30)); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::B(10), TestKeyValue::new(110))), + None, + None, + None, + None + ] + ); + container.retain(|k| k != TestKey::B(10)); + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + + // Remove from an empty container + container.retain(|k| k != TestKey::B(10)); + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + } + + #[test] + fn test_slice_container_retain_removes_none() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + container.retain(|_k| true); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(10), TestKeyValue::new(110))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::B(30), TestKeyValue::new(310))), + Some((TestKey::C, TestKeyValue::new(1000))), + ] + ); + } + + #[test] + fn test_slice_container_retain_removes_some() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + container.retain(|k| matches!(k, TestKey::A | TestKey::B(20) | TestKey::C)); + assert_eq!( + container.data.as_slice(), + [ + Some((TestKey::A, TestKeyValue::new(1))), + Some((TestKey::B(20), TestKeyValue::new(210))), + Some((TestKey::C, TestKeyValue::new(1000))), + None, + None, + ] + ); + } + + #[test] + fn test_slice_container_retain_removes_all() { + let mut container = SliceKeyContainer::::new(vec![None; 5].into_boxed_slice()); + + for (key, value) in [ + (TestKey::A, TestKeyValue::new(1)), + (TestKey::B(10), TestKeyValue::new(110)), + (TestKey::B(20), TestKeyValue::new(210)), + (TestKey::B(30), TestKeyValue::new(310)), + (TestKey::C, TestKeyValue::new(1000)), + ] { + assert!(container.insert(key, value)); + } + + container.retain(|_k| false); + assert_eq!(container.data.as_slice(), [None, None, None, None, None]); + } +} diff --git a/crates/bitwarden-crypto/src/service/mod.rs b/crates/bitwarden-crypto/src/service/mod.rs new file mode 100644 index 000000000..a1416e776 --- /dev/null +++ b/crates/bitwarden-crypto/src/service/mod.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use crate::{EncString, SymmetricCryptoKey}; + +mod crypto_engine; +mod encryptable; +pub mod key_ref; +mod key_store; + +use crypto_engine::{CryptoEngine, CryptoEngineContext, RustCryptoEngine}; +pub use encryptable::{Decryptable, Encryptable}; +use key_ref::{AsymmetricKeyRef, KeyRef, SymmetricKeyRef}; + +#[derive(Clone)] +pub struct CryptoService { + // We use an Arc<> to make it easier to pass this service around, as we can + // clone it instead of passing references + engine: Arc>, +} + +impl + CryptoService +{ + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + engine: Arc::new(RustCryptoEngine::new()), + } + } + + pub fn clear(&self) { + self.engine.clear(); + } + + #[deprecated(note = "We should be generating/decrypting the keys into the service directly")] + pub fn insert_symmetric_key(&self, key_ref: SymmKeyRef, key: SymmetricCryptoKey) { + #[allow(deprecated)] + self.engine.insert_symmetric_key(key_ref, key); + } + + pub fn context(&'_ self) -> CryptoServiceContext<'_, SymmKeyRef, AsymmKeyRef> { + CryptoServiceContext { + engine: self.engine.context(), + } + } + + // These are just convenience methods to avoid having to call `context` every time + pub fn decrypt, Output>( + &self, + key: Key, + data: &Data, + ) -> Result { + data.decrypt(&mut self.context(), key) + } + + pub fn encrypt, Output>( + &self, + key: Key, + data: Data, + ) -> Result { + data.encrypt(&mut self.context(), key) + } +} + +pub struct CryptoServiceContext<'a, SymmKeyRef: SymmetricKeyRef, AsymmKeyRef: AsymmetricKeyRef> { + engine: Box + 'a>, +} + +impl<'a, SymmKeyRef: SymmetricKeyRef, AsymmKeyRef: AsymmetricKeyRef> + CryptoServiceContext<'a, SymmKeyRef, AsymmKeyRef> +{ + /// Decrypt a key and store it in the local key store + pub fn decrypt_and_store_symmetric_key( + &mut self, + encryption_key: SymmKeyRef, + new_key_ref: SymmKeyRef, + encrypted_key: &EncString, + ) -> Result { + self.engine + .decrypt_and_store_symmetric_key(encryption_key, new_key_ref, encrypted_key) + } +}