From 32ca764888b3d6fb5152c019c8c5c439f97c8580 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 6 Oct 2025 11:46:03 -0500 Subject: [PATCH 01/12] Add methods to create rotateable key sets from PRF --- .../src/key_management/crypto.rs | 17 +- .../src/key_management/crypto_client.rs | 10 +- crates/bitwarden-crypto/src/keys/mod.rs | 2 + crates/bitwarden-crypto/src/keys/prf.rs | 44 ++++ crates/bitwarden-crypto/src/lib.rs | 3 +- .../src/store/key_rotation.rs | 201 +++++++++++++++++- crates/bitwarden-uniffi/src/crypto.rs | 8 +- 7 files changed, 275 insertions(+), 10 deletions(-) create mode 100644 crates/bitwarden-crypto/src/keys/prf.rs diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index aa53b9245..b6fa67a10 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -8,9 +8,10 @@ use std::collections::HashMap; use bitwarden_crypto::{ AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable, - KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm, - SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, - UserKey, dangerous_get_v2_rotated_account_keys, safe::PasswordProtectedKeyEnvelopeError, + KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, RotateableKeySet, + SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, dangerous_get_v2_rotated_account_keys, + derive_symmetric_key_from_prf, safe::PasswordProtectedKeyEnvelopeError, }; use bitwarden_encoding::B64; use bitwarden_error::bitwarden_error; @@ -498,6 +499,16 @@ fn derive_pin_protected_user_key( Ok(derived_key.encrypt_user_key(user_key)?) } +pub(super) fn make_prf_user_key_set( + client: &Client, + prf: B64, +) -> Result { + let prf_key = derive_symmetric_key_from_prf(prf.as_bytes())?; + let ctx = client.internal.get_key_store().context(); + let key_set = RotateableKeySet::new(&ctx, &prf_key, SymmetricKeyId::User)?; + Ok(key_set) +} + #[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 08a18168f..712a86006 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::{CryptoError, Decryptable, Kdf}; +use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet}; #[cfg(feature = "internal")] use bitwarden_crypto::{EncString, UnsignedSharedKey}; use bitwarden_encoding::B64; @@ -18,7 +18,7 @@ use crate::key_management::{ crypto::{ DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse, derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key, - initialize_org_crypto, initialize_user_crypto, + initialize_org_crypto, initialize_user_crypto, make_prf_user_key_set, }, }; use crate::{ @@ -172,6 +172,12 @@ impl CryptoClient { derive_pin_user_key(&self.client, encrypted_pin) } + /// Creates a new rotateable key set for the current user key protected + /// by a key derived from the given PRF. + pub fn make_prf_user_key_set(&self, prf: B64) -> Result { + make_prf_user_key_set(&self.client, prf) + } + /// Prepares the account for being enrolled in the admin password reset feature. This encrypts /// the users [UserKey][bitwarden_crypto::UserKey] with the organization's public key. pub fn enroll_admin_password_reset( diff --git a/crates/bitwarden-crypto/src/keys/mod.rs b/crates/bitwarden-crypto/src/keys/mod.rs index 1e6cda4db..f0a263ddb 100644 --- a/crates/bitwarden-crypto/src/keys/mod.rs +++ b/crates/bitwarden-crypto/src/keys/mod.rs @@ -33,4 +33,6 @@ pub use kdf::{ default_pbkdf2_iterations, }; pub(crate) use key_id::{KEY_ID_SIZE, KeyId}; +mod prf; pub(crate) mod utils; +pub use prf::derive_symmetric_key_from_prf; diff --git a/crates/bitwarden-crypto/src/keys/prf.rs b/crates/bitwarden-crypto/src/keys/prf.rs new file mode 100644 index 000000000..84d08cbaf --- /dev/null +++ b/crates/bitwarden-crypto/src/keys/prf.rs @@ -0,0 +1,44 @@ +use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key}; + +/// Takes the output of a PRF and derives a symmetric key +pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result { + let (secret, _) = prf + .split_at_checked(32) + .ok_or_else(|| CryptoError::InvalidKeyLen)?; + let secret: [u8; 32] = secret.try_into().unwrap(); + if secret.iter().all(|b| *b == b'\0') { + return Err(CryptoError::ZeroNumber); + } + Ok(SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key( + &Box::pin(secret.into()), + )?)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_prf_succeeds() { + let prf = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, + ]; + derive_symmetric_key_from_prf(&prf).unwrap(); + } + + #[test] + fn test_zero_key_fails() { + let prf = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, + ]; + let err = derive_symmetric_key_from_prf(&prf).unwrap_err(); + assert!(matches!(err, CryptoError::ZeroNumber)); + } + #[test] + fn test_short_prf_fails() { + let prf = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + let err = derive_symmetric_key_from_prf(&prf).unwrap_err(); + assert!(matches!(err, CryptoError::InvalidKeyLen)); + } +} diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index 34de79131..a76d20168 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -32,7 +32,8 @@ mod wordlist; pub use wordlist::EFF_LONG_WORD_LIST; mod store; pub use store::{ - KeyStore, KeyStoreContext, RotatedUserKeys, dangerous_get_v2_rotated_account_keys, + KeyStore, KeyStoreContext, RotateableKeySet, RotatedUserKeys, + dangerous_get_v2_rotated_account_keys, }; mod cose; pub use cose::CoseSerializable; diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 0d97331c2..57cb1db13 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -1,7 +1,10 @@ +use serde::{Deserialize, Serialize}; + use crate::{ - CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyIds, - KeyStoreContext, SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes, - SymmetricCryptoKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError, + EncString, KeyDecryptable, KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes, + SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, }; /// Rotated set of account keys @@ -45,6 +48,123 @@ pub fn dangerous_get_v2_rotated_account_keys( }) } +/// A set of keys where a given `EncryptionKey` is protected by an encrypted public/private +/// key-pair. The `EncryptionKey` is used to encrypt/decrypt data, while the public/private key-pair +/// is used to rotate the `EncryptionKey`. +/// +/// The `PrivateKey` is protected by an `ExternalKey`, such as a `DeviceKey`, or `PrfKey`, +/// and the `PublicKey` is protected by the `EncryptionKey`. This setup allows: +/// +/// - Access to `EncryptionKey` by knowing the `ExternalKey` +/// - Rotation to a `NewEncryptionKey` by knowing the current `EncryptionKey`, without needing access to +/// the `ExternalKey` +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +pub struct RotateableKeySet { + /// `EncryptionKey` protected by encapsulation key + encapsulated_encryption_key: UnsignedSharedKey, + /// Encapsulation key protected by `EncryptionKey` + encrypted_encapsulation_key: EncString, + /// Decapsulation key protected by `ExternalKey` + wrapped_decapsulation_key: EncString, +} + +impl RotateableKeySet { + /// Create a set of keys to allow access to the user key via the provided + /// symmetric wrapping key while allowing the user key to be rotated. + pub fn new( + ctx: &KeyStoreContext, + wrapping_key: &SymmetricCryptoKey, + key_to_wrap: Ids::Symmetric, + ) -> Result { + let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1); + + // This uses this deprecated method and other methods directly on the other keys + // rather than the key store context because we don't want the keys to + // wind up being stored in the borrowed context. + #[allow(deprecated)] + let key_to_wrap_instance = ctx.dangerous_get_symmetric_key(key_to_wrap)?; + // encapsulate encryption key + let encapsulated_encryption_key = UnsignedSharedKey::encapsulate_key_unsigned( + key_to_wrap_instance, + &key_pair.to_public_key(), + )?; + + // wrap decapsulation key + let wrapped_decapsulation_key = key_pair.to_der()?.encrypt_with_key(wrapping_key)?; + + // wrap encapsulation key with encryption key + // Note: Usually, a public key is - by definition - public, so this should not be necessary. + // The specific use-case for this function is to enable rotateable key sets, where + // the "public key" is not public, with the intent of preventing the server from being able + // to overwrite the user key unlocked by the rotateable keyset. + let encrypted_encapsulation_key = key_pair + .to_public_key() + .to_der()? + .encrypt_with_key(&key_to_wrap_instance)?; + + Ok(RotateableKeySet { + encapsulated_encryption_key, + encrypted_encapsulation_key, + wrapped_decapsulation_key, + }) + } + + // TODO: Eventually, the webauthn-login-strategy service should be migrated + // to use this method, and we can remove the #[allow(dead_code)] attribute. + #[allow(dead_code)] + fn unlock( + &self, + ctx: &mut KeyStoreContext, + unwrapping_key: &SymmetricCryptoKey, + key_to_unwrap: Ids::Symmetric, + ) -> Result<(), CryptoError> { + let priv_key_bytes: Vec = self + .wrapped_decapsulation_key + .decrypt_with_key(unwrapping_key)?; + let decapsulation_key = + AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?; + let encryption_key = self + .encapsulated_encryption_key + .decapsulate_key_unsigned(&decapsulation_key)?; + #[allow(deprecated)] + ctx.set_symmetric_key(key_to_unwrap, encryption_key)?; + Ok(()) + } +} + +fn rotate_key_set( + ctx: &KeyStoreContext, + key_set: RotateableKeySet, + old_encryption_key_id: Ids::Symmetric, + new_encryption_key_id: Ids::Symmetric, +) -> Result { + let pub_key_bytes = ctx.decrypt_data_with_symmetric_key( + old_encryption_key_id, + &key_set.encrypted_encapsulation_key, + )?; + let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes); + let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?; + // TODO: There is no method to store only the public key in the store, so we + // have pull out the encryption key to encapsulate it manually. + #[allow(deprecated)] + let new_encryption_key = ctx.dangerous_get_symmetric_key(new_encryption_key_id)?; + let new_encapsulated_key = + UnsignedSharedKey::encapsulate_key_unsigned(new_encryption_key, &encapsulation_key)?; + let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_encryption_key)?; + Ok(RotateableKeySet { + encapsulated_encryption_key: new_encapsulated_key, + encrypted_encapsulation_key: new_encrypted_encapsulation_key, + wrapped_decapsulation_key: key_set.wrapped_decapsulation_key, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -137,4 +257,79 @@ mod tests { .unwrap() ); } + + #[test] + fn test_rotateable_key_set_can_unlock() { + // generate initial keys + let external_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + // set up store + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let original_encryption_key_id = TestSymmKey::A(0); + ctx.generate_symmetric_key(original_encryption_key_id) + .unwrap(); + + // create key set + let key_set = + RotateableKeySet::new(&ctx, &external_key, original_encryption_key_id).unwrap(); + + // unlock key set + let unwrapped_encryption_key_id = TestSymmKey::A(1); + key_set + .unlock(&mut ctx, &external_key, unwrapped_encryption_key_id) + .unwrap(); + + #[allow(deprecated)] + let original_key = ctx + .dangerous_get_symmetric_key(original_encryption_key_id) + .unwrap(); + #[allow(deprecated)] + let unwrapped_key = ctx + .dangerous_get_symmetric_key(unwrapped_encryption_key_id) + .unwrap(); + assert_eq!(original_key, unwrapped_key); + } + + #[test] + fn test_rotateable_key_set_rotation() { + // generate initial keys + let external_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + // set up store + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let original_encryption_key_id = TestSymmKey::A(1); + ctx.generate_symmetric_key(original_encryption_key_id) + .unwrap(); + + // create key set + let key_set = + RotateableKeySet::new(&ctx, &external_key, original_encryption_key_id).unwrap(); + + // rotate + let new_encryption_key_id = TestSymmKey::A(2_1); + ctx.generate_symmetric_key(new_encryption_key_id).unwrap(); + let new_key_set = rotate_key_set( + &ctx, + key_set, + original_encryption_key_id, + new_encryption_key_id, + ) + .unwrap(); + + // After rotation, the new key set should be unlocked by the same + // external key and return the new encryption key. + let unwrapped_encryption_key_id = TestSymmKey::A(2_2); + new_key_set + .unlock(&mut ctx, &external_key, unwrapped_encryption_key_id) + .unwrap(); + #[allow(deprecated)] + let new_encryption_key = ctx + .dangerous_get_symmetric_key(new_encryption_key_id) + .unwrap(); + #[allow(deprecated)] + let unwrapped_encryption_key = ctx + .dangerous_get_symmetric_key(unwrapped_encryption_key_id) + .unwrap(); + assert_eq!(new_encryption_key, unwrapped_encryption_key); + } } diff --git a/crates/bitwarden-uniffi/src/crypto.rs b/crates/bitwarden-uniffi/src/crypto.rs index d43881ed0..d253059ad 100644 --- a/crates/bitwarden-uniffi/src/crypto.rs +++ b/crates/bitwarden-uniffi/src/crypto.rs @@ -2,7 +2,7 @@ use bitwarden_core::key_management::crypto::{ DeriveKeyConnectorRequest, DerivePinKeyResponse, EnrollPinResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdateKdfResponse, UpdatePasswordResponse, }; -use bitwarden_crypto::{EncString, Kdf, UnsignedSharedKey}; +use bitwarden_crypto::{EncString, Kdf, RotateableKeySet, UnsignedSharedKey}; use bitwarden_encoding::B64; use crate::error::Result; @@ -88,6 +88,12 @@ impl CryptoClient { Ok(self.0.derive_key_connector(request)?) } + /// Creates the a new rotateable key set for the current user key protected + /// by a key derived from the given PRF. + pub fn make_prf_user_key_set(&self, prf: B64) -> Result { + Ok(self.0.make_prf_user_key_set(prf)?) + } + /// Create the data necessary to update the user's kdf settings. The user's encryption key is /// re-encrypted for the password under the new kdf settings. This returns the new encrypted /// user key and the new password hash but does not update sdk state. From 4b8b249c0f9505d159e49959e04ab9e052faa045 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Mon, 6 Oct 2025 13:42:06 -0500 Subject: [PATCH 02/12] Mark unused method --- crates/bitwarden-crypto/src/store/key_rotation.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 57cb1db13..90ea12674 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -139,6 +139,7 @@ impl RotateableKeySet { } } +#[allow(dead_code)] fn rotate_key_set( ctx: &KeyStoreContext, key_set: RotateableKeySet, From eabb6cfc1fa4fd92da4c268481295cd49e024807 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 7 Oct 2025 11:20:42 -0500 Subject: [PATCH 03/12] Clarify implicit PRF truncation --- crates/bitwarden-crypto/src/keys/prf.rs | 36 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden-crypto/src/keys/prf.rs b/crates/bitwarden-crypto/src/keys/prf.rs index 84d08cbaf..3591460af 100644 --- a/crates/bitwarden-crypto/src/keys/prf.rs +++ b/crates/bitwarden-crypto/src/keys/prf.rs @@ -1,11 +1,14 @@ use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key}; -/// Takes the output of a PRF and derives a symmetric key +/// Takes the output of a PRF and derives a symmetric key. +/// +/// The PRF output must be at least 32 bytes long. pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result { let (secret, _) = prf .split_at_checked(32) .ok_or_else(|| CryptoError::InvalidKeyLen)?; let secret: [u8; 32] = secret.try_into().unwrap(); + // Don't allow uninitialized PRFs if secret.iter().all(|b| *b == b'\0') { return Err(CryptoError::ZeroNumber); } @@ -17,28 +20,39 @@ pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result = (0..32).map(|_| 0).collect(); let err = derive_symmetric_key_from_prf(&prf).unwrap_err(); assert!(matches!(err, CryptoError::ZeroNumber)); } + #[test] fn test_short_prf_fails() { - let prf = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + let prf = pseudorandom_bytes(9); let err = derive_symmetric_key_from_prf(&prf).unwrap_err(); assert!(matches!(err, CryptoError::InvalidKeyLen)); } + + #[test] + fn test_long_prf_truncated_to_proper_length() { + let long_prf = pseudorandom_bytes(33); + let prf = pseudorandom_bytes(32); + let key1 = derive_symmetric_key_from_prf(&long_prf).unwrap(); + let key2 = derive_symmetric_key_from_prf(&prf).unwrap(); + assert_eq!(key1, key2); + } + + /// This returns the same bytes deterministically for a given length. + fn pseudorandom_bytes(len: usize) -> Vec { + (0..len).map(|x| (x % 255) as u8).collect() + } } From 748f6d64f705be7bf87752b45b6bb51feb2f0fd2 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 7 Oct 2025 12:56:22 -0500 Subject: [PATCH 04/12] Use upstream/downstream key terminology for rotateable key set --- .../src/store/key_rotation.rs | 144 +++++++++--------- 1 file changed, 71 insertions(+), 73 deletions(-) diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 90ea12674..3162af132 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -48,16 +48,16 @@ pub fn dangerous_get_v2_rotated_account_keys( }) } -/// A set of keys where a given `EncryptionKey` is protected by an encrypted public/private -/// key-pair. The `EncryptionKey` is used to encrypt/decrypt data, while the public/private key-pair -/// is used to rotate the `EncryptionKey`. +/// A set of keys where a given `DownstreamKey` is protected by an encrypted public/private +/// key-pair. The `DownstreamKey` is used to encrypt/decrypt data, while the public/private key-pair +/// is used to rotate the `DownstreamKey`. /// -/// The `PrivateKey` is protected by an `ExternalKey`, such as a `DeviceKey`, or `PrfKey`, -/// and the `PublicKey` is protected by the `EncryptionKey`. This setup allows: +/// The `PrivateKey` is protected by an `UpstreamKey`, such as a `DeviceKey`, or `PrfKey`, +/// and the `PublicKey` is protected by the `DownstreamKey`. This setup allows: /// -/// - Access to `EncryptionKey` by knowing the `ExternalKey` -/// - Rotation to a `NewEncryptionKey` by knowing the current `EncryptionKey`, without needing access to -/// the `ExternalKey` +/// - Access to `DownstreamKey` by knowing the `UpstreamKey` +/// - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing access to +/// the `UpstreamKey` #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -67,21 +67,21 @@ pub fn dangerous_get_v2_rotated_account_keys( tsify(into_wasm_abi, from_wasm_abi) )] pub struct RotateableKeySet { - /// `EncryptionKey` protected by encapsulation key - encapsulated_encryption_key: UnsignedSharedKey, - /// Encapsulation key protected by `EncryptionKey` + /// `DownstreamKey` protected by encapsulation key + encapsulated_downstream_key: UnsignedSharedKey, + /// Encapsulation key protected by `DownstreamKey` encrypted_encapsulation_key: EncString, - /// Decapsulation key protected by `ExternalKey` - wrapped_decapsulation_key: EncString, + /// Decapsulation key protected by `UpstreamKey` + encrypted_decapsulation_key: EncString, } impl RotateableKeySet { - /// Create a set of keys to allow access to the user key via the provided - /// symmetric wrapping key while allowing the user key to be rotated. + /// Create a set of keys to allow access to the downstream key via the provided + /// upstream key while allowing the downstream key to be rotated. pub fn new( ctx: &KeyStoreContext, - wrapping_key: &SymmetricCryptoKey, - key_to_wrap: Ids::Symmetric, + upstream_key: &SymmetricCryptoKey, + downstream_key_id: Ids::Symmetric, ) -> Result { let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1); @@ -89,30 +89,28 @@ impl RotateableKeySet { // rather than the key store context because we don't want the keys to // wind up being stored in the borrowed context. #[allow(deprecated)] - let key_to_wrap_instance = ctx.dangerous_get_symmetric_key(key_to_wrap)?; - // encapsulate encryption key - let encapsulated_encryption_key = UnsignedSharedKey::encapsulate_key_unsigned( - key_to_wrap_instance, - &key_pair.to_public_key(), - )?; + let downstream_key = ctx.dangerous_get_symmetric_key(downstream_key_id)?; + // encapsulate downstream key + let encapsulated_downstream_key = + UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?; - // wrap decapsulation key - let wrapped_decapsulation_key = key_pair.to_der()?.encrypt_with_key(wrapping_key)?; + // wrap decapsulation key with upstream key + let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?; - // wrap encapsulation key with encryption key + // wrap encapsulation key with downstream key // Note: Usually, a public key is - by definition - public, so this should not be necessary. // The specific use-case for this function is to enable rotateable key sets, where // the "public key" is not public, with the intent of preventing the server from being able - // to overwrite the user key unlocked by the rotateable keyset. + // to overwrite the downstream key unlocked by the rotateable keyset. let encrypted_encapsulation_key = key_pair .to_public_key() .to_der()? - .encrypt_with_key(&key_to_wrap_instance)?; + .encrypt_with_key(&downstream_key)?; Ok(RotateableKeySet { - encapsulated_encryption_key, + encapsulated_downstream_key, encrypted_encapsulation_key, - wrapped_decapsulation_key, + encrypted_decapsulation_key, }) } @@ -122,19 +120,19 @@ impl RotateableKeySet { fn unlock( &self, ctx: &mut KeyStoreContext, - unwrapping_key: &SymmetricCryptoKey, - key_to_unwrap: Ids::Symmetric, + upstream_key: &SymmetricCryptoKey, + downstream_key_id: Ids::Symmetric, ) -> Result<(), CryptoError> { let priv_key_bytes: Vec = self - .wrapped_decapsulation_key - .decrypt_with_key(unwrapping_key)?; + .encrypted_decapsulation_key + .decrypt_with_key(upstream_key)?; let decapsulation_key = AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?; - let encryption_key = self - .encapsulated_encryption_key + let downstream_key = self + .encapsulated_downstream_key .decapsulate_key_unsigned(&decapsulation_key)?; #[allow(deprecated)] - ctx.set_symmetric_key(key_to_unwrap, encryption_key)?; + ctx.set_symmetric_key(downstream_key_id, downstream_key)?; Ok(()) } } @@ -143,26 +141,26 @@ impl RotateableKeySet { fn rotate_key_set( ctx: &KeyStoreContext, key_set: RotateableKeySet, - old_encryption_key_id: Ids::Symmetric, - new_encryption_key_id: Ids::Symmetric, + old_downstream_key_id: Ids::Symmetric, + new_downstream_key_id: Ids::Symmetric, ) -> Result { let pub_key_bytes = ctx.decrypt_data_with_symmetric_key( - old_encryption_key_id, + old_downstream_key_id, &key_set.encrypted_encapsulation_key, )?; let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes); let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?; // TODO: There is no method to store only the public key in the store, so we - // have pull out the encryption key to encapsulate it manually. + // have pull out the downstream key to encapsulate it manually. #[allow(deprecated)] - let new_encryption_key = ctx.dangerous_get_symmetric_key(new_encryption_key_id)?; + let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?; let new_encapsulated_key = - UnsignedSharedKey::encapsulate_key_unsigned(new_encryption_key, &encapsulation_key)?; - let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_encryption_key)?; + UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?; + let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?; Ok(RotateableKeySet { - encapsulated_encryption_key: new_encapsulated_key, + encapsulated_downstream_key: new_encapsulated_key, encrypted_encapsulation_key: new_encrypted_encapsulation_key, - wrapped_decapsulation_key: key_set.wrapped_decapsulation_key, + encrypted_decapsulation_key: key_set.encrypted_decapsulation_key, }) } @@ -262,75 +260,75 @@ mod tests { #[test] fn test_rotateable_key_set_can_unlock() { // generate initial keys - let external_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); // set up store let store: KeyStore = KeyStore::default(); let mut ctx = store.context_mut(); - let original_encryption_key_id = TestSymmKey::A(0); - ctx.generate_symmetric_key(original_encryption_key_id) + let original_downstream_key_id = TestSymmKey::A(0); + ctx.generate_symmetric_key(original_downstream_key_id) .unwrap(); // create key set let key_set = - RotateableKeySet::new(&ctx, &external_key, original_encryption_key_id).unwrap(); + RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap(); // unlock key set - let unwrapped_encryption_key_id = TestSymmKey::A(1); + let unwrapped_downstream_key_id = TestSymmKey::A(1); key_set - .unlock(&mut ctx, &external_key, unwrapped_encryption_key_id) + .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id) .unwrap(); #[allow(deprecated)] - let original_key = ctx - .dangerous_get_symmetric_key(original_encryption_key_id) + let original_downstream_key = ctx + .dangerous_get_symmetric_key(original_downstream_key_id) .unwrap(); #[allow(deprecated)] - let unwrapped_key = ctx - .dangerous_get_symmetric_key(unwrapped_encryption_key_id) + let unwrapped_downstream_key = ctx + .dangerous_get_symmetric_key(unwrapped_downstream_key_id) .unwrap(); - assert_eq!(original_key, unwrapped_key); + assert_eq!(original_downstream_key, unwrapped_downstream_key); } #[test] fn test_rotateable_key_set_rotation() { // generate initial keys - let external_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); // set up store let store: KeyStore = KeyStore::default(); let mut ctx = store.context_mut(); - let original_encryption_key_id = TestSymmKey::A(1); - ctx.generate_symmetric_key(original_encryption_key_id) + let original_downstream_key_id = TestSymmKey::A(1); + ctx.generate_symmetric_key(original_downstream_key_id) .unwrap(); // create key set let key_set = - RotateableKeySet::new(&ctx, &external_key, original_encryption_key_id).unwrap(); + RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap(); // rotate - let new_encryption_key_id = TestSymmKey::A(2_1); - ctx.generate_symmetric_key(new_encryption_key_id).unwrap(); + let new_downstream_key_id = TestSymmKey::A(2_1); + ctx.generate_symmetric_key(new_downstream_key_id).unwrap(); let new_key_set = rotate_key_set( &ctx, key_set, - original_encryption_key_id, - new_encryption_key_id, + original_downstream_key_id, + new_downstream_key_id, ) .unwrap(); // After rotation, the new key set should be unlocked by the same - // external key and return the new encryption key. - let unwrapped_encryption_key_id = TestSymmKey::A(2_2); + // upstream key and return the new downstream key. + let unwrapped_downstream_key_id = TestSymmKey::A(2_2); new_key_set - .unlock(&mut ctx, &external_key, unwrapped_encryption_key_id) + .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id) .unwrap(); #[allow(deprecated)] - let new_encryption_key = ctx - .dangerous_get_symmetric_key(new_encryption_key_id) + let new_downstream_key = ctx + .dangerous_get_symmetric_key(new_downstream_key_id) .unwrap(); #[allow(deprecated)] - let unwrapped_encryption_key = ctx - .dangerous_get_symmetric_key(unwrapped_encryption_key_id) + let unwrapped_downstream_key = ctx + .dangerous_get_symmetric_key(unwrapped_downstream_key_id) .unwrap(); - assert_eq!(new_encryption_key, unwrapped_encryption_key); + assert_eq!(new_downstream_key, unwrapped_downstream_key); } } From f986b43951f8a004d2a46911d27e571589a1eacc Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 7 Oct 2025 14:00:11 -0500 Subject: [PATCH 05/12] Fix lints --- crates/bitwarden-crypto/Cargo.toml | 2 +- crates/bitwarden-crypto/src/keys/prf.rs | 6 ++---- crates/bitwarden-crypto/src/store/key_rotation.rs | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index 0da4085fc..e1fdbedbb 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -20,7 +20,7 @@ no-memory-hardening = [] # Disable memory hardening features uniffi = [ "bitwarden-encoding/uniffi", "dep:bitwarden-uniffi-error", - "dep:uniffi" + "dep:uniffi", ] # Uniffi bindings wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support diff --git a/crates/bitwarden-crypto/src/keys/prf.rs b/crates/bitwarden-crypto/src/keys/prf.rs index 3591460af..188b2ffc2 100644 --- a/crates/bitwarden-crypto/src/keys/prf.rs +++ b/crates/bitwarden-crypto/src/keys/prf.rs @@ -4,10 +4,8 @@ use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key}; /// /// The PRF output must be at least 32 bytes long. pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result { - let (secret, _) = prf - .split_at_checked(32) - .ok_or_else(|| CryptoError::InvalidKeyLen)?; - let secret: [u8; 32] = secret.try_into().unwrap(); + let (secret, _) = prf.split_at_checked(32).ok_or(CryptoError::InvalidKeyLen)?; + let secret: [u8; 32] = secret.try_into().expect("length to be 32 bytes"); // Don't allow uninitialized PRFs if secret.iter().all(|b| *b == b'\0') { return Err(CryptoError::ZeroNumber); diff --git a/crates/bitwarden-crypto/src/store/key_rotation.rs b/crates/bitwarden-crypto/src/store/key_rotation.rs index 3162af132..a3d0d27b2 100644 --- a/crates/bitwarden-crypto/src/store/key_rotation.rs +++ b/crates/bitwarden-crypto/src/store/key_rotation.rs @@ -56,8 +56,8 @@ pub fn dangerous_get_v2_rotated_account_keys( /// and the `PublicKey` is protected by the `DownstreamKey`. This setup allows: /// /// - Access to `DownstreamKey` by knowing the `UpstreamKey` -/// - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing access to -/// the `UpstreamKey` +/// - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing +/// access to the `UpstreamKey` #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -105,7 +105,7 @@ impl RotateableKeySet { let encrypted_encapsulation_key = key_pair .to_public_key() .to_der()? - .encrypt_with_key(&downstream_key)?; + .encrypt_with_key(downstream_key)?; Ok(RotateableKeySet { encapsulated_downstream_key, From 0dce15ef6cb34ee842c6d1e895f02b75299552d6 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 7 Oct 2025 14:00:11 -0500 Subject: [PATCH 06/12] Feature-gate wasm-only imports --- crates/bitwarden-vault/src/cipher/cipher_client.rs | 12 +++++++++--- crates/bitwarden-vault/src/collection_client.rs | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index 060e1bcfd..94fe857ee 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -1,14 +1,20 @@ -use bitwarden_core::{Client, OrganizationId, key_management::SymmetricKeyId}; -use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, SymmetricCryptoKey}; +#[cfg(feature = "wasm")] +use bitwarden_core::key_management::SymmetricKeyId; +use bitwarden_core::{Client, OrganizationId}; +use bitwarden_crypto::IdentifyKey; +#[cfg(feature = "wasm")] +use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey}; #[cfg(feature = "wasm")] use bitwarden_encoding::B64; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use super::EncryptionContext; +#[cfg(feature = "wasm")] +use crate::Fido2CredentialFullView; use crate::{ Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError, - Fido2CredentialFullView, cipher::cipher::DecryptCipherListResult, + cipher::cipher::DecryptCipherListResult, }; #[allow(missing_docs)] diff --git a/crates/bitwarden-vault/src/collection_client.rs b/crates/bitwarden-vault/src/collection_client.rs index f74cf7be2..230d82099 100644 --- a/crates/bitwarden-vault/src/collection_client.rs +++ b/crates/bitwarden-vault/src/collection_client.rs @@ -5,6 +5,7 @@ use bitwarden_collections::{ tree::{NodeItem, Tree}, }; use bitwarden_core::Client; +#[cfg(feature = "wasm")] use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use tsify::Tsify; From b9bb1f06cd5720b7c5e2f2ce7956216a0702a838 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 7 Oct 2025 15:19:29 -0500 Subject: [PATCH 07/12] Lint with correct version of cargo-sort --- crates/bitwarden-crypto/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index e1fdbedbb..0da4085fc 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -20,7 +20,7 @@ no-memory-hardening = [] # Disable memory hardening features uniffi = [ "bitwarden-encoding/uniffi", "dep:bitwarden-uniffi-error", - "dep:uniffi", + "dep:uniffi" ] # Uniffi bindings wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support From de25a3db21874ca7e5d92f556079b0baa9c31be3 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 30 Sep 2025 07:36:28 -0500 Subject: [PATCH 08/12] Incorporate upstream passkey-rs changes --- Cargo.lock | 60 ++++++++-------------- crates/bitwarden-fido/Cargo.toml | 4 +- crates/bitwarden-fido/src/authenticator.rs | 1 + crates/bitwarden-fido/src/client.rs | 1 - crates/bitwarden-fido/src/lib.rs | 6 ++- crates/bitwarden-fido/src/types.rs | 4 +- 6 files changed, 29 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2eb06438..84a308020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2633,16 +2633,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -3255,8 +3245,8 @@ dependencies = [ [[package]] name = "passkey" -version = "0.2.0" -source = "git+https://github.com/bitwarden/passkey-rs?rev=3b764633ebc6576c07bdd12ee14d8e5c87b494ed#3b764633ebc6576c07bdd12ee14d8e5c87b494ed" +version = "0.5.0" +source = "git+https://github.com/iinuwa/passkey-rs?rev=99829ad1886ecd695564a226c807b87ec3e2cff7#99829ad1886ecd695564a226c807b87ec3e2cff7" dependencies = [ "passkey-authenticator", "passkey-client", @@ -3266,8 +3256,8 @@ dependencies = [ [[package]] name = "passkey-authenticator" -version = "0.2.0" -source = "git+https://github.com/bitwarden/passkey-rs?rev=3b764633ebc6576c07bdd12ee14d8e5c87b494ed#3b764633ebc6576c07bdd12ee14d8e5c87b494ed" +version = "0.5.0" +source = "git+https://github.com/iinuwa/passkey-rs?rev=99829ad1886ecd695564a226c807b87ec3e2cff7#99829ad1886ecd695564a226c807b87ec3e2cff7" dependencies = [ "async-trait", "coset", @@ -3279,12 +3269,13 @@ dependencies = [ [[package]] name = "passkey-client" -version = "0.2.0" -source = "git+https://github.com/bitwarden/passkey-rs?rev=3b764633ebc6576c07bdd12ee14d8e5c87b494ed#3b764633ebc6576c07bdd12ee14d8e5c87b494ed" +version = "0.5.0" +source = "git+https://github.com/iinuwa/passkey-rs?rev=99829ad1886ecd695564a226c807b87ec3e2cff7#99829ad1886ecd695564a226c807b87ec3e2cff7" dependencies = [ "ciborium", "coset", - "idna 0.5.0", + "idna", + "itertools 0.14.0", "nom", "passkey-authenticator", "passkey-types", @@ -3297,24 +3288,27 @@ dependencies = [ [[package]] name = "passkey-transports" version = "0.1.0" -source = "git+https://github.com/bitwarden/passkey-rs?rev=3b764633ebc6576c07bdd12ee14d8e5c87b494ed#3b764633ebc6576c07bdd12ee14d8e5c87b494ed" +source = "git+https://github.com/iinuwa/passkey-rs?rev=99829ad1886ecd695564a226c807b87ec3e2cff7#99829ad1886ecd695564a226c807b87ec3e2cff7" [[package]] name = "passkey-types" -version = "0.2.1" -source = "git+https://github.com/bitwarden/passkey-rs?rev=3b764633ebc6576c07bdd12ee14d8e5c87b494ed#3b764633ebc6576c07bdd12ee14d8e5c87b494ed" +version = "0.5.0" +source = "git+https://github.com/iinuwa/passkey-rs?rev=99829ad1886ecd695564a226c807b87ec3e2cff7#99829ad1886ecd695564a226c807b87ec3e2cff7" dependencies = [ "bitflags 2.9.1", "ciborium", "coset", "data-encoding", "getrandom 0.2.16", + "hmac", "indexmap 2.9.0", "rand 0.8.5", "serde", "serde_json", "sha2", "strum", + "url", + "zeroize", ] [[package]] @@ -3594,8 +3588,8 @@ dependencies = [ [[package]] name = "public-suffix" -version = "0.1.1" -source = "git+https://github.com/bitwarden/passkey-rs?rev=3b764633ebc6576c07bdd12ee14d8e5c87b494ed#3b764633ebc6576c07bdd12ee14d8e5c87b494ed" +version = "0.1.3" +source = "git+https://github.com/iinuwa/passkey-rs?rev=99829ad1886ecd695564a226c807b87ec3e2cff7#99829ad1886ecd695564a226c807b87ec3e2cff7" [[package]] name = "quick-xml" @@ -5107,27 +5101,12 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -5307,8 +5286,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", + "serde", ] [[package]] @@ -5341,7 +5321,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" dependencies = [ - "idna 1.0.3", + "idna", "once_cell", "regex", "serde", diff --git a/crates/bitwarden-fido/Cargo.toml b/crates/bitwarden-fido/Cargo.toml index 9d8637b96..5a715cd8f 100644 --- a/crates/bitwarden-fido/Cargo.toml +++ b/crates/bitwarden-fido/Cargo.toml @@ -28,8 +28,8 @@ coset = ">=0.3.7, <0.4" itertools = ">=0.13.0, <0.15" log = { workspace = true } p256 = ">=0.13.2, <0.14" -passkey = { git = "https://github.com/bitwarden/passkey-rs", rev = "3b764633ebc6576c07bdd12ee14d8e5c87b494ed" } -passkey-client = { git = "https://github.com/bitwarden/passkey-rs", rev = "3b764633ebc6576c07bdd12ee14d8e5c87b494ed", features = [ +passkey = { git = "https://github.com/iinuwa/passkey-rs", rev = "99829ad1886ecd695564a226c807b87ec3e2cff7" } +passkey-client = { git = "https://github.com/iinuwa/passkey-rs", rev = "99829ad1886ecd695564a226c807b87ec3e2cff7", features = [ "android-asset-validation", ] } reqwest = { workspace = true } diff --git a/crates/bitwarden-fido/src/authenticator.rs b/crates/bitwarden-fido/src/authenticator.rs index 6093142e6..69716aed4 100644 --- a/crates/bitwarden-fido/src/authenticator.rs +++ b/crates/bitwarden-fido/src/authenticator.rs @@ -353,6 +353,7 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { &self, ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>, rp_id: &str, + _user_handle: Option<&[u8]>, ) -> Result, StatusCode> { #[derive(Debug, Error)] enum InnerError { diff --git a/crates/bitwarden-fido/src/client.rs b/crates/bitwarden-fido/src/client.rs index 9e58fdaef..d95cfc1c2 100644 --- a/crates/bitwarden-fido/src/client.rs +++ b/crates/bitwarden-fido/src/client.rs @@ -132,7 +132,6 @@ impl Fido2Client<'_> { .cred_props .map(|c| CredPropsResult { rk: c.discoverable, - authenticator_display_name: c.authenticator_display_name, }), }, response: AuthenticatorAssertionResponse { diff --git a/crates/bitwarden-fido/src/lib.rs b/crates/bitwarden-fido/src/lib.rs index d75a5f32b..1accb995d 100644 --- a/crates/bitwarden-fido/src/lib.rs +++ b/crates/bitwarden-fido/src/lib.rs @@ -7,7 +7,7 @@ use bitwarden_vault::{ CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView, }; use crypto::{CoseKeyToPkcs8Error, PrivateKeyFromSecretKeyError}; -use passkey::types::{Passkey, ctap2::Aaguid}; +use passkey::types::{CredentialExtensions, Passkey, ctap2::Aaguid}; #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); @@ -117,6 +117,9 @@ fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result Result, - pub authenticator_display_name: Option, } impl From for CredPropsResult { fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self { Self { rk: value.discoverable, - authenticator_display_name: value.authenticator_display_name, } } } @@ -481,7 +479,7 @@ impl TryFrom for passkey::client::UnverifiedAssetLink<'_> { Cow::from(value.package_name), value.sha256_cert_fingerprint.as_str(), Cow::from(value.host), - asset_link_url, + asset_link_url.expect("Is asset_link_url ever null?"), ) .map_err(|e| InvalidOriginError(format!("{e:?}"))) } From 9506399d8a10b801e9c16b9461949bd20cdebd50 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 10 Oct 2025 06:36:26 -0500 Subject: [PATCH 09/12] Add hmac-secret field to cipher object --- .../src/models/cipher_fido2_credential_model.rs | 3 +++ crates/bitwarden-exporters/src/cxf/export.rs | 1 + crates/bitwarden-exporters/src/cxf/login.rs | 6 ++++++ crates/bitwarden-exporters/src/lib.rs | 2 ++ crates/bitwarden-exporters/src/models.rs | 1 + crates/bitwarden-vault/src/cipher/cipher.rs | 1 + crates/bitwarden-vault/src/cipher/login.rs | 17 +++++++++++++++++ 7 files changed, 31 insertions(+) diff --git a/crates/bitwarden-api-api/src/models/cipher_fido2_credential_model.rs b/crates/bitwarden-api-api/src/models/cipher_fido2_credential_model.rs index 09e761b41..c7d77667b 100644 --- a/crates/bitwarden-api-api/src/models/cipher_fido2_credential_model.rs +++ b/crates/bitwarden-api-api/src/models/cipher_fido2_credential_model.rs @@ -38,6 +38,8 @@ pub struct CipherFido2CredentialModel { pub counter: Option, #[serde(rename = "discoverable", skip_serializing_if = "Option::is_none")] pub discoverable: Option, + #[serde(rename = "hmac_secret", skip_serializing_if = "Option::is_none")] + pub hmac_secret: Option, #[serde(rename = "creationDate")] pub creation_date: String, } @@ -57,6 +59,7 @@ impl CipherFido2CredentialModel { user_display_name: None, counter: None, discoverable: None, + hmac_secret: None, creation_date, } } diff --git a/crates/bitwarden-exporters/src/cxf/export.rs b/crates/bitwarden-exporters/src/cxf/export.rs index ea066161a..6b884a897 100644 --- a/crates/bitwarden-exporters/src/cxf/export.rs +++ b/crates/bitwarden-exporters/src/cxf/export.rs @@ -210,6 +210,7 @@ mod tests { rp_name: None, user_display_name: None, discoverable: "true".to_string(), + hmac_secret: Some("AAECAwQFBg".to_string()), creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(), }]), })), diff --git a/crates/bitwarden-exporters/src/cxf/login.rs b/crates/bitwarden-exporters/src/cxf/login.rs index d2eb81391..43d7e9735 100644 --- a/crates/bitwarden-exporters/src/cxf/login.rs +++ b/crates/bitwarden-exporters/src/cxf/login.rs @@ -89,6 +89,11 @@ pub(super) fn to_login( rp_name: Some(p.rp_id.clone()), user_display_name: Some(p.user_display_name.clone()), discoverable: "true".to_string(), + hmac_secret: p + .fido2_extensions + .as_ref() + .and_then(|ext| ext.hmac_credentials.as_ref()) + .map(|s| s.cred_with_uv.to_string()), creation_date, }] }), @@ -283,6 +288,7 @@ mod tests { rp_name: None, user_display_name: None, discoverable: "true".to_string(), + hmac_secret: Some("AAECAwQFBg".to_string()), creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(), }; diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index 5c30c9644..4c32daaa8 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -345,6 +345,7 @@ pub struct Fido2Credential { pub rp_name: Option, pub user_display_name: Option, pub discoverable: String, + pub hmac_secret: Option, pub creation_date: DateTime, } @@ -363,6 +364,7 @@ impl From for Fido2CredentialFullView { rp_name: value.rp_name, user_display_name: value.user_display_name, discoverable: value.discoverable, + hmac_secret: value.hmac_secret, creation_date: value.creation_date, } } diff --git a/crates/bitwarden-exporters/src/models.rs b/crates/bitwarden-exporters/src/models.rs index 0c069a535..57efab593 100644 --- a/crates/bitwarden-exporters/src/models.rs +++ b/crates/bitwarden-exporters/src/models.rs @@ -116,6 +116,7 @@ impl From for crate::Fido2Credential { rp_name: value.rp_name, user_display_name: value.user_display_name, discoverable: value.discoverable, + hmac_secret: value.hmac_secret, creation_date: value.creation_date, } } diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 9c6b36173..5b7deb3d7 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -863,6 +863,7 @@ mod tests { rp_name: None, user_display_name: None, discoverable: "true".to_string().encrypt(ctx, key).unwrap(), + hmac_secret: Some("123".to_string().encrypt(ctx, key).unwrap()), creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(), } } diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 814f68385..02a45250b 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -101,6 +101,7 @@ pub struct Fido2Credential { pub rp_name: Option, pub user_display_name: Option, pub discoverable: EncString, + pub hmac_secret: Option, pub creation_date: DateTime, } @@ -137,6 +138,9 @@ pub struct Fido2CredentialView { pub rp_name: Option, pub user_display_name: Option, pub discoverable: String, + // This value doesn't need to be returned to the client + // so we keep it encrypted until we need it + pub hmac_secret: Option, pub creation_date: DateTime, } @@ -159,6 +163,7 @@ pub struct Fido2CredentialFullView { pub rp_name: Option, pub user_display_name: Option, pub discoverable: String, + pub hmac_secret: Option, pub creation_date: DateTime, } @@ -225,6 +230,11 @@ impl CompositeEncryptable for Fido2Cred rp_name: self.rp_name.encrypt(ctx, key)?, user_display_name: self.user_display_name.encrypt(ctx, key)?, discoverable: self.discoverable.encrypt(ctx, key)?, + hmac_secret: self + .hmac_secret + .as_ref() + .map(|s| s.encrypt(ctx, key)) + .transpose()?, creation_date: self.creation_date, }) } @@ -249,6 +259,7 @@ impl Decryptable for Fido2Crede rp_name: self.rp_name.decrypt(ctx, key)?, user_display_name: self.user_display_name.decrypt(ctx, key)?, discoverable: self.discoverable.decrypt(ctx, key)?, + hmac_secret: self.hmac_secret.decrypt(ctx, key)?, creation_date: self.creation_date, }) } @@ -273,6 +284,7 @@ impl Decryptable for Fido2Crede rp_name: self.rp_name.clone(), user_display_name: self.user_display_name.clone(), discoverable: self.discoverable.clone(), + hmac_secret: self.hmac_secret.decrypt(ctx, key)?, creation_date: self.creation_date, }) } @@ -438,6 +450,7 @@ impl CompositeEncryptable for Fido2Cred rp_name: self.rp_name.encrypt(ctx, key)?, user_display_name: self.user_display_name.encrypt(ctx, key)?, discoverable: self.discoverable.encrypt(ctx, key)?, + hmac_secret: self.hmac_secret.clone(), creation_date: self.creation_date, }) } @@ -462,6 +475,7 @@ impl Decryptable for Fido2Credentia rp_name: self.rp_name.decrypt(ctx, key)?, user_display_name: self.user_display_name.decrypt(ctx, key)?, discoverable: self.discoverable.decrypt(ctx, key)?, + hmac_secret: self.hmac_secret.clone(), creation_date: self.creation_date, }) } @@ -557,6 +571,9 @@ impl TryFrom for Fido2Cre .ok() .flatten(), discoverable: require!(value.discoverable).parse()?, + hmac_secret: EncString::try_from_optional(value.hmac_secret) + .ok() + .flatten(), creation_date: value.creation_date.parse()?, }) } From 3d38c9632e7b462d1e51141acf3d5e9f9de15a26 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 10 Oct 2025 06:36:26 -0500 Subject: [PATCH 10/12] Enable PRF for FIDO authenticator --- crates/bitwarden-fido/src/authenticator.rs | 6 +- crates/bitwarden-fido/src/lib.rs | 24 +- crates/bitwarden-fido/src/types.rs | 264 ++++++++++++++++++++- 3 files changed, 286 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-fido/src/authenticator.rs b/crates/bitwarden-fido/src/authenticator.rs index 19ac72fe8..962cd786e 100644 --- a/crates/bitwarden-fido/src/authenticator.rs +++ b/crates/bitwarden-fido/src/authenticator.rs @@ -6,7 +6,10 @@ use bitwarden_vault::{CipherError, CipherView, EncryptionContext}; use itertools::Itertools; use log::error; use passkey::{ - authenticator::{Authenticator, DiscoverabilitySupport, StoreInfo, UIHint, UserCheck}, + authenticator::{ + Authenticator, DiscoverabilitySupport, StoreInfo, UIHint, UserCheck, + extensions::HmacSecretConfig, + }, types::{ Passkey, ctap2::{ @@ -304,6 +307,7 @@ impl<'a> Fido2Authenticator<'a> { authenticator: self, }, ) + .hmac_secret(HmacSecretConfig::new_with_uv_only().enable_on_make_credential()) } async fn convert_requested_uv(&self, uv: UV) -> bool { diff --git a/crates/bitwarden-fido/src/lib.rs b/crates/bitwarden-fido/src/lib.rs index 1accb995d..a94d00fb7 100644 --- a/crates/bitwarden-fido/src/lib.rs +++ b/crates/bitwarden-fido/src/lib.rs @@ -7,7 +7,7 @@ use bitwarden_vault::{ CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView, }; use crypto::{CoseKeyToPkcs8Error, PrivateKeyFromSecretKeyError}; -use passkey::types::{CredentialExtensions, Passkey, ctap2::Aaguid}; +use passkey::types::{CredentialExtensions, Passkey, StoredHmacSecret, ctap2::Aaguid}; #[cfg(feature = "uniffi")] uniffi::setup_scaffolding!(); @@ -117,8 +117,16 @@ fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result = value.credential_id.into(); let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string(); let user_handle = B64Url::from(user.id.to_vec()).to_string(); + let hmac_secret = value + .extensions + .hmac_secret + .as_ref() + .map(|s| B64Url::from(s.cred_with_uv.as_ref()).to_string()); Ok(Fido2CredentialFullView { credential_id: guid_bytes_to_string(&cred_id)?, @@ -219,6 +238,7 @@ pub(crate) fn try_from_credential_full( user_name: user.name, user_display_name: user.display_name, discoverable: options.rk.to_string(), + hmac_secret, creation_date: chrono::offset::Utc::now(), }) } diff --git a/crates/bitwarden-fido/src/types.rs b/crates/bitwarden-fido/src/types.rs index 0db9c1519..b3a7b02de 100644 --- a/crates/bitwarden-fido/src/types.rs +++ b/crates/bitwarden-fido/src/types.rs @@ -1,10 +1,18 @@ -use std::borrow::Cow; +use std::{borrow::Cow, collections::HashMap}; use bitwarden_core::key_management::KeyIds; use bitwarden_crypto::{CryptoError, KeyStoreContext}; use bitwarden_encoding::{B64Url, NotB64UrlEncodedError}; use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView}; -use passkey::types::webauthn::UserVerificationRequirement; +use passkey::types::{ + Bytes, + crypto::sha256, + ctap2::{ + extensions::{AuthenticatorPrfInputs, AuthenticatorPrfValues}, + get_assertion, make_credential, + }, + webauthn::UserVerificationRequirement, +}; use reqwest::Url; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -250,6 +258,7 @@ pub struct MakeCredentialResult { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug)] pub struct MakeCredentialExtensionsInput { + prf: Option, } impl From @@ -259,7 +268,7 @@ impl From Self { hmac_secret: None, hmac_secret_mc: None, - prf: None, + prf: value.prf.map(AuthenticatorPrfInputs::from), } } } @@ -268,14 +277,56 @@ impl From #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug)] pub struct MakeCredentialExtensionsOutput { + pub prf: Option, +} + +impl From> for MakeCredentialExtensionsOutput { + fn from(value: Option) -> Self { + if let Some(ext) = value { + MakeCredentialExtensionsOutput::from(ext) + } else { + MakeCredentialExtensionsOutput { prf: None } + } + } } impl From for MakeCredentialExtensionsOutput { fn from(value: make_credential::UnsignedExtensionOutputs) -> Self { - MakeCredentialExtensionsOutput { } + let prf = value.prf.map(|prf| MakeCredentialPrfOutput { + enabled: prf.enabled, + results: prf.results.map(|v| PrfValues { + first: v.first.to_vec(), + second: v.second.map(|second| second.to_vec()), + }), + }); + MakeCredentialExtensionsOutput { prf } } } +#[allow(missing_docs)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug)] +pub struct MakeCredentialPrfInput { + eval: Option, +} + +impl From for AuthenticatorPrfInputs { + fn from(value: MakeCredentialPrfInput) -> Self { + Self { + eval: value.eval.map(AuthenticatorPrfValues::from), + eval_by_credential: None, + } + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug)] +pub struct MakeCredentialPrfOutput { + pub enabled: bool, + pub results: Option, +} + #[allow(missing_docs)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct GetAssertionRequest { @@ -367,13 +418,14 @@ pub struct GetAssertionResult { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug)] pub struct GetAssertionExtensionsInput { + prf: Option, } impl From for get_assertion::ExtensionInputs { fn from(value: GetAssertionExtensionsInput) -> Self { Self { hmac_secret: None, - prf: None, + prf: value.prf.map(AuthenticatorPrfInputs::from), } } } @@ -382,12 +434,65 @@ impl From for get_assertion::ExtensionInputs { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[derive(Debug)] pub struct GetAssertionExtensionsOutput { + pub prf: Option, +} + +impl From> for GetAssertionExtensionsOutput { + fn from(value: Option) -> Self { + if let Some(value) = value { + value.into() + } else { + Self { prf: None } + } + } } impl From for GetAssertionExtensionsOutput { fn from(value: get_assertion::UnsignedExtensionOutputs) -> Self { + let prf = value.prf.map(|prf| GetAssertionPrfOutput { + results: PrfValues { + first: prf.results.first.to_vec(), + second: prf.results.second.map(|second| second.to_vec()), + }, + }); + GetAssertionExtensionsOutput { prf } } } + +#[allow(missing_docs)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug)] +pub struct GetAssertionPrfInput { + eval: Option, + eval_by_credential: Option, PrfValues>>, +} + +impl From for AuthenticatorPrfInputs { + fn from(value: GetAssertionPrfInput) -> Self { + let eval_by_credential = if let Some(values) = value.eval_by_credential { + let map: HashMap = values + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + Some(map) + } else { + None + }; + Self { + eval: value.eval.map(AuthenticatorPrfValues::from), + eval_by_credential, + } + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug)] +pub struct GetAssertionPrfOutput { + pub results: PrfValues, +} + +#[allow(missing_docs)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum ClientData { DefaultWithExtraData { android_package_name: String }, @@ -420,6 +525,27 @@ impl passkey::client::ClientData> for ClientData { } } +#[allow(missing_docs)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[derive(Debug)] +pub struct PrfValues { + pub first: Vec, + pub second: Option>, +} + +impl From for AuthenticatorPrfValues { + fn from(value: PrfValues) -> Self { + // passkey-rs expects the salt to be hashed already according to + // WebAuthn PRF extension client processing rules. + let prefix = b"WebAuthn PRF\0".as_slice(); + let first = sha256(&[prefix, value.first.as_ref()].concat()); + let second = value + .second + .map(|second| sha256(&[prefix, second.as_ref()].concat())); + Self { first, second } + } +} + #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct ClientExtensionResults { pub cred_props: Option, @@ -545,9 +671,20 @@ impl TryFrom for passkey::client::UnverifiedAssetLink<'_> { #[cfg(test)] mod tests { + use passkey::types::ctap2::{ + extensions::{ + AuthenticatorPrfGetOutputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues, + }, + get_assertion, make_credential, + }; use serde::{Deserialize, Serialize}; use super::AndroidClientData; + use crate::types::{ + GetAssertionExtensionsInput, GetAssertionExtensionsOutput, GetAssertionPrfInput, + MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput, MakeCredentialPrfInput, + PrfValues, + }; // This is a stripped down of the passkey-rs implementation, to test the // serialization of the `ClientData` enum, and to make sure that () and None @@ -601,4 +738,121 @@ mod tests { r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"# ); } + + #[test] + fn test_transform_make_credential_extension_input() { + let salt1 = b"salt1".to_vec(); + let salt2 = b"salt2".to_vec(); + let input = MakeCredentialExtensionsInput { + prf: Some(MakeCredentialPrfInput { + eval: Some(PrfValues { + first: salt1.clone(), + second: Some(salt2.clone()), + }), + }), + }; + let transformed = make_credential::ExtensionInputs::from(input); + // SHA-256(UTF-8("WebAuthn PRF") || 0x00 || salt1) + let hashed_first = [ + 0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, + 0x0F, 0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, + 0xEE, 0xCB, 0xDD, 0x3C, + ]; + assert_eq!( + hashed_first, + transformed + .prf + .as_ref() + .unwrap() + .eval + .as_ref() + .unwrap() + .first + ); + // SHA-256(UTF-8("WebAuthn PRF") || 0x00 || salt2) + let hashed_second = [ + 0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, + 0x1D, 0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, + 0x94, 0x9A, 0xDD, 0x65, + ]; + assert_eq!( + hashed_second, + transformed.prf.unwrap().eval.unwrap().second.unwrap() + ); + } + + #[test] + fn test_transform_make_credential_extension_output() { + let prf1: Vec = (0..32).collect(); + let output = make_credential::UnsignedExtensionOutputs { + prf: Some(AuthenticatorPrfMakeOutputs { + enabled: true, + results: Some(AuthenticatorPrfValues { + first: prf1.clone().try_into().unwrap(), + second: None, + }), + }), + }; + let transformed = MakeCredentialExtensionsOutput::from(output); + assert!(transformed.prf.as_ref().unwrap().enabled); + assert_eq!(prf1, transformed.prf.unwrap().results.unwrap().first); + } + + #[test] + fn test_transform_get_assertion_extension_input() { + let salt1 = b"salt1".to_vec(); + let salt2 = b"salt2".to_vec(); + let input = GetAssertionExtensionsInput { + prf: Some(GetAssertionPrfInput { + eval: Some(PrfValues { + first: salt1.clone(), + second: Some(salt2.clone()), + }), + eval_by_credential: None, + }), + }; + let transformed = get_assertion::ExtensionInputs::from(input); + // SHA-256(UTF-8("WebAuthn PRF") || 0x00 || salt1) + let hashed_first = [ + 0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, + 0x0F, 0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, + 0xEE, 0xCB, 0xDD, 0x3C, + ]; + assert_eq!( + hashed_first, + transformed + .prf + .as_ref() + .unwrap() + .eval + .as_ref() + .unwrap() + .first + ); + // SHA-256(UTF-8("WebAuthn PRF") || 0x00 || salt2) + let hashed_second = [ + 0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, + 0x1D, 0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, + 0x94, 0x9A, 0xDD, 0x65, + ]; + assert_eq!( + hashed_second, + transformed.prf.unwrap().eval.unwrap().second.unwrap() + ); + } + + #[test] + fn test_transform_get_assertion_extension_output() { + let prf1: Vec = (0..32).collect(); + let output = get_assertion::UnsignedExtensionOutputs { + prf: Some(AuthenticatorPrfGetOutputs { + results: AuthenticatorPrfValues { + first: prf1.clone().try_into().unwrap(), + second: None, + }, + }), + }; + let transformed = GetAssertionExtensionsOutput::from(output); + assert_eq!(prf1, transformed.prf.unwrap().results.first); + } } From d136309f49d1a5ef80227e4699a5013f019a29fd Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 10 Oct 2025 06:36:26 -0500 Subject: [PATCH 11/12] Apply lints --- crates/bitwarden-fido/src/client.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/bitwarden-fido/src/client.rs b/crates/bitwarden-fido/src/client.rs index d95cfc1c2..59a0ba245 100644 --- a/crates/bitwarden-fido/src/client.rs +++ b/crates/bitwarden-fido/src/client.rs @@ -130,9 +130,7 @@ impl Fido2Client<'_> { cred_props: result .client_extension_results .cred_props - .map(|c| CredPropsResult { - rk: c.discoverable, - }), + .map(|c| CredPropsResult { rk: c.discoverable }), }, response: AuthenticatorAssertionResponse { client_data_json: result.response.client_data_json.into(), From 2002a83fdf9f4f5b6acac7e610ad67110b60ab55 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 10 Oct 2025 06:36:26 -0500 Subject: [PATCH 12/12] Allow encrypting FIDO2 credentials under device-bound key instead of user key. --- crates/bitwarden-exporters/src/lib.rs | 1 + crates/bitwarden-exporters/src/models.rs | 2 + crates/bitwarden-fido/src/authenticator.rs | 58 ++++++++++++++----- crates/bitwarden-fido/src/client_fido.rs | 25 ++++++-- crates/bitwarden-uniffi/src/platform/fido2.rs | 37 ++++++++++-- crates/bitwarden-uniffi/src/vault/ciphers.rs | 5 +- crates/bitwarden-uniffi/swift/build.sh | 30 ++++++---- .../bitwarden-vault/src/cipher/attachment.rs | 3 + crates/bitwarden-vault/src/cipher/cipher.rs | 15 ++++- .../src/cipher/cipher_client.rs | 20 +++++-- .../bitwarden-vault/src/cipher/secure_note.rs | 1 + 11 files changed, 153 insertions(+), 44 deletions(-) diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index 4c32daaa8..a4dfb05ac 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -223,6 +223,7 @@ impl From for CipherView { Self { id: None, + device_bound: false, organization_id: None, folder_id: value.folder_id.map(FolderId::new), collection_ids: vec![], diff --git a/crates/bitwarden-exporters/src/models.rs b/crates/bitwarden-exporters/src/models.rs index 57efab593..ec9cbc368 100644 --- a/crates/bitwarden-exporters/src/models.rs +++ b/crates/bitwarden-exporters/src/models.rs @@ -259,6 +259,7 @@ mod tests { }), id: Some(CipherId::new(test_id)), organization_id: None, + device_bound: false, folder_id: None, collection_ids: vec![], key: None, @@ -311,6 +312,7 @@ mod tests { }), id: Some(CipherId::new(test_id)), organization_id: None, + device_bound: false, folder_id: None, collection_ids: vec![], key: None, diff --git a/crates/bitwarden-fido/src/authenticator.rs b/crates/bitwarden-fido/src/authenticator.rs index 962cd786e..b8e345251 100644 --- a/crates/bitwarden-fido/src/authenticator.rs +++ b/crates/bitwarden-fido/src/authenticator.rs @@ -1,7 +1,7 @@ use std::sync::Mutex; -use bitwarden_core::Client; -use bitwarden_crypto::CryptoError; +use bitwarden_core::{Client, key_management::SymmetricKeyId}; +use bitwarden_crypto::{CompositeEncryptable, CryptoError, SymmetricCryptoKey}; use bitwarden_vault::{CipherError, CipherView, EncryptionContext}; use itertools::Itertools; use log::error; @@ -107,6 +107,7 @@ pub struct Fido2Authenticator<'a> { pub client: &'a Client, pub user_interface: &'a dyn Fido2UserInterface, pub credential_store: &'a dyn Fido2CredentialStore, + pub encryption_key: Option, pub(crate) selected_cipher: Mutex>, pub(crate) requested_uv: Mutex>, @@ -118,11 +119,13 @@ impl<'a> Fido2Authenticator<'a> { client: &'a Client, user_interface: &'a dyn Fido2UserInterface, credential_store: &'a dyn Fido2CredentialStore, + encryption_key: Option, ) -> Fido2Authenticator<'a> { Fido2Authenticator { client, user_interface, credential_store, + encryption_key, selected_cipher: Mutex::new(None), requested_uv: Mutex::new(None), } @@ -332,7 +335,15 @@ impl<'a> Fido2Authenticator<'a> { .clone() .ok_or(GetSelectedCredentialError::NoSelectedCredential)?; - let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?; + let mut ctx = if let Some(encryption_key) = &self.encryption_key { + let key = SymmetricKeyId::Local("device_key"); + let mut ctx = key_store.context(); + ctx.set_symmetric_key(key, encryption_key.clone())?; + ctx + } else { + key_store.context() + }; + let creds = cipher.decrypt_fido2_credentials(&mut ctx)?; let credential = creds .first() @@ -400,9 +411,13 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { // When using the credential for authentication we have to ask the user to pick one. if this.create_credential { + let mut ctx = key_store.context(); + if let Some(device_key) = &this.authenticator.encryption_key { + ctx.set_symmetric_key(SymmetricKeyId::Local("device_key"), device_key.clone())?; + } Ok(creds .into_iter() - .map(|c| CipherViewContainer::new(c, &mut key_store.context())) + .map(|c| CipherViewContainer::new(c, &mut ctx)) .collect::>()?) } else { let picked = this @@ -418,10 +433,11 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { .expect("Mutex is not poisoned") .replace(picked.clone()); - Ok(vec![CipherViewContainer::new( - picked, - &mut key_store.context(), - )?]) + let mut ctx = key_store.context(); + if let Some(device_key) = &this.authenticator.encryption_key { + ctx.set_symmetric_key(SymmetricKeyId::Local("device_key"), device_key.clone())?; + } + Ok(vec![CipherViewContainer::new(picked, &mut ctx)?]) } } @@ -484,7 +500,13 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { let key_store = this.authenticator.client.internal.get_key_store(); - selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?; + { + let mut ctx = key_store.context(); + if let Some(device_key) = &this.authenticator.encryption_key { + ctx.set_symmetric_key(SymmetricKeyId::Local("device_key"), device_key.clone())?; + } + selected.set_new_fido2_credentials(&mut ctx, vec![cred])?; + } // Store the updated credential for later use this.authenticator @@ -493,13 +515,20 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { .expect("Mutex is not poisoned") .replace(selected.clone()); - // Encrypt the updated cipher before sending it to the clients to be stored - let encrypted = key_store.encrypt(selected)?; + let cipher = if let Some(encryption_key) = &this.authenticator.encryption_key { + let key = SymmetricKeyId::Local("device_key"); + let mut ctx = key_store.context(); + ctx.set_symmetric_key(key, encryption_key.clone())?; + selected.encrypt_composite(&mut ctx, key)? + } else { + // Encrypt the updated cipher before sending it to the clients to be stored + key_store.encrypt(selected)? + }; this.authenticator .credential_store .save_credential(EncryptionContext { - cipher: encrypted, + cipher, encrypted_for: user_id, }) .await?; @@ -627,13 +656,16 @@ impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<' let new_credential = try_from_credential_new_view(user, rp) .map_err(|_| Ctap2Error::InvalidCredential)?; - let (cipher_view, user_check) = self + let (mut cipher_view, user_check) = self .authenticator .user_interface .check_user_and_pick_credential_for_creation(options, new_credential) .await .map_err(|_| Ctap2Error::OperationDenied)?; + if self.authenticator.encryption_key.is_some() { + cipher_view.device_bound = true; + } self.authenticator .selected_cipher .lock() diff --git a/crates/bitwarden-fido/src/client_fido.rs b/crates/bitwarden-fido/src/client_fido.rs index c03d5627f..43e356f65 100644 --- a/crates/bitwarden-fido/src/client_fido.rs +++ b/crates/bitwarden-fido/src/client_fido.rs @@ -1,4 +1,5 @@ -use bitwarden_core::Client; +use bitwarden_core::{Client, key_management::SymmetricKeyId}; +use bitwarden_crypto::SymmetricCryptoKey; use bitwarden_vault::CipherView; use thiserror::Error; @@ -32,8 +33,14 @@ impl ClientFido2 { &'a self, user_interface: &'a dyn Fido2UserInterface, credential_store: &'a dyn Fido2CredentialStore, + encryption_key: Option, ) -> Fido2Authenticator<'a> { - Fido2Authenticator::new(&self.client, user_interface, credential_store) + Fido2Authenticator::new( + &self.client, + user_interface, + credential_store, + encryption_key, + ) } #[allow(missing_docs)] @@ -43,7 +50,7 @@ impl ClientFido2 { credential_store: &'a dyn Fido2CredentialStore, ) -> Fido2Client<'a> { Fido2Client { - authenticator: self.create_authenticator(user_interface, credential_store), + authenticator: self.create_authenticator(user_interface, credential_store, None), } } @@ -51,12 +58,22 @@ impl ClientFido2 { pub fn decrypt_fido2_autofill_credentials( &self, cipher_view: CipherView, + encryption_key: Option, ) -> Result, DecryptFido2AutofillCredentialsError> { let key_store = self.client.internal.get_key_store(); + let mut ctx = key_store.context(); + if let Some(key) = encryption_key { + ctx.set_symmetric_key(SymmetricKeyId::Local("device_key"), key.clone()) + .map_err(|err| { + DecryptFido2AutofillCredentialsError::Fido2CredentialAutofillView( + Fido2CredentialAutofillViewError::Crypto(err), + ) + })?; + } Ok(Fido2CredentialAutofillView::from_cipher_view( &cipher_view, - &mut key_store.context(), + &mut ctx, )?) } } diff --git a/crates/bitwarden-uniffi/src/platform/fido2.rs b/crates/bitwarden-uniffi/src/platform/fido2.rs index a16c12dea..6bba2290e 100644 --- a/crates/bitwarden-uniffi/src/platform/fido2.rs +++ b/crates/bitwarden-uniffi/src/platform/fido2.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use bitwarden_crypto::{BitwardenLegacyKeyBytes, SymmetricCryptoKey}; use bitwarden_fido::{ CheckUserOptions, ClientData, Fido2CallbackError as BitFido2CallbackError, Fido2CredentialAutofillView, GetAssertionRequest, GetAssertionResult, MakeCredentialRequest, @@ -16,7 +17,7 @@ pub struct ClientFido2(pub(crate) bitwarden_fido::ClientFido2); #[uniffi::export] impl ClientFido2 { - pub fn authenticator( + pub fn vault_authenticator( &self, user_interface: Arc, credential_store: Arc, @@ -25,6 +26,23 @@ impl ClientFido2 { self.0.clone(), user_interface, credential_store, + None, + )) + } + + pub fn device_authenticator( + &self, + user_interface: Arc, + credential_store: Arc, + encryption_key: Vec, + ) -> Arc { + let key = + SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(encryption_key)).unwrap(); + Arc::new(ClientFido2Authenticator( + self.0.clone(), + user_interface, + credential_store, + Some(key), )) } @@ -37,16 +55,22 @@ impl ClientFido2 { self.0.clone(), user_interface, credential_store, + None, ))) } pub fn decrypt_fido2_autofill_credentials( &self, cipher_view: CipherView, + encryption_key: Option>, ) -> Result> { + let key = encryption_key + .map(|key| SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key))) + .transpose()?; + let result = self .0 - .decrypt_fido2_autofill_credentials(cipher_view) + .decrypt_fido2_autofill_credentials(cipher_view, key) .map_err(Error::DecryptFido2AutofillCredentials)?; Ok(result) @@ -58,6 +82,7 @@ pub struct ClientFido2Authenticator( pub(crate) bitwarden_fido::ClientFido2, pub(crate) Arc, pub(crate) Arc, + pub(crate) Option, ); #[uniffi::export] @@ -68,7 +93,7 @@ impl ClientFido2Authenticator { ) -> Result { let ui = UniffiTraitBridge(self.1.as_ref()); let cs = UniffiTraitBridge(self.2.as_ref()); - let mut auth = self.0.create_authenticator(&ui, &cs); + let mut auth = self.0.create_authenticator(&ui, &cs, self.3.clone()); let result = auth .make_credential(request) @@ -80,7 +105,7 @@ impl ClientFido2Authenticator { pub async fn get_assertion(&self, request: GetAssertionRequest) -> Result { let ui = UniffiTraitBridge(self.1.as_ref()); let cs = UniffiTraitBridge(self.2.as_ref()); - let mut auth = self.0.create_authenticator(&ui, &cs); + let mut auth = self.0.create_authenticator(&ui, &cs, self.3.clone()); let result = auth .get_assertion(request) @@ -95,7 +120,7 @@ impl ClientFido2Authenticator { ) -> Result> { let ui = UniffiTraitBridge(self.1.as_ref()); let cs = UniffiTraitBridge(self.2.as_ref()); - let mut auth = self.0.create_authenticator(&ui, &cs); + let mut auth = self.0.create_authenticator(&ui, &cs, self.3.clone()); let result = auth .silently_discover_credentials(rp_id) @@ -107,7 +132,7 @@ impl ClientFido2Authenticator { pub async fn credentials_for_autofill(&self) -> Result> { let ui = UniffiTraitBridge(self.1.as_ref()); let cs = UniffiTraitBridge(self.2.as_ref()); - let mut auth = self.0.create_authenticator(&ui, &cs); + let mut auth = self.0.create_authenticator(&ui, &cs, self.3.clone()); let result = auth .credentials_for_autofill() diff --git a/crates/bitwarden-uniffi/src/vault/ciphers.rs b/crates/bitwarden-uniffi/src/vault/ciphers.rs index 6508558eb..7645e5755 100644 --- a/crates/bitwarden-uniffi/src/vault/ciphers.rs +++ b/crates/bitwarden-uniffi/src/vault/ciphers.rs @@ -41,8 +41,11 @@ impl CiphersClient { pub fn decrypt_fido2_credentials( &self, cipher_view: CipherView, + encryption_key: Option>, ) -> Result> { - Ok(self.0.decrypt_fido2_credentials(cipher_view)?) + Ok(self + .0 + .decrypt_fido2_credentials(cipher_view, encryption_key)?) } /// Move a cipher to an organization, reencrypting the cipher key if necessary diff --git a/crates/bitwarden-uniffi/swift/build.sh b/crates/bitwarden-uniffi/swift/build.sh index a10e78b95..73a25efbf 100755 --- a/crates/bitwarden-uniffi/swift/build.sh +++ b/crates/bitwarden-uniffi/swift/build.sh @@ -9,23 +9,31 @@ cd "$(dirname "$0")" rm -rf BitwardenFFI.xcframework rm -rf tmp -mkdir -p tmp/target/universal-ios-sim/release - # Build native library export IPHONEOS_DEPLOYMENT_TARGET="13.0" export RUSTFLAGS="-C link-arg=-Wl,-application_extension" -cargo build --package bitwarden-uniffi --target aarch64-apple-ios-sim --release -cargo build --package bitwarden-uniffi --target aarch64-apple-ios --release -cargo build --package bitwarden-uniffi --target x86_64-apple-ios --release +if [[ $DEBUG_MODE = "true" ]]; then + PROFILE="debug" + PROFILE_FLAG="" +else + PROFILE="release" + PROFILE_FLAG="--release" +fi +echo "$PROFILE_FLAG" +cargo build --package bitwarden-uniffi --target aarch64-apple-ios-sim $PROFILE_FLAG +cargo build --package bitwarden-uniffi --target aarch64-apple-ios $PROFILE_FLAG +cargo build --package bitwarden-uniffi --target x86_64-apple-ios $PROFILE_FLAG + +mkdir -p tmp/target/universal-ios-sim/$PROFILE # Create universal libraries -lipo -create ../../../target/aarch64-apple-ios-sim/release/libbitwarden_uniffi.a \ - ../../../target/x86_64-apple-ios/release/libbitwarden_uniffi.a \ - -output ./tmp/target/universal-ios-sim/release/libbitwarden_uniffi.a +lipo -create ../../../target/aarch64-apple-ios-sim/$PROFILE/libbitwarden_uniffi.a \ + ../../../target/x86_64-apple-ios/$PROFILE/libbitwarden_uniffi.a \ + -output ./tmp/target/universal-ios-sim/$PROFILE/libbitwarden_uniffi.a # Generate swift bindings cargo run -p uniffi-bindgen generate \ - ../../../target/aarch64-apple-ios-sim/release/libbitwarden_uniffi.dylib \ + ../../../target/aarch64-apple-ios-sim/$PROFILE/libbitwarden_uniffi.dylib \ --library \ --language swift \ --no-format \ @@ -41,9 +49,9 @@ cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap # Build xcframework xcodebuild -create-xcframework \ - -library ../../../target/aarch64-apple-ios/release/libbitwarden_uniffi.a \ + -library ../../../target/aarch64-apple-ios/$PROFILE/libbitwarden_uniffi.a \ -headers ./tmp/Headers \ - -library ./tmp/target/universal-ios-sim/release/libbitwarden_uniffi.a \ + -library ./tmp/target/universal-ios-sim/$PROFILE/libbitwarden_uniffi.a \ -headers ./tmp/Headers \ -output ./BitwardenFFI.xcframework diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index ba8c9bb5d..ba7b06bc4 100644 --- a/crates/bitwarden-vault/src/cipher/attachment.rs +++ b/crates/bitwarden-vault/src/cipher/attachment.rs @@ -262,6 +262,7 @@ mod tests { cipher: Cipher { id: None, organization_id: None, + device_bound: false, folder_id: None, collection_ids: Vec::new(), key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()), @@ -317,6 +318,7 @@ mod tests { let cipher = Cipher { id: None, organization_id: None, + device_bound: false, folder_id: None, collection_ids: Vec::new(), key: Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap()), @@ -376,6 +378,7 @@ mod tests { let cipher = Cipher { id: None, organization_id: None, + device_bound: false, folder_id: None, collection_ids: Vec::new(), key: None, diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 5b7deb3d7..4bc1e91bc 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -108,6 +108,7 @@ pub struct EncryptionContext { pub struct Cipher { pub id: Option, pub organization_id: Option, + pub device_bound: bool, pub folder_id: Option, pub collection_ids: Vec, @@ -153,6 +154,7 @@ bitwarden_state::register_repository_item!(Cipher, "Cipher"); pub struct CipherView { pub id: Option, pub organization_id: Option, + pub device_bound: bool, pub folder_id: Option, pub collection_ids: Vec, @@ -310,6 +312,7 @@ impl CompositeEncryptable for CipherView { Ok(Cipher { id: cipher_view.id, organization_id: cipher_view.organization_id, + device_bound: cipher_view.device_bound, folder_id: cipher_view.folder_id, collection_ids: cipher_view.collection_ids, key: cipher_view.key, @@ -356,6 +359,7 @@ impl Decryptable for Cipher { let mut cipher = CipherView { id: self.id, organization_id: self.organization_id, + device_bound: self.device_bound, folder_id: self.folder_id, collection_ids: self.collection_ids.clone(), key: self.key.clone(), @@ -706,9 +710,10 @@ impl IdentifyKey for Cipher { impl IdentifyKey for CipherView { fn key_identifier(&self) -> SymmetricKeyId { - match self.organization_id { - Some(organization_id) => SymmetricKeyId::Organization(organization_id), - None => SymmetricKeyId::User, + match (self.organization_id, self.device_bound) { + (Some(organization_id), _) => SymmetricKeyId::Organization(organization_id), + (_, true) => SymmetricKeyId::Local("device_key"), + _ => SymmetricKeyId::User, } } } @@ -771,6 +776,8 @@ impl TryFrom for Cipher { revision_date: require!(cipher.revision_date).parse()?, key: EncString::try_from_optional(cipher.key)?, archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?, + // TODO(II): need to forward this to other models + device_bound: false, }) } } @@ -823,6 +830,7 @@ mod tests { }), id: Some(test_id), organization_id: None, + device_bound: false, folder_id: None, collection_ids: vec![], key: None, @@ -912,6 +920,7 @@ mod tests { deleted_date: None, revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(), archived_date: None, + device_bound: false, }; let view: CipherListView = key_store.decrypt(&cipher).unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index 94fe857ee..90671393a 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -1,9 +1,7 @@ +use bitwarden_core::{Client, OrganizationId, key_management::SymmetricKeyId}; #[cfg(feature = "wasm")] -use bitwarden_core::key_management::SymmetricKeyId; -use bitwarden_core::{Client, OrganizationId}; -use bitwarden_crypto::IdentifyKey; -#[cfg(feature = "wasm")] -use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey}; +use bitwarden_crypto::CompositeEncryptable; +use bitwarden_crypto::{BitwardenLegacyKeyBytes, IdentifyKey, SymmetricCryptoKey}; #[cfg(feature = "wasm")] use bitwarden_encoding::B64; #[cfg(feature = "wasm")] @@ -135,9 +133,16 @@ impl CiphersClient { pub fn decrypt_fido2_credentials( &self, cipher_view: CipherView, + encryption_key: Option>, ) -> Result, DecryptError> { let key_store = self.client.internal.get_key_store(); - let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?; + let mut ctx = key_store.context(); + if let Some(key) = encryption_key { + let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key))?; + ctx.set_symmetric_key(SymmetricKeyId::Local("device_key"), key)?; + } + + let credentials = cipher_view.decrypt_fido2_credentials(&mut ctx)?; Ok(credentials) } @@ -195,6 +200,7 @@ mod tests { Cipher { id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()), organization_id: None, + device_bound: false, folder_id: None, collection_ids: vec![], key: None, @@ -246,6 +252,7 @@ mod tests { }), id: Some(test_id), organization_id: None, + device_bound: false, folder_id: None, collection_ids: vec![], key: None, @@ -304,6 +311,7 @@ mod tests { .decrypt_list(vec![Cipher { id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()), organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()), + device_bound: false, folder_id: None, collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()], key: None, diff --git a/crates/bitwarden-vault/src/cipher/secure_note.rs b/crates/bitwarden-vault/src/cipher/secure_note.rs index 022fc651a..e1c106cca 100644 --- a/crates/bitwarden-vault/src/cipher/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/secure_note.rs @@ -140,6 +140,7 @@ mod tests { deleted_date: None, revision_date: "2024-01-01T00:00:00.000Z".parse().unwrap(), archived_date: None, + device_bound: false, } }