diff --git a/crates/bitwarden-exporters/src/export.rs b/crates/bitwarden-exporters/src/export.rs index e8e892658b..544d28467c 100644 --- a/crates/bitwarden-exporters/src/export.rs +++ b/crates/bitwarden-exporters/src/export.rs @@ -92,7 +92,7 @@ pub fn encrypt_import( if let Some(passkey) = passkey { let passkeys = passkey.into_iter().map(|p| p.into()).collect(); - view.set_new_fido2_credentials(ctx, passkeys)?; + view.set_new_fido2_credentials(passkeys)?; } // Select the encryption format based on the account's current security state, matching how diff --git a/crates/bitwarden-exporters/src/models.rs b/crates/bitwarden-exporters/src/models.rs index f9a60222eb..7e1a9818c1 100644 --- a/crates/bitwarden-exporters/src/models.rs +++ b/crates/bitwarden-exporters/src/models.rs @@ -25,7 +25,7 @@ impl crate::Cipher { let view: CipherView = key_store.decrypt(&cipher)?; let r = match view.r#type { - CipherType::Login => crate::CipherType::Login(Box::new(from_login(&view, key_store)?)), + CipherType::Login => crate::CipherType::Login(Box::new(from_login(&view)?)), CipherType::SecureNote => { let s = require!(view.secure_note); crate::CipherType::SecureNote(Box::new(s.into())) @@ -93,10 +93,7 @@ impl From for crate::PasswordHistory { } /// Convert a `LoginView` into a `crate::Login`. -fn from_login( - view: &CipherView, - key_store: &KeyStore, -) -> Result { +fn from_login(view: &CipherView) -> Result { let l = require!(view.login.clone()); Ok(crate::Login { @@ -110,11 +107,16 @@ fn from_login( .collect(), totp: l.totp, fido2_credentials: l.fido2_credentials.as_ref().and_then(|_| { - let credentials = view.get_fido2_credentials(&mut key_store.context()).ok()?; + let credentials = view.get_fido2_credentials(); if credentials.is_empty() { None } else { - Some(credentials.into_iter().map(|c| c.into()).collect()) + Some( + credentials + .into_iter() + .map(|c| Fido2CredentialFullView::from(c).into()) + .collect(), + ) } }), }) @@ -269,9 +271,6 @@ mod tests { #[test] fn test_from_login() { - let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); - let key_store = create_test_crypto_with_user_key(key); - let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap(); let view = CipherView { r#type: CipherType::Login, @@ -315,7 +314,7 @@ mod tests { archived_date: None, }; - let login = from_login(&view, &key_store).unwrap(); + let login = from_login(&view).unwrap(); assert_eq!(login.username, Some("test_username".to_string())); assert_eq!(login.password, Some("test_password".to_string())); diff --git a/crates/bitwarden-fido/src/authenticator.rs b/crates/bitwarden-fido/src/authenticator.rs index 3c26f5c922..32d4b8f3be 100644 --- a/crates/bitwarden-fido/src/authenticator.rs +++ b/crates/bitwarden-fido/src/authenticator.rs @@ -261,18 +261,16 @@ impl<'a> Fido2Authenticator<'a> { rp_id: String, user_handle: Option>, ) -> Result, SilentlyDiscoverCredentialsError> { - let key_store = self.client.internal.get_key_store(); let result = self .credential_store .find_credentials(None, rp_id, user_handle) .await?; - let mut ctx = key_store.context(); result .into_iter() .map( |cipher| -> Result, SilentlyDiscoverCredentialsError> { - Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?) + Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher)?) }, ) .flatten_ok() @@ -326,8 +324,6 @@ impl<'a> Fido2Authenticator<'a> { pub(super) fn get_selected_credential( &self, ) -> Result { - let key_store = self.client.internal.get_key_store(); - let cipher = self .selected_cipher .lock() @@ -335,7 +331,7 @@ impl<'a> Fido2Authenticator<'a> { .clone() .ok_or(GetSelectedCredentialError::NoSelectedCredential)?; - let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?; + let creds = cipher.get_fido2_credentials(); let credential = creds .first() @@ -400,13 +396,11 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { }) .collect(); - let key_store = this.authenticator.client.internal.get_key_store(); - // When using the credential for authentication we have to ask the user to pick one. if this.create_credential { Ok(creds .into_iter() - .map(|c| CipherViewContainer::new(c, &mut key_store.context())) + .map(CipherViewContainer::new) .collect::>()?) } else { let picked = this @@ -422,10 +416,7 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { .expect("Mutex is not poisoned") .replace(picked.clone()); - Ok(vec![CipherViewContainer::new( - picked, - &mut key_store.context(), - )?]) + Ok(vec![CipherViewContainer::new(picked)?]) } } @@ -480,9 +471,7 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { .clone() .ok_or(InnerError::NoSelectedCredential)?; - let key_store = this.authenticator.client.internal.get_key_store(); - - selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?; + selected.set_new_fido2_credentials(vec![cred])?; // Store the updated credential for later use this.authenticator @@ -554,10 +543,8 @@ impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> { let cred = fill_with_credential(&selected.credential, cred)?; - let key_store = this.authenticator.client.internal.get_key_store(); - let mut selected = selected.cipher; - selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?; + selected.set_new_fido2_credentials(vec![cred])?; // Store the updated credential for later use this.authenticator @@ -681,15 +668,12 @@ fn map_ui_hint(hint: UiHint<'_, CipherViewContainer>) -> UiHint<'_, CipherView> #[cfg(test)] mod tests { use async_trait::async_trait; - use bitwarden_core::{ - Client, - key_management::{KeySlotIds, SymmetricKeySlotId}, - }; - use bitwarden_crypto::{KeyStoreContext, PrimitiveEncryptable, SymmetricCryptoKey}; + use bitwarden_core::{Client, key_management::SymmetricKeySlotId}; + use bitwarden_crypto::SymmetricCryptoKey; use bitwarden_encoding::B64Url; use bitwarden_vault::{ CipherListView, CipherRepromptType, CipherType, CipherView, EncryptionContext, - Fido2Credential, Fido2CredentialNewView, LoginView, + Fido2CredentialNewView, Fido2CredentialView, LoginView, }; use passkey::authenticator::UiHint; @@ -783,23 +767,22 @@ mod tests { 0x84, 0x05, 0x71, ]; - fn create_test_cipher(ctx: &mut KeyStoreContext) -> CipherView { - let key = SymmetricKeySlotId::User; + fn create_test_cipher() -> CipherView { let key_value = B64Url::from(TEST_FIDO_P256_KEY).to_string(); - let fido2_credential = Fido2Credential { - credential_id: TEST_FIDO_CREDENTIAL_ID.encrypt(ctx, key).unwrap(), - key_type: "public-key".to_string().encrypt(ctx, key).unwrap(), - key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(), - key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(), - key_value: key_value.encrypt(ctx, key).unwrap(), - rp_id: TEST_FIDO_RP_ID.encrypt(ctx, key).unwrap(), - user_handle: Some(TEST_FIDO_USER_HANDLE.encrypt(ctx, key).unwrap()), + let fido2_credential = Fido2CredentialView { + credential_id: TEST_FIDO_CREDENTIAL_ID.to_string(), + key_type: "public-key".to_string(), + key_algorithm: "ECDSA".to_string(), + key_curve: "P-256".to_string(), + key_value, + rp_id: TEST_FIDO_RP_ID.to_string(), + user_handle: Some(TEST_FIDO_USER_HANDLE.to_string()), user_name: None, - counter: "0".to_string().encrypt(ctx, key).unwrap(), + counter: "0".to_string(), rp_name: None, user_display_name: None, - discoverable: "true".to_string().encrypt(ctx, key).unwrap(), + discoverable: "true".to_string(), creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(), }; @@ -867,10 +850,7 @@ mod tests { .set_symmetric_key(SymmetricKeySlotId::User, user_key) .unwrap(); - let cipher = { - let mut ctx = client.internal.get_key_store().context(); - create_test_cipher(&mut ctx) - }; + let cipher = create_test_cipher(); let user_interface = MockUserInterface; let credential_store = MockCredentialStore { cipher }; diff --git a/crates/bitwarden-fido/src/client_fido.rs b/crates/bitwarden-fido/src/client_fido.rs index 66d15e42d6..9ae49cd340 100644 --- a/crates/bitwarden-fido/src/client_fido.rs +++ b/crates/bitwarden-fido/src/client_fido.rs @@ -65,12 +65,7 @@ impl ClientFido2 { &self, cipher_view: CipherView, ) -> Result, DecryptFido2AutofillCredentialsError> { - let key_store = self.client.internal.get_key_store(); - - Ok(Fido2CredentialAutofillView::from_cipher_view( - &cipher_view, - &mut key_store.context(), - )?) + Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher_view)?) } } diff --git a/crates/bitwarden-fido/src/lib.rs b/crates/bitwarden-fido/src/lib.rs index 09f664dc67..d9bc384add 100644 --- a/crates/bitwarden-fido/src/lib.rs +++ b/crates/bitwarden-fido/src/lib.rs @@ -1,7 +1,5 @@ #![doc = include_str!("../README.md")] -use bitwarden_core::key_management::KeySlotIds; -use bitwarden_crypto::KeyStoreContext; use bitwarden_encoding::{B64Url, NotB64UrlEncodedError}; use bitwarden_vault::{ CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView, @@ -73,8 +71,12 @@ pub(crate) struct CipherViewContainer { } impl CipherViewContainer { - fn new(cipher: CipherView, ctx: &mut KeyStoreContext) -> Result { - let fido2_credentials = cipher.get_fido2_credentials(ctx)?; + fn new(cipher: CipherView) -> Result { + let fido2_credentials = cipher + .get_fido2_credentials() + .into_iter() + .map(Fido2CredentialFullView::from) + .collect(); Ok(Self { cipher, fido2_credentials, diff --git a/crates/bitwarden-fido/src/types.rs b/crates/bitwarden-fido/src/types.rs index 7a0ff0265f..c10a66f4b5 100644 --- a/crates/bitwarden-fido/src/types.rs +++ b/crates/bitwarden-fido/src/types.rs @@ -1,7 +1,6 @@ use std::{borrow::Cow, collections::HashMap}; -use bitwarden_core::key_management::KeySlotIds; -use bitwarden_crypto::{CryptoError, KeyStoreContext}; +use bitwarden_crypto::CryptoError; use bitwarden_encoding::{B64Url, NotB64UrlEncodedError}; use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView}; use passkey::types::webauthn::UserVerificationRequirement; @@ -70,9 +69,8 @@ impl Fido2CredentialAutofillView { #[allow(missing_docs)] pub fn from_cipher_view( cipher: &CipherView, - ctx: &mut KeyStoreContext, ) -> Result, Fido2CredentialAutofillViewError> { - let credentials = cipher.decrypt_fido2_credentials(ctx)?; + let credentials = cipher.get_fido2_credentials(); credentials .iter() diff --git a/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs b/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs index 290519cfb9..7cf59342ad 100644 --- a/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs +++ b/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs @@ -408,7 +408,7 @@ mod tests { // A legacy cipher that already carries a per-item cipher key let mut cipher = make_cipher_view(); - cipher.generate_cipher_key(&mut ctx, user_key_old).unwrap(); + cipher.generate_cipher_key(&mut ctx).unwrap(); let encrypted = EncryptMode::Legacy(cipher.clone()) .encrypt_composite(&mut ctx, user_key_old) .unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/attachment.rs b/crates/bitwarden-vault/src/cipher/attachment.rs index dd7cbc8fa7..ef7e42ca50 100644 --- a/crates/bitwarden-vault/src/cipher/attachment.rs +++ b/crates/bitwarden-vault/src/cipher/attachment.rs @@ -75,46 +75,8 @@ pub struct AttachmentView { pub size: Option, pub size_name: Option, pub file_name: Option, - pub key: Option, - /// The decrypted attachmentkey in base64 format. - /// - /// **TEMPORARY FIELD**: This field is a temporary workaround to provide - /// decrypted attachment keys to the TypeScript client during the migration - /// process. It will be removed once the encryption/decryption logic is - /// fully migrated to the SDK. - /// - /// **Ticket**: - /// - /// Do not rely on this field for long-term use. - #[cfg(feature = "wasm")] - pub decrypted_key: Option, -} - -impl AttachmentView { - pub(crate) fn reencrypt_key( - &mut self, - ctx: &mut KeyStoreContext, - old_key: SymmetricKeySlotId, - new_key: SymmetricKeySlotId, - ) -> Result<(), CryptoError> { - if let Some(attachment_key) = &mut self.key { - let tmp_attachment_key_id = ctx.unwrap_symmetric_key(old_key, attachment_key)?; - *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?; - } - Ok(()) - } - - pub(crate) fn reencrypt_keys( - attachment_views: &mut Vec, - ctx: &mut KeyStoreContext, - old_key: SymmetricKeySlotId, - new_key: SymmetricKeySlotId, - ) -> Result<(), CryptoError> { - for attachment in attachment_views { - attachment.reencrypt_key(ctx, old_key, new_key)?; - } - Ok(()) - } + #[cfg_attr(feature = "wasm", tsify(type = "SymmetricKey | undefined"))] + pub key: Option, } #[allow(missing_docs)] @@ -173,7 +135,9 @@ impl CompositeEncryptable> for AttachmentFile { ctx: &mut KeyStoreContext, key: SymmetricKeySlotId, ) -> Result, CryptoError> { - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.cipher.key).map_err(|e| { - tracing::warn!( - attachment_id = ?self.attachment.id, - cipher_id = ?self.cipher.id, - has_cipher_key = self.cipher.key.is_some(), - error = %e, - "Failed to decrypt cipher key for attachment" - ); - e - })?; - - // Version 2 or 3, `AttachmentKey` or `CipherKey(AttachmentKey)` + // Version 2 or 3: attachment view already holds the raw attachment key if let Some(attachment_key) = &self.attachment.key { - let content_key = ctx - .unwrap_symmetric_key(ciphers_key, attachment_key) - .map_err(|e| { - tracing::warn!( - attachment_id = ?self.attachment.id, - cipher_id = ?self.cipher.id, - error = %e, - "Failed to unwrap attachment key (v2/v3)" - ); - e - })?; + let content_key = ctx.add_local_symmetric_key(attachment_key.clone()); self.contents.decrypt(ctx, content_key).map_err(|e| { tracing::warn!( attachment_id = ?self.attachment.id, @@ -252,10 +195,6 @@ impl Decryptable> for AttachmentFile { } } -// ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::CompositeEncryptable`: `AttachmentView` retains -// key-bound ciphertext (`key`, the attachment content key wrapped under the cipher key) and copies -// it through unchanged (`key: self.key.clone()` below) instead of re-wrapping it under `key`. As a -// result decrypt(K) -> encrypt(K1) -> decrypt(K1) does NOT round-trip. impl CompositeEncryptable for AttachmentView { fn encrypt_composite( &self, @@ -268,9 +207,14 @@ impl CompositeEncryptable for Attach size: self.size.clone(), size_name: self.size_name.clone(), file_name: self.file_name.encrypt(ctx, key)?, - // ⚠️ pass-through of wrapped key-bound ciphertext — see the contract-violation note - // above. - key: self.key.clone(), + key: self + .key + .as_ref() + .map(|k| { + let slot = ctx.add_local_symmetric_key(k.clone()); + ctx.wrap_symmetric_key(key, slot) + }) + .transpose()?, }) } } @@ -281,17 +225,12 @@ impl Decryptable for Attachment ctx: &mut KeyStoreContext, key: SymmetricKeySlotId, ) -> Result { - // Decrypt the file name or return an error if decryption fails let file_name = self.file_name.decrypt(ctx, key)?; - #[cfg(feature = "wasm")] let decrypted_key = if let Some(attachment_key) = &self.key { let content_key_id = ctx.unwrap_symmetric_key(key, attachment_key)?; - #[allow(deprecated)] - let actual_key = ctx.dangerous_get_symmetric_key(content_key_id)?; - - Some(actual_key.to_base64()) + Some(ctx.dangerous_get_symmetric_key(content_key_id)?.clone()) } else { None }; @@ -302,14 +241,7 @@ impl Decryptable for Attachment size: self.size.clone(), size_name: self.size_name.clone(), file_name, - // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting - // `AttachmentView` is a decrypted DTO, yet `key` (the attachment content key wrapped - // under the cipher key) is copied through still encrypted (`self.key.clone()`) rather - // than decrypted. The wrapped key is therefore key-bound to the original cipher key, - // which is what makes the `CompositeEncryptable` pass-through above non-round-tripping. - key: self.key.clone(), - #[cfg(feature = "wasm")] - decrypted_key: decrypted_key.map(|k| k.to_string()), + key: decrypted_key, }) } } @@ -330,15 +262,18 @@ pub(crate) fn decrypt_attachments_with_failures( Ok(decrypted) => successes.push(decrypted), Err(e) => { tracing::warn!(attachment_id = ?attachment.id, error = %e, "Failed to decrypt attachment"); + let recovered_key = attachment.key.as_ref().and_then(|attachment_key| { + let slot = ctx.unwrap_symmetric_key(key, attachment_key).ok()?; + #[allow(deprecated)] + Some(ctx.dangerous_get_symmetric_key(slot).ok()?.clone()) + }); failures.push(AttachmentView { id: attachment.id.clone(), url: attachment.url.clone(), size: attachment.size.clone(), size_name: attachment.size_name.clone(), file_name: None, - key: attachment.key.clone(), - #[cfg(feature = "wasm")] - decrypted_key: None, + key: recovered_key, }); } } @@ -399,8 +334,6 @@ mod tests { size_name: Some("100 Bytes".into()), file_name: Some("Test.txt".into()), key: None, - #[cfg(feature = "wasm")] - decrypted_key: None, }; let contents = b"This is a test file that we will encrypt. It's 100 bytes long, the encrypted version will be longer!"; @@ -455,15 +388,32 @@ mod tests { let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap(); let key_store = create_test_crypto_with_user_key(user_key); + let attachment_key = { + let mut ctx = key_store.context(); + let cipher_key_enc: EncString = "2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap(); + let cipher_key_slot = Cipher::decrypt_cipher_key( + &mut ctx, + bitwarden_core::key_management::SymmetricKeySlotId::User, + &Some(cipher_key_enc), + ) + .unwrap(); + let att_key_enc: EncString = "2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap(); + let att_key_slot = ctx + .unwrap_symmetric_key(cipher_key_slot, &att_key_enc) + .unwrap(); + #[allow(deprecated)] + ctx.dangerous_get_symmetric_key(att_key_slot) + .unwrap() + .clone() + }; + let attachment = AttachmentView { id: None, url: None, size: Some("161".into()), size_name: Some("161 Bytes".into()), file_name: Some("Test.txt".into()), - key: Some("2.r288/AOSPiaLFkW07EBGBw==|SAmnnCbOLFjX5lnURvoualOetQwuyPc54PAmHDTRrhT0gwO9ailna9U09q9bmBfI5XrjNNEsuXssgzNygRkezoVQvZQggZddOwHB6KQW5EQ=|erIMUJp8j+aTcmhdE50zEX+ipv/eR1sZ7EwULJm/6DY=".parse().unwrap()), - #[cfg(feature = "wasm")] - decrypted_key: None, + key: Some(attachment_key), }; let cipher = Cipher { @@ -526,8 +476,6 @@ mod tests { size_name: Some("161 Bytes".into()), file_name: Some("Test.txt".into()), key: None, - #[cfg(feature = "wasm")] - decrypted_key: None, }; let cipher = Cipher { diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/bank_account.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/bank_account.rs index 618e41ae12..11e9a3acad 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/bank_account.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/bank_account.rs @@ -24,9 +24,6 @@ mod tests { #[test] fn test_bank_account_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My Bank Account".to_string(), notes: None, @@ -46,10 +43,9 @@ mod tests { ..create_shell_cipher_view(CipherType::BankAccount) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::BankAccount); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My Bank Account"); assert_eq!(restored.r#type, CipherType::BankAccount); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/card.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/card.rs index 8c92eb4c6d..daa85076d2 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/card.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/card.rs @@ -13,9 +13,6 @@ mod tests { #[test] fn test_card_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My Card".to_string(), notes: None, @@ -31,10 +28,9 @@ mod tests { ..create_shell_cipher_view(CipherType::Card) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::Card); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My Card"); assert_eq!(restored.r#type, CipherType::Card); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/drivers_license.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/drivers_license.rs index 763eec1dfb..768a660b36 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/drivers_license.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/drivers_license.rs @@ -43,9 +43,6 @@ mod tests { #[test] fn test_drivers_license_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My Driver's License".to_string(), notes: None, @@ -66,10 +63,9 @@ mod tests { ..create_shell_cipher_view(CipherType::DriversLicense) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::DriversLicense); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My Driver's License"); assert_eq!(restored.r#type, CipherType::DriversLicense); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/identity.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/identity.rs index 4d8d941d98..f782fce8fd 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/identity.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/identity.rs @@ -32,9 +32,6 @@ mod tests { #[test] fn test_identity_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My Identity".to_string(), notes: Some("Identity notes".to_string()), @@ -62,10 +59,9 @@ mod tests { ..create_shell_cipher_view(CipherType::Identity) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::Identity); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My Identity"); assert_eq!(restored.r#type, CipherType::Identity); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/login.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/login.rs index e508d8a8b2..8a34c62767 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/login.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/login.rs @@ -65,7 +65,6 @@ impl From<&Fido2CredentialDataV1> for Fido2CredentialFullView { #[cfg(test)] mod tests { - use bitwarden_crypto::{CompositeEncryptable, Decryptable}; use chrono::{TimeZone, Utc}; use super::super::{CipherBlobV1, CipherTypeDataV1, LoginUriDataV1, test_support::*}; @@ -73,7 +72,9 @@ mod tests { cipher::CipherType, field::{FieldType, FieldView}, linked_id::{LinkedIdType, LoginLinkedIdType}, - login::{Fido2Credential, Fido2CredentialFullView, LoginUriView, LoginView, UriMatchType}, + login::{ + Fido2CredentialFullView, Fido2CredentialView, LoginUriView, LoginView, UriMatchType, + }, }; #[test] @@ -182,11 +183,8 @@ mod tests { #[test] fn test_login_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - - // Create fido2 credentials by encrypting a FullView - let fido2_full = Fido2CredentialFullView { + // Create a decrypted fido2 credential view + let fido2_view = Fido2CredentialView { credential_id: "cred-123".to_string(), key_type: "public-key".to_string(), key_algorithm: "ECDSA".to_string(), @@ -201,8 +199,6 @@ mod tests { discoverable: "true".to_string(), creation_date: Utc.with_ymd_and_hms(2024, 6, 1, 10, 30, 0).unwrap(), }; - let encrypted_fido2: Fido2Credential = - fido2_full.encrypt_composite(&mut ctx, key_id).unwrap(); let original = crate::CipherView { name: "My Login".to_string(), @@ -219,7 +215,7 @@ mod tests { }]), totp: Some("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP".to_string()), autofill_on_page_load: Some(true), - fido2_credentials: Some(vec![encrypted_fido2]), + fido2_credentials: Some(vec![fido2_view]), }), fields: Some(vec![FieldView { name: Some("Custom Field".to_string()), @@ -234,7 +230,7 @@ mod tests { ..create_shell_cipher_view(CipherType::Login) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); // Verify blob intermediate state assert_eq!(blob.name, "My Login"); @@ -255,8 +251,7 @@ mod tests { // Round-trip back let mut restored = create_shell_cipher_view(CipherType::Login); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My Login"); assert_eq!(restored.notes, Some("Login notes".to_string())); @@ -278,15 +273,13 @@ mod tests { assert_eq!(uris[0].r#match, Some(UriMatchType::Domain)); assert_eq!(uris[0].uri_checksum, None); - // Fido2 credentials should be re-encrypted + // Fido2 credentials should round-trip as decrypted views let fido2 = login.fido2_credentials.unwrap(); assert_eq!(fido2.len(), 1); - // Decrypt to verify content survived the round-trip - let decrypted: Fido2CredentialFullView = fido2[0].decrypt(&mut ctx, key_id).unwrap(); - assert_eq!(decrypted.credential_id, "cred-123"); - assert_eq!(decrypted.counter, "42"); - assert_eq!(decrypted.discoverable, "true"); - assert_eq!(decrypted.rp_id, "example.com"); + assert_eq!(fido2[0].credential_id, "cred-123"); + assert_eq!(fido2[0].counter, "42"); + assert_eq!(fido2[0].discoverable, "true"); + assert_eq!(fido2[0].rp_id, "example.com"); // Fields and password history assert_eq!(restored.fields.as_ref().unwrap().len(), 1); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/mod.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/mod.rs index 8a1f4164c4..3092eb789e 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/mod.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/mod.rs @@ -1,5 +1,4 @@ -use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId}; -use bitwarden_crypto::{CompositeEncryptable, CryptoError, Decryptable, KeyStoreContext}; +use bitwarden_crypto::CryptoError; use super::v1::*; use crate::{ @@ -11,7 +10,7 @@ use crate::{ drivers_license::DriversLicenseView, field::FieldView, identity::IdentityView, - login::{Fido2CredentialFullView, LoginUriView, LoginView}, + login::{Fido2CredentialFullView, Fido2CredentialView, LoginUriView, LoginView}, passport::PassportView, secure_note::SecureNoteView, ssh_key::SshKeyView, @@ -57,11 +56,7 @@ mod secure_note; mod ssh_key; impl CipherBlobV1 { - pub(crate) fn from_cipher_view( - view: &CipherView, - ctx: &mut KeyStoreContext, - key: SymmetricKeySlotId, - ) -> Result { + pub(crate) fn from_cipher_view(view: &CipherView) -> Result { let type_data = match view.r#type { CipherType::Login => { let login = view @@ -69,14 +64,19 @@ impl CipherBlobV1 { .as_ref() .ok_or(CryptoError::MissingField("login"))?; - let fido2_credentials: Vec = login + let fido2_credentials = login .fido2_credentials - .as_ref() - .map(|creds| -> Result, CryptoError> { - let full_views: Vec = creds.decrypt(ctx, key)?; - Ok(full_views.iter().map(Fido2CredentialDataV1::from).collect()) + .as_deref() + .map(|creds| { + creds + .iter() + .map(|v| { + Fido2CredentialDataV1::from(&Fido2CredentialFullView::from( + v.clone(), + )) + }) + .collect() }) - .transpose()? .unwrap_or_default(); CipherTypeDataV1::Login(LoginDataV1 { @@ -161,12 +161,7 @@ impl CipherBlobV1 { }) } - pub(crate) fn apply_to_cipher_view( - &self, - view: &mut CipherView, - ctx: &mut KeyStoreContext, - key: SymmetricKeySlotId, - ) -> Result<(), CryptoError> { + pub(crate) fn apply_to_cipher_view(&self, view: &mut CipherView) -> Result<(), CryptoError> { view.name = self.name.clone(); view.notes = self.notes.clone(); view.fields = none_if_empty(self.fields.iter().map(FieldView::from).collect()); @@ -191,12 +186,13 @@ impl CipherBlobV1 { let fido2_credentials = if login_data.fido2_credentials.is_empty() { None } else { - let full_views: Vec = login_data - .fido2_credentials - .iter() - .map(Fido2CredentialFullView::from) - .collect(); - Some(full_views.encrypt_composite(ctx, key)?) + Some( + login_data + .fido2_credentials + .iter() + .map(|d| Fido2CredentialView::from(Fido2CredentialFullView::from(d))) + .collect::>(), + ) }; view.r#type = CipherType::Login; @@ -314,9 +310,6 @@ mod tests { #[test] fn test_option_vec_normalization_none_to_empty_to_none() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "Minimal Note".to_string(), notes: None, @@ -329,23 +322,19 @@ mod tests { ..create_shell_cipher_view(CipherType::SecureNote) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); assert!(blob.fields.is_empty()); assert!(blob.password_history.is_empty()); let mut restored = create_shell_cipher_view(CipherType::SecureNote); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert!(restored.fields.is_none()); assert!(restored.password_history.is_none()); } #[test] fn test_login_none_uris_and_fido2_normalization() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "Simple Login".to_string(), notes: None, @@ -362,7 +351,7 @@ mod tests { ..create_shell_cipher_view(CipherType::Login) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); if let CipherTypeDataV1::Login(ref login_data) = blob.type_data { assert!(login_data.uris.is_empty()); @@ -372,8 +361,7 @@ mod tests { } let mut restored = create_shell_cipher_view(CipherType::Login); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); let login = restored.login.unwrap(); assert!(login.uris.is_none()); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/passport.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/passport.rs index 15f9db91bb..121bbb477d 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/passport.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/passport.rs @@ -47,9 +47,6 @@ mod tests { #[test] fn test_passport_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My Passport".to_string(), notes: None, @@ -72,10 +69,9 @@ mod tests { ..create_shell_cipher_view(CipherType::Passport) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::Passport); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My Passport"); assert_eq!(restored.r#type, CipherType::Passport); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/secure_note.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/secure_note.rs index 28b89e7603..04bf3f29cd 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/secure_note.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/secure_note.rs @@ -18,9 +18,6 @@ mod tests { #[test] fn test_secure_note_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My Secure Note".to_string(), notes: Some("Secret notes".to_string()), @@ -41,10 +38,9 @@ mod tests { ..create_shell_cipher_view(CipherType::SecureNote) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::SecureNote); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, original.name); assert_eq!(restored.notes, original.notes); diff --git a/crates/bitwarden-vault/src/cipher/blob/conversions/ssh_key.rs b/crates/bitwarden-vault/src/cipher/blob/conversions/ssh_key.rs index d7efcd66b3..ae84fcafa3 100644 --- a/crates/bitwarden-vault/src/cipher/blob/conversions/ssh_key.rs +++ b/crates/bitwarden-vault/src/cipher/blob/conversions/ssh_key.rs @@ -13,9 +13,6 @@ mod tests { #[test] fn test_ssh_key_cipher_round_trip() { - let (key_store, key_id) = create_test_key_store(); - let mut ctx = key_store.context_mut(); - let original = crate::CipherView { name: "My SSH Key".to_string(), notes: None, @@ -28,10 +25,9 @@ mod tests { ..create_shell_cipher_view(CipherType::SshKey) }; - let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap(); + let blob = CipherBlobV1::from_cipher_view(&original).unwrap(); let mut restored = create_shell_cipher_view(CipherType::SshKey); - blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id) - .unwrap(); + blob.apply_to_cipher_view(&mut restored).unwrap(); assert_eq!(restored.name, "My SSH Key"); assert_eq!(restored.r#type, CipherType::SshKey); diff --git a/crates/bitwarden-vault/src/cipher/blob/encryption.rs b/crates/bitwarden-vault/src/cipher/blob/encryption.rs index ec89b8deb3..d1d877bad3 100644 --- a/crates/bitwarden-vault/src/cipher/blob/encryption.rs +++ b/crates/bitwarden-vault/src/cipher/blob/encryption.rs @@ -36,15 +36,15 @@ impl From for CryptoError { } } -/// Seals a `CipherView` into an opaque blob string, using `wrapping_key` as -/// the outer key that protects the cipher's wrapped CEK. +/// Seals a `CipherView` into an opaque blob string under the given `cipher_key` slot. +/// The caller is responsible for loading the key slot before calling (e.g. via +/// `CipherView::load_cipher_key_slot`); this avoids allocating a duplicate slot. fn seal_cipher( view: &CipherView, ctx: &mut KeyStoreContext, - wrapping_key: SymmetricKeySlotId, + cipher_key: SymmetricKeySlotId, ) -> Result { - let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &view.key)?; - let blob = CipherBlobLatest::from_cipher_view(view, ctx, cipher_key)?; + let blob = CipherBlobLatest::from_cipher_view(view)?; seal_blob_content(blob, cipher_key, ctx) } @@ -94,12 +94,12 @@ pub(crate) fn encrypt_blob_cipher_with_wrapping_key( wrapping_key: SymmetricKeySlotId, ) -> Result { if view.key.is_none() { - view.generate_cipher_key(ctx, wrapping_key)?; + view.generate_cipher_key(ctx)?; } - let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &view.key)?; + let cipher_key = view.load_cipher_key_slot(ctx, wrapping_key)?; - let sealed_string = seal_cipher(view, ctx, wrapping_key)?; + let sealed_string = seal_cipher(view, ctx, cipher_key)?; let attachments = view.attachments.encrypt_composite(ctx, cipher_key)?; let local_data = view.local_data.encrypt_composite(ctx, cipher_key)?; @@ -113,7 +113,11 @@ pub(crate) fn encrypt_blob_cipher_with_wrapping_key( organization_id: view.organization_id, folder_id: view.folder_id, collection_ids: view.collection_ids.clone(), - key: view.key.clone(), + key: view + .key + .as_ref() + .map(|_| ctx.wrap_symmetric_key(wrapping_key, cipher_key)) + .transpose()?, r#type: view.r#type, favorite: view.favorite, reprompt: view.reprompt, @@ -180,7 +184,12 @@ pub(crate) fn decrypt_blob_cipher( organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids.clone(), - key: cipher.key.clone(), + key: if cipher.key.is_some() { + #[allow(deprecated)] + Some(ctx.dangerous_get_symmetric_key(cipher_key)?.clone()) + } else { + None + }, r#type: cipher.r#type, favorite: cipher.favorite, reprompt: cipher.reprompt, @@ -213,7 +222,7 @@ pub(crate) fn decrypt_blob_cipher( password_history: None, }; - blob.apply_to_cipher_view(&mut view, ctx, cipher_key)?; + blob.apply_to_cipher_view(&mut view)?; Ok(view) } @@ -310,13 +319,18 @@ mod tests { view.secure_note = Some(SecureNoteView { r#type: SecureNoteType::Generic, }); - view.generate_cipher_key(&mut ctx, view.key_identifier()) - .unwrap(); + view.generate_cipher_key(&mut ctx).unwrap(); - let sealed_string = seal_cipher(&view, &mut ctx, view.key_identifier()).unwrap(); + let cipher_key = view + .load_cipher_key_slot(&mut ctx, view.key_identifier()) + .unwrap(); + let sealed_string = seal_cipher(&view, &mut ctx, cipher_key).unwrap(); let mut cipher = make_test_cipher_with_data(&mut ctx, Some(sealed_string)); - cipher.key = view.key.clone(); + if let Some(key) = &view.key { + let slot = ctx.add_local_symmetric_key(key.clone()); + cipher.key = Some(ctx.wrap_symmetric_key(view.key_identifier(), slot).unwrap()); + } let view = decrypt_blob_cipher( &cipher, diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 2fb23f0b82..d7a69b11f1 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -37,8 +37,8 @@ use super::{ passport, secure_note, ssh_key, }; use crate::{ - AttachmentView, DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView, - FieldView, FolderId, Login, LoginView, VaultParseError, + DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView, FieldView, FolderId, + Login, LoginView, VaultParseError, password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES}, }; @@ -437,8 +437,8 @@ pub struct CipherView { pub folder_id: Option, pub collection_ids: Vec, - /// Temporary, required to support re-encrypting existing items. - pub key: Option, + #[cfg_attr(feature = "wasm", tsify(type = "SymmetricKey | undefined"))] + pub key: Option, pub name: String, pub notes: Option, @@ -533,9 +533,6 @@ pub struct CipherListView { pub folder_id: Option, pub collection_ids: Vec, - /// Temporary, required to support calculating TOTP from CipherListView. - pub key: Option, - pub name: String, pub subtitle: String, @@ -623,35 +620,32 @@ pub struct ListOrganizationCiphersResult { } impl CipherListView { - pub(crate) fn get_totp_key( - self, - ctx: &mut KeyStoreContext, - ) -> Result, CryptoError> { - let key = self.key_identifier(); - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - - let totp = match self.r#type { - CipherListViewType::Login(LoginListView { totp, .. }) => { - totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()? - } + pub(crate) fn get_totp_key(self) -> Result, CryptoError> { + Ok(match self.r#type { + CipherListViewType::Login(LoginListView { totp, .. }) => totp, _ => None, - }; - - Ok(totp) + }) } } -// ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::CompositeEncryptable`: `CipherView` retains key-bound -// ciphertext (`key`, the cipher content-encryption key wrapped under the decrypting key) and copies -// it through unchanged (`key: cipher_view.key` below) instead of re-wrapping it under `key`. As a -// result decrypt(K) -> encrypt(K1) -> decrypt(K1) does NOT round-trip. impl CipherView { + pub(crate) fn load_cipher_key_slot( + &self, + ctx: &mut KeyStoreContext, + wrapping_key: SymmetricKeySlotId, + ) -> Result { + match &self.key { + Some(key) => Ok(ctx.add_local_symmetric_key(key.clone())), + None => Ok(wrapping_key), + } + } + fn encrypt_legacy_field_encryption( &self, ctx: &mut KeyStoreContext, key: SymmetricKeySlotId, ) -> Result { - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; + let ciphers_key = self.load_cipher_key_slot(ctx, key)?; let mut cipher_view = self.clone(); cipher_view.generate_checksums(); @@ -661,9 +655,11 @@ impl CipherView { organization_id: cipher_view.organization_id, folder_id: cipher_view.folder_id, collection_ids: cipher_view.collection_ids, - // ⚠️ pass-through of wrapped key-bound ciphertext — see the contract-violation note - // above. - key: cipher_view.key, + key: cipher_view + .key + .as_ref() + .map(|_| ctx.wrap_symmetric_key(key, ciphers_key)) + .transpose()?, name: Some(cipher_view.name.encrypt(ctx, ciphers_key)?), notes: cipher_view.notes.encrypt(ctx, ciphers_key)?, r#type: cipher_view.r#type, @@ -729,13 +725,12 @@ pub(crate) fn lenient_decrypt_cipher_view( organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids.clone(), - // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting `CipherView` is a - // decrypted DTO, yet `key` (the cipher's content key wrapped under the user/org key) is - // copied through still encrypted (`cipher.key.clone()`) rather than decrypted, because - // `CipherView` stores it as an `EncString`. The wrapped key is therefore key-bound to the - // original user/org key: a `CipherView` cannot be re-encrypted under a different user/org - // key without explicitly rewrapping `key`. - key: cipher.key.clone(), + key: if cipher.key.is_some() { + #[allow(deprecated)] + Some(ctx.dangerous_get_symmetric_key(ciphers_key)?.clone()) + } else { + None + }, name: cipher .name .as_ref() @@ -902,17 +897,11 @@ impl CipherView { pub fn generate_cipher_key( &mut self, ctx: &mut KeyStoreContext, - wrapping_key: SymmetricKeySlotId, ) -> Result<(), CryptoError> { - let old_unwrapping_key = self.key_identifier(); - let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, old_unwrapping_key, &self.key)?; - let new_key = ctx.generate_symmetric_key(); - - self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?; - self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?; - - self.key = Some(ctx.wrap_symmetric_key(wrapping_key, new_key)?); + #[allow(deprecated)] + let new_key_raw = ctx.dangerous_get_symmetric_key(new_key)?.clone(); + self.key = Some(new_key_raw); Ok(()) } @@ -930,138 +919,54 @@ impl CipherView { } } - fn reencrypt_attachment_keys( - &mut self, - ctx: &mut KeyStoreContext, - old_key: SymmetricKeySlotId, - new_key: SymmetricKeySlotId, - ) -> Result<(), CryptoError> { - if let Some(attachments) = &mut self.attachments { - AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?; - } - Ok(()) - } - #[allow(missing_docs)] - pub fn decrypt_fido2_credentials( - &self, - ctx: &mut KeyStoreContext, - ) -> Result, CryptoError> { - let key = self.key_identifier(); - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - - Ok(self - .login + pub fn get_fido2_credentials(&self) -> Vec { + self.login .as_ref() .and_then(|l| l.fido2_credentials.as_ref()) - .map(|f| f.decrypt(ctx, ciphers_key)) - .transpose()? - .unwrap_or_default()) - } - - fn reencrypt_fido2_credentials( - &mut self, - ctx: &mut KeyStoreContext, - old_key: SymmetricKeySlotId, - new_key: SymmetricKeySlotId, - ) -> Result<(), CryptoError> { - if let Some(login) = self.login.as_mut() { - login.reencrypt_fido2_credentials(ctx, old_key, new_key)?; - } - Ok(()) + .cloned() + .unwrap_or_default() } - /// Moves the cipher to an organization by re-encrypting the cipher keys with the organization - /// key and assigning the organization ID to the cipher. + /// Moves the cipher to an organization by assigning the organization ID to the cipher. /// /// # Arguments - /// * `ctx` - The key store context where the cipher keys will be re-encrypted /// * `organization_id` - The ID of the organization to move the cipher to pub fn move_to_organization( &mut self, - ctx: &mut KeyStoreContext, organization_id: OrganizationId, ) -> Result<(), CipherError> { - let new_key = SymmetricKeySlotId::Organization(organization_id); - - self.reencrypt_cipher_keys(ctx, new_key)?; + self.validate_attachment_keys()?; self.organization_id = Some(organization_id); Ok(()) } - /// Re-encrypt the cipher key(s) using a new wrapping key. + /// Validates that all attachments have keys, returning an error if any are missing. /// - /// If the cipher has a cipher key, it will be re-encrypted with the new wrapping key. - /// Otherwise, the cipher will re-encrypt all attachment keys and FIDO2 credential keys - pub fn reencrypt_cipher_keys( - &mut self, - ctx: &mut KeyStoreContext, - new_wrapping_key: SymmetricKeySlotId, - ) -> Result<(), CipherError> { - let old_key = self.key_identifier(); - - // If any attachment is missing a key we can't reencrypt the attachment keys + /// Key re-wrapping under the new wrapping key happens at encrypt time inside + /// `CompositeEncryptable` / `encrypt_legacy_field_encryption`. + pub fn validate_attachment_keys(&mut self) -> Result<(), CipherError> { if self.attachments.iter().flatten().any(|a| a.key.is_none()) { return Err(CipherError::AttachmentsWithoutKeys); } - - // If the cipher has a key, reencrypt it with the new wrapping key - if self.key.is_some() { - // Decrypt the current cipher key using the existing wrapping key - let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?; - - // Wrap the cipher key with the new wrapping key - self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?); - } else { - // The cipher does not have a key, we must reencrypt all attachment keys and FIDO2 - // credentials individually - self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?; - self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?; - } - Ok(()) } #[allow(missing_docs)] pub fn set_new_fido2_credentials( &mut self, - ctx: &mut KeyStoreContext, creds: Vec, ) -> Result<(), CipherError> { - let key = self.key_identifier(); - - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - require!(self.login.as_mut()).fido2_credentials = - Some(creds.encrypt_composite(ctx, ciphers_key)?); - + Some(creds.into_iter().map(Fido2CredentialView::from).collect()); Ok(()) } #[allow(missing_docs)] - pub fn get_fido2_credentials( - &self, - ctx: &mut KeyStoreContext, - ) -> Result, CipherError> { - let key = self.key_identifier(); - - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - - let login = require!(self.login.as_ref()); - let creds = require!(login.fido2_credentials.as_ref()); - let res = creds.decrypt(ctx, ciphers_key)?; - Ok(res) - } - - #[allow(missing_docs)] - pub fn decrypt_fido2_private_key( - &self, - ctx: &mut KeyStoreContext, - ) -> Result { - let fido2_credential = self.get_fido2_credentials(ctx)?; - - Ok(fido2_credential[0].key_value.clone()) + pub fn decrypt_fido2_private_key(&self) -> Result { + let creds = self.get_fido2_credentials(); + Ok(require!(creds.first()).key_value.clone()) } pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) { @@ -1090,18 +995,7 @@ impl CipherView { /// Used by the blob decryption path: blob ciphers are fully unsealed to a /// `CipherView` by [`decrypt_blob_cipher`], and this method then derives the /// list-view shape without re-decrypting any sensitive fields. - /// - /// The login `totp` is re-encrypted under the cipher key because - /// [`LoginListView::totp`] stores an [`EncString`] (decrypted lazily via - /// [`CipherListView::get_totp_key`]); avoids a breaking change by keeping the - /// existing API contract - pub(crate) fn to_list_view( - &self, - ctx: &mut KeyStoreContext, - key: SymmetricKeySlotId, - ) -> Result { - let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?; - + pub(crate) fn to_list_view(&self) -> Result { let all_attachments = || { self.attachments .iter() @@ -1117,7 +1011,7 @@ impl CipherView { .login .as_ref() .ok_or(CryptoError::MissingField("login"))?; - CipherListViewType::Login(login.to_list_view(ctx, ciphers_key)?) + CipherListViewType::Login(login.to_list_view()) } CipherType::SecureNote => CipherListViewType::SecureNote, CipherType::Card => { @@ -1150,7 +1044,6 @@ impl CipherView { organization_id: self.organization_id, folder_id: self.folder_id, collection_ids: self.collection_ids.clone(), - key: self.key.clone(), name: self.name.clone(), subtitle: self.subtitle(), r#type: list_type, @@ -1414,9 +1307,6 @@ pub(crate) fn lenient_decrypt_cipher_list_view( organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids.clone(), - // ⚠️ pass-through of the wrapped, key-bound cipher key — see the contract-violation note in - // `lenient_decrypt_cipher_view`. - key: cipher.key.clone(), name: cipher .name .as_ref() @@ -1529,7 +1419,7 @@ impl Decryptable for Cipher { key: SymmetricKeySlotId, ) -> Result { match try_parse_blob(self) { - Some(sealed) => decrypt_blob_cipher(self, &sealed, ctx, key)?.to_list_view(ctx, key), + Some(sealed) => decrypt_blob_cipher(self, &sealed, ctx, key)?.to_list_view(), None => lenient_decrypt_cipher_list_view(self, ctx, key), } } @@ -1607,9 +1497,12 @@ fn strict_decrypt_cipher_view( organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids.clone(), - // ⚠️ pass-through of the wrapped, key-bound cipher key — see the contract-violation note in - // `lenient_decrypt_cipher_view`. - key: cipher.key.clone(), + key: if cipher.key.is_some() { + #[allow(deprecated)] + Some(ctx.dangerous_get_symmetric_key(ciphers_key)?.clone()) + } else { + None + }, name: cipher .name .as_ref() @@ -1681,7 +1574,7 @@ impl Decryptable for StrictDecry key: SymmetricKeySlotId, ) -> Result { match try_parse_blob(&self.0) { - Some(sealed) => decrypt_blob_cipher(&self.0, &sealed, ctx, key)?.to_list_view(ctx, key), + Some(sealed) => decrypt_blob_cipher(&self.0, &sealed, ctx, key)?.to_list_view(), None => strict_decrypt_cipher_list_view(&self.0, ctx, key), } } @@ -1701,9 +1594,6 @@ fn strict_decrypt_cipher_list_view( organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids.clone(), - // ⚠️ pass-through of the wrapped, key-bound cipher key — see the contract-violation note in - // `lenient_decrypt_cipher_view`. - key: cipher.key.clone(), name: cipher .name .as_ref() @@ -2244,6 +2134,24 @@ mod tests { } } + fn generate_fido2_view() -> Fido2CredentialView { + Fido2CredentialView { + credential_id: "123".to_string(), + key_type: "public-key".to_string(), + key_algorithm: "ECDSA".to_string(), + key_curve: "P-256".to_string(), + key_value: "123".to_string(), + rp_id: "123".to_string(), + user_handle: None, + user_name: None, + counter: "123".to_string(), + rp_name: None, + user_display_name: None, + discoverable: "true".to_string(), + creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(), + } + } + #[test] fn test_decrypt_cipher_list_view() { let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap(); @@ -2303,7 +2211,6 @@ mod tests { organization_id: cipher.organization_id, folder_id: cipher.folder_id, collection_ids: cipher.collection_ids, - key: cipher.key, name: "My test login".to_string(), subtitle: "test_username".to_string(), r#type: CipherListViewType::Login(LoginListView { @@ -2317,7 +2224,9 @@ mod tests { }]), has_fido2: true, username: Some("test_username".to_string()), - totp: cipher.login.as_ref().unwrap().totp.clone(), + totp: cipher.login.as_ref().unwrap().totp.as_ref().map(|t| t + .decrypt(&mut key_store.context(), SymmetricKeySlotId::User) + .unwrap()), uris: None, }), favorite: cipher.favorite, @@ -2481,7 +2390,7 @@ mod tests { let mut cipher = generate_cipher(); cipher - .generate_cipher_key(&mut key_store.context(), cipher.key_identifier()) + .generate_cipher_key(&mut key_store.context()) .unwrap(); // Check that the cipher gets encrypted correctly when it's assigned it's own key @@ -2500,23 +2409,16 @@ mod tests { { let mut ctx = key_store.context(); let cipher_key = ctx.generate_symmetric_key(); - - original_cipher.key = Some( - ctx.wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key) - .unwrap(), - ); + #[allow(deprecated)] + let raw = ctx.dangerous_get_symmetric_key(cipher_key).unwrap().clone(); + original_cipher.key = Some(raw); } original_cipher - .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier()) + .generate_cipher_key(&mut key_store.context()) .unwrap(); - // Make sure that the cipher key is decryptable - let wrapped_key = original_cipher.key.unwrap(); - let mut ctx = key_store.context(); - let _ = ctx - .unwrap_symmetric_key(SymmetricKeySlotId::User, &wrapped_key) - .unwrap(); + assert!(original_cipher.key.is_some()); } #[test] @@ -2532,13 +2434,11 @@ mod tests { size_name: None, file_name: Some("Attachment test name".into()), key: None, - #[cfg(feature = "wasm")] - decrypted_key: None, }; cipher.attachments = Some(vec![attachment]); cipher - .generate_cipher_key(&mut key_store.context(), cipher.key_identifier()) + .generate_cipher_key(&mut key_store.context()) .unwrap(); assert!(cipher.attachments.unwrap()[0].key.is_none()); } @@ -2546,40 +2446,23 @@ mod tests { #[test] fn test_reencrypt_cipher_key() { let old_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); - let new_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_key(old_key); let mut ctx = key_store.context_mut(); let mut cipher = generate_cipher(); - cipher - .generate_cipher_key(&mut ctx, cipher.key_identifier()) - .unwrap(); - - // Re-encrypt the cipher key with a new wrapping key - let new_key_id = ctx.add_local_symmetric_key(new_key); + cipher.generate_cipher_key(&mut ctx).unwrap(); - cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap(); + cipher.validate_attachment_keys().unwrap(); - // Check that the cipher key can be unwrapped with the new key assert!(cipher.key.is_some()); - assert!( - ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap()) - .is_ok() - ); } #[test] fn test_reencrypt_cipher_key_ignores_missing_key() { - let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); - let key_store = create_test_crypto_with_user_key(key); - let mut ctx = key_store.context_mut(); let mut cipher = generate_cipher(); - // The cipher does not have a key, so re-encryption should not add one - let new_cipher_key = ctx.generate_symmetric_key(); - cipher - .reencrypt_cipher_keys(&mut ctx, new_cipher_key) - .unwrap(); + // The cipher does not have a key, so validation should pass without error + cipher.validate_attachment_keys().unwrap(); // Check that the cipher key is still None assert!(cipher.key.is_none()); @@ -2595,12 +2478,10 @@ mod tests { // Create a cipher with a user key let mut cipher = generate_cipher(); cipher - .generate_cipher_key(&mut key_store.context(), cipher.key_identifier()) + .generate_cipher_key(&mut key_store.context()) .unwrap(); - cipher - .move_to_organization(&mut key_store.context(), org) - .unwrap(); + cipher.move_to_organization(org).unwrap(); assert_eq!(cipher.organization_id, Some(org)); // Check that the cipher can be encrypted/decrypted with the new org key @@ -2620,14 +2501,16 @@ mod tests { // Create a cipher with a user key let mut cipher = generate_cipher(); cipher - .generate_cipher_key(&mut key_store.context(), cipher.key_identifier()) + .generate_cipher_key(&mut key_store.context()) .unwrap(); cipher.organization_id = Some(org); - // Check that the cipher can not be encrypted, as the - // cipher key is tied to the user key and not the org key - assert!(key_store.encrypt(EncryptMode::Legacy(cipher)).is_err()); + // The cipher key is now stored as raw bytes (not wrapped under the user key), so it can + // be re-wrapped under any available key at encrypt time — this now succeeds. + let cipher_enc = key_store.encrypt(EncryptMode::Legacy(cipher)).unwrap(); + let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap(); + assert_eq!(cipher_dec.name, "My test login"); } #[test] @@ -2635,7 +2518,7 @@ mod tests { let org = OrganizationId::new_v4(); let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); - let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key); + let _key_store = create_test_crypto_with_user_and_org_key(key, org, org_key); let mut cipher = generate_cipher(); let attachment = AttachmentView { @@ -2645,17 +2528,11 @@ mod tests { size_name: None, file_name: Some("Attachment test name".into()), key: None, - #[cfg(feature = "wasm")] - decrypted_key: None, }; cipher.attachments = Some(vec![attachment]); // Neither cipher nor attachment have keys, so the cipher can't be moved - assert!( - cipher - .move_to_organization(&mut key_store.context(), org) - .is_err() - ); + assert!(cipher.move_to_organization(org).is_err()); } #[test] @@ -2664,22 +2541,15 @@ mod tests { let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key); - let org_key = SymmetricKeySlotId::Organization(org); - // Attachment has a key that is encrypted with the user key, as the cipher has no key itself - let (attachment_key_enc, attachment_key_val) = { + // Attachment has a key stored as raw base64 on the view; the cipher itself has no key + let attachment_key_val = { let mut ctx = key_store.context(); let attachment_key = ctx.generate_symmetric_key(); - let attachment_key_enc = ctx - .wrap_symmetric_key(SymmetricKeySlotId::User, attachment_key) - .unwrap(); #[allow(deprecated)] - let attachment_key_val = ctx - .dangerous_get_symmetric_key(attachment_key) + ctx.dangerous_get_symmetric_key(attachment_key) .unwrap() - .clone(); - - (attachment_key_enc, attachment_key_val) + .clone() }; let mut cipher = generate_cipher(); @@ -2689,43 +2559,30 @@ mod tests { size: None, size_name: None, file_name: Some("Attachment test name".into()), - key: Some(attachment_key_enc), - #[cfg(feature = "wasm")] - decrypted_key: None, + key: Some(attachment_key_val.clone()), }; cipher.attachments = Some(vec![attachment]); - let cred = generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User); + let cred = generate_fido2_view(); cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]); - cipher - .move_to_organization(&mut key_store.context(), org) - .unwrap(); + cipher.move_to_organization(org).unwrap(); assert!(cipher.key.is_none()); - // Check that the attachment key has been re-encrypted with the org key, - // and the value matches with the original attachment key - let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap(); - let mut ctx = key_store.context(); - let new_attachment_key_id = ctx - .unwrap_symmetric_key(org_key, &new_attachment_key) - .unwrap(); - #[allow(deprecated)] - let new_attachment_key_dec = ctx - .dangerous_get_symmetric_key(new_attachment_key_id) - .unwrap(); - - assert_eq!(*new_attachment_key_dec, attachment_key_val); + // Attachment raw key bytes are preserved (re-wrapping happens at encrypt time) + assert_eq!( + cipher.attachments.unwrap()[0].key.clone().unwrap(), + attachment_key_val + ); - let cred2: Fido2CredentialFullView = cipher + let cred2 = cipher .login .unwrap() .fido2_credentials .unwrap() .first() .unwrap() - .decrypt(&mut key_store.context(), org_key) - .unwrap(); + .clone(); assert_eq!(cred2.credential_id, "123"); } @@ -2736,21 +2593,23 @@ mod tests { let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key); - let org_key = SymmetricKeySlotId::Organization(org); let mut ctx = key_store.context(); let cipher_key = ctx.generate_symmetric_key(); - let cipher_key_enc = ctx - .wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key) - .unwrap(); + #[allow(deprecated)] + let cipher_key_raw = ctx.dangerous_get_symmetric_key(cipher_key).unwrap().clone(); // Attachment has a key that is encrypted with the cipher key let attachment_key = ctx.generate_symmetric_key(); - let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap(); + #[allow(deprecated)] + let attachment_key_raw = ctx + .dangerous_get_symmetric_key(attachment_key) + .unwrap() + .clone(); let mut cipher = generate_cipher(); - cipher.key = Some(cipher_key_enc); + cipher.key = Some(cipher_key_raw.clone()); let attachment = AttachmentView { id: None, @@ -2758,40 +2617,25 @@ mod tests { size: None, size_name: None, file_name: Some("Attachment test name".into()), - key: Some(attachment_key_enc.clone()), - #[cfg(feature = "wasm")] - decrypted_key: None, + key: Some(attachment_key_raw.clone()), }; cipher.attachments = Some(vec![attachment]); - let cred = generate_fido2(&mut ctx, cipher_key); + let cred = generate_fido2_view(); cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]); - cipher.move_to_organization(&mut ctx, org).unwrap(); - - // Check that the cipher key has been re-encrypted with the org key, - let wrapped_new_cipher_key = cipher.key.clone().unwrap(); - let new_cipher_key_dec = ctx - .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key) - .unwrap(); - #[allow(deprecated)] - let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap(); - #[allow(deprecated)] - let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap(); + cipher.move_to_organization(org).unwrap(); - assert_eq!(new_cipher_key_dec, cipher_key_val); + // Raw cipher key bytes are unchanged (re-wrapping happens at encrypt time) + assert_eq!(cipher.key.clone().unwrap(), cipher_key_raw); - // Check that the attachment key hasn't changed + // Attachment raw key bytes are unchanged (re-wrapping happens at encrypt time) assert_eq!( - cipher.attachments.unwrap()[0] - .key - .as_ref() - .unwrap() - .to_string(), - attachment_key_enc.to_string() + cipher.attachments.unwrap()[0].key.as_ref().unwrap(), + &attachment_key_raw ); - let cred2: Fido2Credential = cipher + let cred2 = cipher .login .unwrap() .fido2_credentials @@ -2800,10 +2644,7 @@ mod tests { .unwrap() .clone(); - assert_eq!( - cred2.credential_id.to_string(), - cred.credential_id.to_string() - ); + assert_eq!(cred2.credential_id, cred.credential_id); } #[test] @@ -2814,19 +2655,14 @@ mod tests { let mut ctx = key_store.context(); let mut cipher_view = generate_cipher(); - cipher_view - .generate_cipher_key(&mut ctx, cipher_view.key_identifier()) - .unwrap(); - - let key_id = cipher_view.key_identifier(); - let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap(); + cipher_view.generate_cipher_key(&mut ctx).unwrap(); - let fido2_credential = generate_fido2(&mut ctx, ciphers_key); + let fido2_credential = generate_fido2_view(); cipher_view.login.as_mut().unwrap().fido2_credentials = Some(vec![fido2_credential.clone()]); - let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap(); + let decrypted_key_value = cipher_view.decrypt_fido2_private_key().unwrap(); assert_eq!(decrypted_key_value, "123"); } @@ -4090,7 +3926,7 @@ mod tests { CipherListViewType::Login(login) => assert!(login.totp.is_some()), other => panic!("expected Login, got {other:?}"), } - let totp = list_view.get_totp_key(&mut key_store.context()).unwrap(); + let totp = list_view.get_totp_key().unwrap(); assert_eq!(totp.as_deref(), Some("otpauth://totp/test?secret=SECRET")); } diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/admin/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/admin/create.rs index f1402f0a1c..f75d9ab771 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/admin/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/admin/create.rs @@ -2,7 +2,7 @@ use bitwarden_api_api::models::{CipherCreateRequestModel, CipherRequestModel}; use bitwarden_core::{ ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeySlotIds, }; -use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore}; +use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use thiserror::Error; #[cfg(feature = "wasm")] @@ -109,8 +109,7 @@ impl CipherAdminClient { // TODO: Once this flag is removed, the key generation logic should // be moved directly into the CompositeEncryptable implementation. if self.client.flags().get().await.enable_cipher_key_encryption { - let key = view.key_identifier(); - view.generate_cipher_key(&mut key_store.context(), key)?; + view.generate_cipher_key(&mut key_store.context())?; } let use_blob = should_use_blob_encryption(&key_store.context(), view.organization_id); diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/admin/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/admin/edit.rs index 561e3a7324..1cf6fb453e 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/admin/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/admin/edit.rs @@ -6,7 +6,7 @@ use bitwarden_collections::collection::CollectionId; use bitwarden_core::{ ApiError, MissingFieldError, NotAuthenticatedError, UserId, key_management::KeySlotIds, }; -use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore}; +use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::RepositoryError; use thiserror::Error; @@ -73,8 +73,7 @@ async fn edit_cipher( // TODO: Once this flag is removed, the key generation logic should be // moved directly into the CompositeEncryptable implementation. if view.key.is_none() && enable_cipher_key_encryption { - let key = view.key_identifier(); - view.generate_cipher_key(&mut key_store.context(), key)?; + view.generate_cipher_key(&mut key_store.context())?; } // Admin endpoints operate on organization-owned ciphers, which aren't diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs index 22b6474fbf..199cd43fd2 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -4,7 +4,7 @@ use bitwarden_core::{ ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, key_management::KeySlotIds, require, }; -use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore}; +use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -174,8 +174,7 @@ impl CiphersClient { // TODO: Once this flag is removed, the key generation logic should // be moved directly into the CompositeEncryptable implementation. if self.client.flags().get().await.enable_cipher_key_encryption { - let key = view.key_identifier(); - view.generate_cipher_key(&mut key_store.context(), key)?; + view.generate_cipher_key(&mut key_store.context())?; } let use_blob = self.should_use_blob_encryption(view.organization_id); diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs index c5790bcb15..ab3f1b6514 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -6,7 +6,7 @@ use bitwarden_core::{ ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, key_management::KeySlotIds, require, }; -use bitwarden_crypto::{CryptoError, EncString, IdentifyKey, KeyStore}; +use bitwarden_crypto::{CryptoError, KeyStore, SymmetricCryptoKey}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -66,7 +66,8 @@ pub struct CipherEditRequest { pub revision_date: DateTime, pub archived_date: Option>, pub attachments: Vec, - pub key: Option, + #[cfg_attr(feature = "wasm", tsify(type = "SymmetricKey | undefined"))] + pub key: Option, } impl TryFrom for CipherEditRequest { @@ -188,8 +189,7 @@ async fn edit_cipher + ?Sized>( // TODO: Once this flag is removed, the key generation logic should be // moved directly into the CompositeEncryptable implementation. if view.key.is_none() && enable_cipher_key_encryption { - let key = view.key_identifier(); - view.generate_cipher_key(&mut key_store.context(), key)?; + view.generate_cipher_key(&mut key_store.context())?; } let mode = if use_blob { diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs index 11385171ec..0d6c6eb4b8 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs @@ -7,7 +7,7 @@ use bitwarden_core::{ }; #[cfg(feature = "wasm")] use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey}; -use bitwarden_crypto::{IdentifyKey, KeyStore, KeyStoreContext}; +use bitwarden_crypto::{KeyStore, KeyStoreContext}; #[cfg(feature = "wasm")] use bitwarden_encoding::B64; use bitwarden_state::repository::{Repository, RepositoryError}; @@ -98,8 +98,7 @@ impl CiphersClient { // be moved directly into the KeyEncryptable implementation if cipher_view.key.is_none() && self.client.flags().get().await.enable_cipher_key_encryption { - let key = cipher_view.key_identifier(); - cipher_view.generate_cipher_key(&mut key_store.context(), key)?; + cipher_view.generate_cipher_key(&mut key_store.context())?; } let mode = if self.should_use_blob_encryption(cipher_view.organization_id) { @@ -147,9 +146,9 @@ impl CiphersClient { let new_key_id = ctx.add_local_symmetric_key(new_key); if cipher_view.key.is_none() && enable_cipher_key_encryption { - cipher_view.generate_cipher_key(&mut ctx, new_key_id)?; + cipher_view.generate_cipher_key(&mut ctx)?; } else { - cipher_view.reencrypt_cipher_keys(&mut ctx, new_key_id)?; + cipher_view.validate_attachment_keys()?; } // Rotation installs the new key under a `Local` slot id (`new_key_id`), not the view's @@ -191,8 +190,7 @@ impl CiphersClient { .into_iter() .map(|mut cv| { if cv.key.is_none() && enable_cipher_key { - let key = cv.key_identifier(); - cv.generate_cipher_key(&mut ctx, key)?; + cv.generate_cipher_key(&mut ctx)?; } let mode = if self.should_use_blob_encryption(cv.organization_id) { EncryptMode::Blob(cv) @@ -293,9 +291,7 @@ impl CiphersClient { &self, cipher_view: CipherView, ) -> Result, DecryptError> { - let key_store = self.client.internal.get_key_store(); - let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?; - Ok(credentials) + Ok(cipher_view.get_fido2_credentials()) } /// Temporary method used to re-encrypt FIDO2 credentials for a cipher view. @@ -309,9 +305,7 @@ impl CiphersClient { mut cipher_view: CipherView, fido2_credentials: Vec, ) -> Result { - let key_store = self.client.internal.get_key_store(); - - cipher_view.set_new_fido2_credentials(&mut key_store.context(), fido2_credentials)?; + cipher_view.set_new_fido2_credentials(fido2_credentials)?; Ok(cipher_view) } @@ -322,8 +316,7 @@ impl CiphersClient { mut cipher_view: CipherView, organization_id: OrganizationId, ) -> Result { - let key_store = self.client.internal.get_key_store(); - cipher_view.move_to_organization(&mut key_store.context(), organization_id)?; + cipher_view.move_to_organization(organization_id)?; Ok(cipher_view) } @@ -333,8 +326,7 @@ impl CiphersClient { &self, cipher_view: CipherView, ) -> Result { - let key_store = self.client.internal.get_key_store(); - let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?; + let decrypted_key = cipher_view.decrypt_fido2_private_key()?; Ok(decrypted_key) } @@ -673,12 +665,6 @@ mod tests { let attachment_view = attachments.first().unwrap().clone(); assert!(attachment_view.key.is_some()); - // Ensure attachment key is updated since it's now protected by the cipher key - assert_ne!( - attachment.clone().key.unwrap().to_string(), - attachment_view.clone().key.unwrap().to_string() - ); - assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt")); let buf = vec![ @@ -718,11 +704,9 @@ mod tests { .unwrap() .clone(); - // Ensure attachment key is still the same since it's protected by the cipher key - assert_eq!( - attachment.clone().key.as_ref().unwrap().to_string(), - attachment_view.key.as_ref().unwrap().to_string() - ); + // The attachment key (raw bytes) is unchanged; it's re-wrapped under the cipher key at + // encrypt time with a fresh IV, so EncString ≠ raw base64 — verify via round-trip instead. + assert!(attachment.key.is_some()); let content = client .vault() diff --git a/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs b/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs index 88777b60ff..a10787f6df 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/share_cipher.rs @@ -448,8 +448,6 @@ mod tests { size_name: Some("2 KB".to_string()), file_name: Some("test2.txt".to_string()), key: None, // No key! - #[cfg(feature = "wasm")] - decrypted_key: None, }]); let organization_id: OrganizationId = TEST_ORG_ID.parse().unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 3ca9590e39..0593ca29fe 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -119,7 +119,7 @@ pub struct Fido2CredentialListView { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -128,9 +128,10 @@ pub struct Fido2CredentialView { pub key_type: String, pub key_algorithm: String, pub key_curve: String, - // This value doesn't need to be returned to the client - // so we keep it encrypted until we need it - pub key_value: EncString, + /// The raw private key material for this passkey credential. + /// Callers that receive `Fido2CredentialView` over a binding boundary should + /// treat this field with the same care as any private key material. + pub key_value: String, pub rp_id: String, pub user_handle: Option, pub user_name: Option, @@ -257,27 +258,56 @@ impl Decryptable for Fi } } -impl Decryptable for Fido2CredentialView { - fn decrypt( - &self, - ctx: &mut KeyStoreContext, - key: SymmetricKeySlotId, - ) -> Result { - Ok(Fido2CredentialFullView { - credential_id: self.credential_id.clone(), - key_type: self.key_type.clone(), - key_algorithm: self.key_algorithm.clone(), - key_curve: self.key_curve.clone(), - key_value: self.key_value.decrypt(ctx, key)?, - rp_id: self.rp_id.clone(), - user_handle: self.user_handle.clone(), - user_name: self.user_name.clone(), - counter: self.counter.clone(), - rp_name: self.rp_name.clone(), - user_display_name: self.user_display_name.clone(), - discoverable: self.discoverable.clone(), - creation_date: self.creation_date, - }) +impl From for Fido2CredentialFullView { + fn from(v: Fido2CredentialView) -> Self { + Fido2CredentialFullView { + credential_id: v.credential_id, + key_type: v.key_type, + key_algorithm: v.key_algorithm, + key_curve: v.key_curve, + key_value: v.key_value, + rp_id: v.rp_id, + user_handle: v.user_handle, + user_name: v.user_name, + counter: v.counter, + rp_name: v.rp_name, + user_display_name: v.user_display_name, + discoverable: v.discoverable, + creation_date: v.creation_date, + } + } +} + +impl From for Fido2CredentialView { + fn from(v: Fido2CredentialFullView) -> Self { + Fido2CredentialView { + credential_id: v.credential_id, + key_type: v.key_type, + key_algorithm: v.key_algorithm, + key_curve: v.key_curve, + key_value: v.key_value, + rp_id: v.rp_id, + user_handle: v.user_handle, + user_name: v.user_name, + counter: v.counter, + rp_name: v.rp_name, + user_display_name: v.user_display_name, + discoverable: v.discoverable, + creation_date: v.creation_date, + } + } +} + +impl From<&Fido2CredentialView> for Fido2CredentialListView { + fn from(v: &Fido2CredentialView) -> Self { + Fido2CredentialListView { + credential_id: v.credential_id.clone(), + rp_id: v.rp_id.clone(), + user_handle: v.user_handle.clone(), + user_name: v.user_name.clone(), + user_display_name: v.user_display_name.clone(), + counter: v.counter.clone(), + } } } @@ -312,8 +342,7 @@ pub struct LoginView { pub totp: Option, pub autofill_on_page_load: Option, - // TODO: Remove this once the SDK supports state - pub fido2_credentials: Option>, + pub fido2_credentials: Option>, } impl LoginView { @@ -326,50 +355,18 @@ impl LoginView { } } - /// Re-encrypts the fido2 credentials with a new key, replacing the old encrypted values. - pub fn reencrypt_fido2_credentials( - &mut self, - ctx: &mut KeyStoreContext, - old_key: SymmetricKeySlotId, - new_key: SymmetricKeySlotId, - ) -> Result<(), CryptoError> { - if let Some(creds) = &mut self.fido2_credentials { - let decrypted_creds: Vec = creds.decrypt(ctx, old_key)?; - *creds = decrypted_creds.encrypt_composite(ctx, new_key)?; - } - Ok(()) - } - /// Projects this [`LoginView`] into a [`LoginListView`]. - /// - /// `totp` is re-encrypted under `cipher_key` because [`LoginListView`] stores the - /// TOTP as an [`EncString`] that [`crate::CipherListView::get_totp_key`] decrypts - /// on demand. `fido2_credentials` are still encrypted on [`LoginView`], so they - /// decrypt directly to [`Fido2CredentialListView`] via the existing impl. - pub(crate) fn to_list_view( - &self, - ctx: &mut KeyStoreContext, - cipher_key: SymmetricKeySlotId, - ) -> Result { - let totp = self - .totp - .as_ref() - .map(|t| t.encrypt(ctx, cipher_key)) - .transpose()?; - - let fido2_credentials = self - .fido2_credentials - .as_ref() - .map(|creds| creds.decrypt(ctx, cipher_key)) - .transpose()?; - - Ok(LoginListView { + pub(crate) fn to_list_view(&self) -> LoginListView { + LoginListView { has_fido2: self.fido2_credentials.is_some(), - fido2_credentials, + fido2_credentials: self + .fido2_credentials + .as_ref() + .map(|creds| creds.iter().map(Fido2CredentialListView::from).collect()), username: self.username.clone(), - totp, + totp: self.totp.clone(), uris: self.uris.clone(), - }) + } } /// Compares this LoginView to the original, and returns any new password history items. @@ -411,8 +408,7 @@ pub struct LoginListView { pub fido2_credentials: Option>, pub has_fido2: bool, pub username: Option, - /// The TOTP key is not decrypted. Useable as is with [`crate::generate_totp_cipher_view`]. - pub totp: Option, + pub totp: Option, pub uris: Option>, } @@ -430,13 +426,6 @@ impl CompositeEncryptable for LoginUri } } -// ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::CompositeEncryptable`: `LoginView` is a decrypted -// DTO, yet it stores `fido2_credentials` as `Vec` (already-encrypted values) -// rather than a decrypted view type. Encryption therefore copies the ciphertext through unchanged -// (`fido2_credentials: self.fido2_credentials.clone()` below) instead of re-encrypting it under -// `key`. As a result decrypt(K) -> encrypt(K1) -> decrypt(K1) does NOT round-trip the credentials: -// they remain wrapped under the original key K. Callers that rewrap the cipher key must invoke -// `LoginView::reencrypt_fido2_credentials` explicitly to keep the credentials decryptable. impl CompositeEncryptable for LoginView { fn encrypt_composite( &self, @@ -454,9 +443,17 @@ impl CompositeEncryptable for LoginView { .filter(|s| !s.is_empty()) .encrypt(ctx, key)?, autofill_on_page_load: self.autofill_on_page_load, - // ⚠️ pass-through of already-encrypted credentials — see the contract-violation note - // above. - fido2_credentials: self.fido2_credentials.clone(), + fido2_credentials: self + .fido2_credentials + .as_ref() + .map(|creds| { + creds + .iter() + .map(|v| Fido2CredentialFullView::from(v.clone())) + .collect::>() + .encrypt_composite(ctx, key) + }) + .transpose()?, }) } } @@ -488,12 +485,11 @@ impl Decryptable for Login { uris: self.uris.decrypt(ctx, key).ok().flatten(), totp: self.totp.decrypt(ctx, key).ok().flatten(), autofill_on_page_load: self.autofill_on_page_load, - // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting `LoginView` - // is a decrypted DTO, but `fido2_credentials` are copied through still - // encrypted (`self.fido2_credentials.clone()`) rather than decrypted, - // because `LoginView` stores them as the encrypted `Vec`. - // Consumers must decrypt each credential separately. - fido2_credentials: self.fido2_credentials.clone(), + fido2_credentials: self + .fido2_credentials + .as_ref() + .map(|c| c.decrypt(ctx, key)) + .transpose()?, }) } } @@ -511,7 +507,7 @@ impl Decryptable for Login { .and_then(|fido2_credentials| fido2_credentials.decrypt(ctx, key).ok()), has_fido2: self.fido2_credentials.is_some(), username: self.username.decrypt(ctx, key).ok().flatten(), - totp: self.totp.clone(), + totp: self.totp.decrypt(ctx, key).ok().flatten(), uris: self.uris.decrypt(ctx, key).ok().flatten(), }) } @@ -530,12 +526,12 @@ impl Decryptable for StrictDecrypt<&L uris: self.0.uris.decrypt(ctx, key)?, totp: self.0.totp.decrypt(ctx, key)?, autofill_on_page_load: self.0.autofill_on_page_load, - // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting `LoginView` - // is a decrypted DTO, but `fido2_credentials` are copied through still - // encrypted (`self.0.fido2_credentials.clone()`) rather than decrypted, - // because `LoginView` stores them as the encrypted `Vec`. - // Consumers must decrypt each credential separately. - fido2_credentials: self.0.fido2_credentials.clone(), + fido2_credentials: self + .0 + .fido2_credentials + .as_ref() + .map(|c| c.decrypt(ctx, key)) + .transpose()?, }) } } @@ -555,7 +551,7 @@ impl Decryptable for StrictDecryp .transpose()?, has_fido2: self.0.fido2_credentials.is_some(), username: self.0.username.decrypt(ctx, key)?, - totp: self.0.totp.clone(), + totp: self.0.totp.decrypt(ctx, key)?, uris: self.0.uris.decrypt(ctx, key)?, }) } @@ -572,7 +568,7 @@ impl Decryptable for Fido2C key_type: self.key_type.decrypt(ctx, key)?, key_algorithm: self.key_algorithm.decrypt(ctx, key)?, key_curve: self.key_curve.decrypt(ctx, key)?, - key_value: self.key_value.clone(), + key_value: self.key_value.decrypt(ctx, key)?, rp_id: self.rp_id.decrypt(ctx, key)?, user_handle: self.user_handle.decrypt(ctx, key)?, user_name: self.user_name.decrypt(ctx, key)?, diff --git a/crates/bitwarden-vault/src/totp.rs b/crates/bitwarden-vault/src/totp.rs index a5c326d11e..5a336f9f0c 100644 --- a/crates/bitwarden-vault/src/totp.rs +++ b/crates/bitwarden-vault/src/totp.rs @@ -4,8 +4,7 @@ use std::{ str::FromStr, }; -use bitwarden_core::key_management::KeySlotIds; -use bitwarden_crypto::{CryptoError, KeyStoreContext}; +use bitwarden_crypto::CryptoError; use bitwarden_error::bitwarden_error; use chrono::{DateTime, Utc}; use data_encoding::BASE32_NOPAD; @@ -86,12 +85,11 @@ pub fn generate_totp(key: String, time: Option>) -> Result, view: CipherListView, time: Option>, ) -> Result { let key = view - .get_totp_key(ctx)? + .get_totp_key()? .filter(|s| !s.is_empty()) .ok_or(TotpError::MissingSecret)?; @@ -373,8 +371,6 @@ fn decode_b32(s: &str) -> Vec { #[cfg(test)] mod tests { - use bitwarden_core::key_management::create_test_crypto_with_user_key; - use bitwarden_crypto::SymmetricCryptoKey; use chrono::Utc; use super::*; @@ -735,14 +731,13 @@ mod tests { organization_id: None, folder_id: None, collection_ids: vec![], - key: None, name: "My test login".to_string(), subtitle: "test_username".to_string(), - r#type: CipherListViewType::Login(LoginListView{ + r#type: CipherListViewType::Login(LoginListView { fido2_credentials: None, has_fido2: true, username: None, - totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()), + totp: Some("DKWOW4PCP3MYFWLN53BLYAMYQEQJU4MJ".to_string()), uris: None, }), favorite: false, @@ -767,15 +762,11 @@ mod tests { attachment_names: None, }; - let key = SymmetricCryptoKey::try_from("w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string()).unwrap(); - let key_store = create_test_crypto_with_user_key(key); - let time = DateTime::parse_from_rfc3339("2023-01-01T00:00:00.000Z") .unwrap() .with_timezone(&Utc); - let response = - generate_totp_cipher_view(&mut key_store.context(), view, Some(time)).unwrap(); + let response = generate_totp_cipher_view(view, Some(time)).unwrap(); assert_eq!(response.code, "559388".to_string()); assert_eq!(response.period, 30); } diff --git a/crates/bitwarden-vault/src/totp_client.rs b/crates/bitwarden-vault/src/totp_client.rs index 23510fb7c6..07d78b4a5e 100644 --- a/crates/bitwarden-vault/src/totp_client.rs +++ b/crates/bitwarden-vault/src/totp_client.rs @@ -1,4 +1,3 @@ -use bitwarden_core::Client; use chrono::{DateTime, Utc}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -7,9 +6,7 @@ use crate::{CipherListView, TotpError, TotpResponse, generate_totp, generate_tot #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] -pub struct TotpClient { - pub(crate) client: Client, -} +pub struct TotpClient; #[cfg(feature = "wasm")] #[wasm_bindgen] @@ -55,8 +52,6 @@ impl TotpClient { view: CipherListView, time: Option>, ) -> Result { - let key_store = self.client.internal.get_key_store(); - - generate_totp_cipher_view(&mut key_store.context(), view, time) + generate_totp_cipher_view(view, time) } } diff --git a/crates/bitwarden-vault/src/uniffi_support.rs b/crates/bitwarden-vault/src/uniffi_support.rs index a3243031a7..24147c08fd 100644 --- a/crates/bitwarden-vault/src/uniffi_support.rs +++ b/crates/bitwarden-vault/src/uniffi_support.rs @@ -1,3 +1,4 @@ +use bitwarden_crypto::SymmetricCryptoKey; use uuid::Uuid; type DateTime = chrono::DateTime; @@ -5,3 +6,4 @@ uniffi::use_remote_type!(bitwarden_core::DateTime); type NaiveDate = chrono::NaiveDate; uniffi::use_remote_type!(bitwarden_core::NaiveDate); uniffi::use_remote_type!(bitwarden_core::Uuid); +uniffi::use_remote_type!(bitwarden_crypto::SymmetricCryptoKey); diff --git a/crates/bitwarden-vault/src/vault_client.rs b/crates/bitwarden-vault/src/vault_client.rs index 8b60a9fdbb..2b6142e183 100644 --- a/crates/bitwarden-vault/src/vault_client.rs +++ b/crates/bitwarden-vault/src/vault_client.rs @@ -46,9 +46,7 @@ impl VaultClient { /// TOTP related operations. pub fn totp(&self) -> TotpClient { - TotpClient { - client: self.client.clone(), - } + TotpClient } /// Collection related operations.