From 43a483a4a137a1410ae1786fd04ab1d11630e709 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 15 Jul 2026 16:29:02 +0100 Subject: [PATCH 1/2] Key rotation corrupts attachments, fido2 credentials --- .../src/key_rotation/data.rs | 302 ++++++++++++++---- crates/bitwarden-vault/src/cipher/cipher.rs | 114 ++++++- 2 files changed, 351 insertions(+), 65 deletions(-) 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 f0c728fa5..38d300caf 100644 --- a/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs +++ b/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs @@ -153,11 +153,10 @@ fn reencrypt_ciphers( .collect::, DataReencryptionError>>() } -/// Decrypts a legacy cipher into a view that can be re-sealed as a blob under `new_key`. +/// Decrypts a legacy cipher into a view ready to be re-sealed as a blob under `new_key`. /// -/// If the cipher has a per-item key, it is re-wrapped under the new user key first so the cipher -/// key (and the data sealed against it) stay consistent under the new key. Otherwise the cipher is -/// decrypted under the current key and blob sealing will generate a fresh per-item key. +/// The returned view carries a per-item cipher key wrapped under `new_key`, with its sub-keys +/// (attachment keys, FIDO2 credentials) wrapped under that cipher key. fn decrypt_for_blob_upgrade( cipher: &bitwarden_vault::Cipher, current_key: SymmetricKeySlotId, @@ -173,9 +172,12 @@ fn decrypt_for_blob_upgrade( .decrypt(ctx, new_key) .map_err(|_| DataReencryptionError::Decryption) } else { - cipher + let mut view: CipherView = cipher .decrypt(ctx, current_key) - .map_err(|_| DataReencryptionError::Decryption) + .map_err(|_| DataReencryptionError::Decryption)?; + view.generate_cipher_key_from(ctx, current_key, new_key) + .map_err(|_| DataReencryptionError::Encryption)?; + Ok(view) } } @@ -202,10 +204,13 @@ fn reencrypt_sends( #[cfg(test)] mod tests { - use bitwarden_core::key_management::KeySlotIds; - use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore}; + use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId}; + use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore, PrimitiveEncryptable}; use bitwarden_send::SendView; - use bitwarden_vault::{Attachment, Cipher, CipherRepromptType, CipherType, EncryptMode}; + use bitwarden_vault::{ + Attachment, Cipher, CipherRepromptType, CipherType, CipherView, EncryptMode, + Fido2CredentialFullView, + }; use chrono::Utc; use super::check_for_old_attachments; @@ -351,8 +356,8 @@ mod tests { fn assert_decrypts_to( cipher: &Cipher, - expected: &bitwarden_vault::CipherView, - key: bitwarden_core::key_management::SymmetricKeySlotId, + expected: &CipherView, + key: SymmetricKeySlotId, ctx: &mut bitwarden_crypto::KeyStoreContext, ) { use bitwarden_vault::CipherView; @@ -370,57 +375,6 @@ mod tests { ); } - #[test] - fn test_ciphers() { - let store: KeyStore = KeyStore::default(); - let mut ctx = store.context_mut(); - - let user_key_old = - ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac); - let user_key_new = - ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac); - - let cipher = make_cipher_view(); - let encrypted_cipher = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap(); - - // Rotate it - let ciphers = vec![encrypted_cipher]; - let reencrypted_ciphers = - super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx) - .unwrap(); - - // The keyless legacy cipher is upgraded to the blob format and decrypts under the new key - assert!(reencrypted_ciphers[0].is_blob_encrypted()); - assert_decrypts_to(&reencrypted_ciphers[0], &cipher, user_key_new, &mut ctx); - } - - #[test] - fn test_blob_gate_upgrades_keyed_legacy_cipher() { - let store: KeyStore = KeyStore::default(); - let mut ctx = store.context_mut(); - - let user_key_old = - ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac); - let user_key_new = - ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305); - - // 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(); - let encrypted = EncryptMode::Legacy(cipher.clone()) - .encrypt_composite(&mut ctx, user_key_old) - .unwrap(); - assert!(!encrypted.is_blob_encrypted()); - assert!(encrypted.key.is_some()); - - let reencrypted = - super::reencrypt_ciphers(&[encrypted], user_key_old, user_key_new, &mut ctx).unwrap(); - - // The keyed legacy cipher is fully upgraded to the blob format - assert!(reencrypted[0].is_blob_encrypted()); - assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx); - } - #[test] fn test_blob_gate_rewraps_existing_blob_without_re_encrypting() { let store: KeyStore = KeyStore::default(); @@ -534,4 +488,228 @@ mod tests { // The send seed must be the same assert_eq!(send.key, decrypted_send.key); } + + #[test] + fn test_rotation_keyless_plain() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, false, false, false); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyless_fido2() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, false, false, true); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + assert_fido2_decryptable(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyless_attachment() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, false, true, false); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + assert_attachment_key_decryptable(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyless_fido2_and_attachment() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, false, true, true); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + assert_fido2_decryptable(&mut ctx, &out[0], new); + assert_attachment_key_decryptable(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyed_plain() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, true, false, false); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyed_fido2() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, true, false, true); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + assert_fido2_decryptable(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyed_attachment() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, true, true, false); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + assert_attachment_key_decryptable(&mut ctx, &out[0], new); + } + + #[test] + fn test_rotation_keyed_fido2_and_attachment() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let (old, new) = make_rotation_keys(&mut ctx); + + let cipher = make_rotatable_cipher(&mut ctx, old, true, true, true); + let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap(); + + assert_upgraded_to_blob(&mut ctx, &out[0], new); + assert_fido2_decryptable(&mut ctx, &out[0], new); + assert_attachment_key_decryptable(&mut ctx, &out[0], new); + } + + /// The old and new user keys for a rotation: two distinct non-`User` slots (so the rotation + /// must honor the explicit keys rather than the cipher's `key_identifier()`), with a change of + /// algorithm mirroring production, which rotates onto an XChaCha20-Poly1305 key. + fn make_rotation_keys( + ctx: &mut bitwarden_crypto::KeyStoreContext, + ) -> (SymmetricKeySlotId, SymmetricKeySlotId) { + let old = ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac); + let new = + ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305); + (old, new) + } + + /// Builds a legacy personal cipher encrypted under `user_key`, optionally carrying a per-item + /// cipher key, an attachment, and a FIDO2 credential. + fn make_rotatable_cipher( + ctx: &mut bitwarden_crypto::KeyStoreContext, + user_key: SymmetricKeySlotId, + with_cipher_key: bool, + with_attachment: bool, + with_fido2: bool, + ) -> Cipher { + let mut view = make_cipher_view(); + if with_fido2 { + let full = Fido2CredentialFullView { + credential_id: "cred-123".to_string(), + key_type: "public-key".to_string(), + key_algorithm: "ECDSA".to_string(), + key_curve: "P-256".to_string(), + key_value: "key-value".to_string(), + rp_id: "example.com".to_string(), + user_handle: None, + user_name: None, + counter: "0".to_string(), + rp_name: None, + user_display_name: None, + discoverable: "true".to_string(), + creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(), + }; + view.login.as_mut().unwrap().fido2_credentials = + Some(vec![full.encrypt_composite(ctx, user_key).unwrap()]); + } + if with_cipher_key { + view.generate_cipher_key_from(ctx, user_key, user_key) + .unwrap(); + } + let mut cipher = EncryptMode::Legacy(view) + .encrypt_composite(ctx, user_key) + .unwrap(); + if with_attachment { + // The attachment content key and file name are wrapped under the cipher key: the + // per-item key for a keyed cipher, otherwise the user key directly. + let cipher_key = match &cipher.key { + Some(key) => ctx.unwrap_symmetric_key(user_key, key).unwrap(), + None => user_key, + }; + let content_key = ctx.generate_symmetric_key(); + cipher.attachments = Some(vec![Attachment { + id: Some("att1".to_string()), + url: None, + size: None, + size_name: None, + file_name: Some("secret.txt".encrypt(ctx, cipher_key).unwrap()), + key: Some(ctx.wrap_symmetric_key(cipher_key, content_key).unwrap()), + }]); + } + cipher + } + + /// Asserts the rotated cipher is a blob whose body decrypts under `user_key` to the baseline + /// [`make_cipher_view`] fields. + fn assert_upgraded_to_blob( + ctx: &mut bitwarden_crypto::KeyStoreContext, + rotated: &Cipher, + user_key: SymmetricKeySlotId, + ) { + assert!(rotated.is_blob_encrypted()); + assert_decrypts_to(rotated, &make_cipher_view(), user_key, ctx); + } + + /// Asserts the rotated cipher's FIDO2 credential decrypts to its original values under + /// `user_key`. + fn assert_fido2_decryptable( + ctx: &mut bitwarden_crypto::KeyStoreContext, + rotated: &Cipher, + user_key: SymmetricKeySlotId, + ) { + let dv: CipherView = rotated.decrypt(ctx, user_key).unwrap(); + let cipher_key = ctx + .unwrap_symmetric_key(user_key, dv.key.as_ref().unwrap()) + .unwrap(); + let creds: Vec = dv + .login + .as_ref() + .unwrap() + .fido2_credentials + .as_ref() + .unwrap() + .decrypt(ctx, cipher_key) + .unwrap(); + assert_eq!(creds[0].credential_id, "cred-123"); + assert_eq!(creds[0].key_value, "key-value"); + } + + /// Asserts the rotated cipher's attachment key unwraps under `user_key`. + fn assert_attachment_key_decryptable( + ctx: &mut bitwarden_crypto::KeyStoreContext, + rotated: &Cipher, + user_key: SymmetricKeySlotId, + ) { + let dv: CipherView = rotated.decrypt(ctx, user_key).unwrap(); + let cipher_key = ctx + .unwrap_symmetric_key(user_key, dv.key.as_ref().unwrap()) + .unwrap(); + let att = &dv.attachments.as_ref().unwrap()[0]; + assert_eq!(att.file_name.as_deref(), Some("secret.txt")); + let _ = ctx + .unwrap_symmetric_key(cipher_key, att.key.as_ref().unwrap()) + .expect("attachment key must unwrap under the new cipher key"); + } } diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 6274fa72a..fef0058fb 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -891,14 +891,34 @@ impl Cipher { } } impl CipherView { - #[allow(missing_docs)] + /// Generates a fresh per-item cipher key and re-wraps the cipher's attachment and FIDO2 + /// sub-keys under it. The existing sub-keys are assumed to be wrapped under + /// [`self.key_identifier()`](IdentifyKey::key_identifier). 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)?; + self.generate_cipher_key_from(ctx, self.key_identifier(), wrapping_key) + } + + /// Variant of [`generate_cipher_key`](Self::generate_cipher_key) that unwraps the existing + /// attachment and FIDO2 sub-keys using an explicitly supplied `source_key` rather than + /// deriving it from [`IdentifyKey::key_identifier`]. Use this when the sub-keys are wrapped + /// under a key other than the cipher's identifier — e.g. during key rotation, where they are + /// under the current user key rather than the [`SymmetricKeySlotId::User`] slot. + /// + /// * `source_key` - The key the current attachment/FIDO2 sub-keys are wrapped under. For a + /// keyless cipher this is the current user (or organization) key. + /// * `wrapping_key` - The key the freshly generated cipher key will be wrapped under (during + /// rotation, the new user key). + pub fn generate_cipher_key_from( + &mut self, + ctx: &mut KeyStoreContext, + source_key: SymmetricKeySlotId, + wrapping_key: SymmetricKeySlotId, + ) -> Result<(), CryptoError> { + let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, source_key, &self.key)?; let new_key = ctx.generate_symmetric_key(); @@ -2496,6 +2516,94 @@ mod tests { assert!(cipher.attachments.unwrap()[0].key.is_none()); } + #[test] + fn test_generate_cipher_key_from_rewraps_fido2_credentials() { + use crate::cipher::login::Fido2CredentialFullView; + + let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make( + SymmetricKeyAlgorithm::Aes256CbcHmac, + )); + let mut ctx = key_store.context_mut(); + + // Local slots, distinct from the cipher's `key_identifier()` (`User`). + let source_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make( + SymmetricKeyAlgorithm::Aes256CbcHmac, + )); + let new_user_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make( + SymmetricKeyAlgorithm::Aes256CbcHmac, + )); + + // Keyless cipher whose FIDO2 credential is wrapped under `source_key`. + let mut cipher = generate_cipher(); + cipher.login.as_mut().unwrap().fido2_credentials = + Some(vec![generate_fido2(&mut ctx, source_key)]); + assert!(cipher.key.is_none()); + + cipher + .generate_cipher_key_from(&mut ctx, source_key, new_user_key) + .unwrap(); + + // The FIDO2 credential decrypts under the freshly installed cipher key. + let cipher_key = Cipher::decrypt_cipher_key(&mut ctx, new_user_key, &cipher.key).unwrap(); + let creds: Vec = cipher + .login + .as_ref() + .unwrap() + .fido2_credentials + .as_ref() + .unwrap() + .decrypt(&mut ctx, cipher_key) + .unwrap(); + assert_eq!(creds[0].credential_id, "123"); + } + + #[test] + fn test_generate_cipher_key_from_rewraps_attachment_key() { + let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make( + SymmetricKeyAlgorithm::Aes256CbcHmac, + )); + let mut ctx = key_store.context_mut(); + + // Local slots, distinct from the cipher's `key_identifier()` (`User`). + let source_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make( + SymmetricKeyAlgorithm::Aes256CbcHmac, + )); + let new_user_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make( + SymmetricKeyAlgorithm::Aes256CbcHmac, + )); + + // Keyless cipher whose attachment key is wrapped under `source_key`. + let content_key = ctx.generate_symmetric_key(); + let mut cipher = generate_cipher(); + cipher.attachments = Some(vec![AttachmentView { + id: None, + url: None, + size: None, + size_name: None, + file_name: Some("secret.txt".into()), + key: Some(ctx.wrap_symmetric_key(source_key, content_key).unwrap()), + #[cfg(feature = "wasm")] + decrypted_key: None, + }]); + assert!(cipher.key.is_none()); + + cipher + .generate_cipher_key_from(&mut ctx, source_key, new_user_key) + .unwrap(); + + // The attachment key unwraps under the freshly installed cipher key. + let cipher_key = Cipher::decrypt_cipher_key(&mut ctx, new_user_key, &cipher.key).unwrap(); + let _ = ctx + .unwrap_symmetric_key( + cipher_key, + cipher.attachments.as_ref().unwrap()[0] + .key + .as_ref() + .unwrap(), + ) + .expect("attachment key must unwrap under the new cipher key"); + } + #[test] fn test_reencrypt_cipher_key() { let old_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); From 5bd728690f8cdf0d1313ba0cc42925ff7d5f583a Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 21 Jul 2026 10:12:38 +0100 Subject: [PATCH 2/2] upgrade to cipher key name change --- .../src/key_rotation/data.rs | 4 +- .../src/cipher/blob/encryption.rs | 4 +- crates/bitwarden-vault/src/cipher/cipher.rs | 57 +++++++++++-------- .../src/cipher/cipher_client/admin/create.rs | 2 +- .../src/cipher/cipher_client/admin/edit.rs | 2 +- .../src/cipher/cipher_client/create.rs | 2 +- .../src/cipher/cipher_client/edit.rs | 2 +- .../src/cipher/cipher_client/mod.rs | 6 +- 8 files changed, 43 insertions(+), 36 deletions(-) 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 38d300caf..968b2369e 100644 --- a/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs +++ b/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs @@ -175,7 +175,7 @@ fn decrypt_for_blob_upgrade( let mut view: CipherView = cipher .decrypt(ctx, current_key) .map_err(|_| DataReencryptionError::Decryption)?; - view.generate_cipher_key_from(ctx, current_key, new_key) + view.upgrade_to_cipher_key_encryption_with_external_key(ctx, current_key, new_key) .map_err(|_| DataReencryptionError::Encryption)?; Ok(view) } @@ -635,7 +635,7 @@ mod tests { Some(vec![full.encrypt_composite(ctx, user_key).unwrap()]); } if with_cipher_key { - view.generate_cipher_key_from(ctx, user_key, user_key) + view.upgrade_to_cipher_key_encryption_with_external_key(ctx, user_key, user_key) .unwrap(); } let mut cipher = EncryptMode::Legacy(view) diff --git a/crates/bitwarden-vault/src/cipher/blob/encryption.rs b/crates/bitwarden-vault/src/cipher/blob/encryption.rs index ec89b8deb..b0defddc4 100644 --- a/crates/bitwarden-vault/src/cipher/blob/encryption.rs +++ b/crates/bitwarden-vault/src/cipher/blob/encryption.rs @@ -94,7 +94,7 @@ 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.upgrade_to_cipher_key_encryption(ctx, wrapping_key)?; } let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &view.key)?; @@ -310,7 +310,7 @@ mod tests { view.secure_note = Some(SecureNoteView { r#type: SecureNoteType::Generic, }); - view.generate_cipher_key(&mut ctx, view.key_identifier()) + view.upgrade_to_cipher_key_encryption(&mut ctx, view.key_identifier()) .unwrap(); let sealed_string = seal_cipher(&view, &mut ctx, view.key_identifier()).unwrap(); diff --git a/crates/bitwarden-vault/src/cipher/cipher.rs b/crates/bitwarden-vault/src/cipher/cipher.rs index 903adc6d3..2ce338166 100644 --- a/crates/bitwarden-vault/src/cipher/cipher.rs +++ b/crates/bitwarden-vault/src/cipher/cipher.rs @@ -892,28 +892,32 @@ impl Cipher { } } impl CipherView { - /// Generates a fresh per-item cipher key and re-wraps the cipher's attachment and FIDO2 - /// sub-keys under it. The existing sub-keys are assumed to be wrapped under - /// [`self.key_identifier()`](IdentifyKey::key_identifier). - pub fn generate_cipher_key( + /// Upgrades the cipher to cipher-key encryption: generates a fresh per-item cipher key and + /// re-wraps the cipher's attachment and FIDO2 sub-keys under it. The existing sub-keys are + /// assumed to be wrapped under [`self.key_identifier()`](IdentifyKey::key_identifier). + pub fn upgrade_to_cipher_key_encryption( &mut self, ctx: &mut KeyStoreContext, wrapping_key: SymmetricKeySlotId, ) -> Result<(), CryptoError> { - self.generate_cipher_key_from(ctx, self.key_identifier(), wrapping_key) + self.upgrade_to_cipher_key_encryption_with_external_key( + ctx, + self.key_identifier(), + wrapping_key, + ) } - /// Variant of [`generate_cipher_key`](Self::generate_cipher_key) that unwraps the existing - /// attachment and FIDO2 sub-keys using an explicitly supplied `source_key` rather than - /// deriving it from [`IdentifyKey::key_identifier`]. Use this when the sub-keys are wrapped - /// under a key other than the cipher's identifier — e.g. during key rotation, where they are - /// under the current user key rather than the [`SymmetricKeySlotId::User`] slot. + /// Variant of [`upgrade_to_cipher_key_encryption`](Self::upgrade_to_cipher_key_encryption) that + /// unwraps the existing attachment and FIDO2 sub-keys using an explicitly supplied `source_key` + /// rather than deriving it from [`IdentifyKey::key_identifier`]. Use this when the sub-keys are + /// wrapped under a key other than the cipher's identifier — e.g. during key rotation, where + /// they are under the current user key rather than the [`SymmetricKeySlotId::User`] slot. /// /// * `source_key` - The key the current attachment/FIDO2 sub-keys are wrapped under. For a /// keyless cipher this is the current user (or organization) key. /// * `wrapping_key` - The key the freshly generated cipher key will be wrapped under (during /// rotation, the new user key). - pub fn generate_cipher_key_from( + pub fn upgrade_to_cipher_key_encryption_with_external_key( &mut self, ctx: &mut KeyStoreContext, source_key: SymmetricKeySlotId, @@ -2470,7 +2474,7 @@ mod tests { } #[test] - fn test_generate_cipher_key() { + fn test_upgrade_to_cipher_key_encryption() { let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_key(key); @@ -2485,7 +2489,7 @@ mod tests { let mut cipher = generate_cipher(); cipher - .generate_cipher_key(&mut key_store.context(), cipher.key_identifier()) + .upgrade_to_cipher_key_encryption(&mut key_store.context(), cipher.key_identifier()) .unwrap(); // Check that the cipher gets encrypted correctly when it's assigned it's own key @@ -2496,7 +2500,7 @@ mod tests { } #[test] - fn test_generate_cipher_key_when_a_cipher_key_already_exists() { + fn test_upgrade_to_cipher_key_encryption_when_a_cipher_key_already_exists() { let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_key(key); @@ -2512,7 +2516,10 @@ mod tests { } original_cipher - .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier()) + .upgrade_to_cipher_key_encryption( + &mut key_store.context(), + original_cipher.key_identifier(), + ) .unwrap(); // Make sure that the cipher key is decryptable @@ -2524,7 +2531,7 @@ mod tests { } #[test] - fn test_generate_cipher_key_ignores_attachments_without_key() { + fn test_upgrade_to_cipher_key_encryption_ignores_attachments_without_key() { let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_key(key); @@ -2542,13 +2549,13 @@ mod tests { cipher.attachments = Some(vec![attachment]); cipher - .generate_cipher_key(&mut key_store.context(), cipher.key_identifier()) + .upgrade_to_cipher_key_encryption(&mut key_store.context(), cipher.key_identifier()) .unwrap(); assert!(cipher.attachments.unwrap()[0].key.is_none()); } #[test] - fn test_generate_cipher_key_from_rewraps_fido2_credentials() { + fn test_upgrade_to_cipher_key_encryption_with_external_key_rewraps_fido2_credentials() { use crate::cipher::login::Fido2CredentialFullView; let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make( @@ -2571,7 +2578,7 @@ mod tests { assert!(cipher.key.is_none()); cipher - .generate_cipher_key_from(&mut ctx, source_key, new_user_key) + .upgrade_to_cipher_key_encryption_with_external_key(&mut ctx, source_key, new_user_key) .unwrap(); // The FIDO2 credential decrypts under the freshly installed cipher key. @@ -2589,7 +2596,7 @@ mod tests { } #[test] - fn test_generate_cipher_key_from_rewraps_attachment_key() { + fn test_upgrade_to_cipher_key_encryption_with_external_key_rewraps_attachment_key() { let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make( SymmetricKeyAlgorithm::Aes256CbcHmac, )); @@ -2619,7 +2626,7 @@ mod tests { assert!(cipher.key.is_none()); cipher - .generate_cipher_key_from(&mut ctx, source_key, new_user_key) + .upgrade_to_cipher_key_encryption_with_external_key(&mut ctx, source_key, new_user_key) .unwrap(); // The attachment key unwraps under the freshly installed cipher key. @@ -2644,7 +2651,7 @@ mod tests { let mut cipher = generate_cipher(); cipher - .generate_cipher_key(&mut ctx, cipher.key_identifier()) + .upgrade_to_cipher_key_encryption(&mut ctx, cipher.key_identifier()) .unwrap(); // Re-encrypt the cipher key with a new wrapping key @@ -2687,7 +2694,7 @@ 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()) + .upgrade_to_cipher_key_encryption(&mut key_store.context(), cipher.key_identifier()) .unwrap(); cipher @@ -2712,7 +2719,7 @@ 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()) + .upgrade_to_cipher_key_encryption(&mut key_store.context(), cipher.key_identifier()) .unwrap(); cipher.organization_id = Some(org); @@ -2907,7 +2914,7 @@ mod tests { let mut cipher_view = generate_cipher(); cipher_view - .generate_cipher_key(&mut ctx, cipher_view.key_identifier()) + .upgrade_to_cipher_key_encryption(&mut ctx, cipher_view.key_identifier()) .unwrap(); let key_id = cipher_view.key_identifier(); 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 6e927e6f7..19446116f 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/admin/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/admin/create.rs @@ -116,7 +116,7 @@ impl CipherAdminClient { // 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.upgrade_to_cipher_key_encryption(&mut key_store.context(), key)?; } let use_blob = should_use_blob_encryption(&self.client, 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 379a84d5c..4dc4b4511 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/admin/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/admin/edit.rs @@ -80,7 +80,7 @@ async fn edit_cipher( // 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.upgrade_to_cipher_key_encryption(&mut key_store.context(), key)?; } // 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 bca97b8a0..321a42f78 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/create.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/create.rs @@ -183,7 +183,7 @@ impl CiphersClient { // 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.upgrade_to_cipher_key_encryption(&mut key_store.context(), key)?; } 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 521066541..d35b81b1a 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/edit.rs @@ -195,7 +195,7 @@ async fn edit_cipher + ?Sized>( // 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.upgrade_to_cipher_key_encryption(&mut key_store.context(), key)?; } 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 214153da2..864ccc8ed 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client/mod.rs @@ -107,7 +107,7 @@ impl CiphersClient { 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.upgrade_to_cipher_key_encryption(&mut key_store.context(), key)?; } let mode = if self.should_use_blob_encryption(cipher_view.organization_id) { @@ -155,7 +155,7 @@ 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.upgrade_to_cipher_key_encryption(&mut ctx, new_key_id)?; } else { cipher_view.reencrypt_cipher_keys(&mut ctx, new_key_id)?; } @@ -204,7 +204,7 @@ impl CiphersClient { .map(|mut cv| { if cv.key.is_none() && enable_cipher_key { let key = cv.key_identifier(); - cv.generate_cipher_key(&mut ctx, key)?; + cv.upgrade_to_cipher_key_encryption(&mut ctx, key)?; } let mode = if self.should_use_blob_encryption(cv.organization_id) { EncryptMode::Blob(cv)