From a79946b7831c147baffd33a1d2511272a208a0ef Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 30 Sep 2025 13:07:37 -0500 Subject: [PATCH 1/2] Add method to create rotateable key set --- .../src/key_management/crypto.rs | 134 +++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index aa53b9245..f65bd0531 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -586,6 +586,66 @@ pub(super) fn make_key_pair(user_key: B64) -> Result Result { + let key_store = client.internal.get_key_store(); + let mut ctx = key_store.context(); + let wrapping_key_id = SymmetricKeyId::Local("wrapping_key"); + #[allow(deprecated)] + ctx.set_symmetric_key(wrapping_key_id, wrapping_key)?; + + // encapsulate user key + let encapsulation_key_id = AsymmetricKeyId::Local("encapsulation_key"); + ctx.make_asymmetric_key(encapsulation_key_id)?; + let encapsulated_user_key = + ctx.encapsulate_key_unsigned(encapsulation_key_id, SymmetricKeyId::User)?; + + // wrap decapsulation key + let wrapped_decapsulation_key = ctx.wrap_private_key(wrapping_key_id, encapsulation_key_id)?; + + // wrap encapsulation key with user key + // Note: Usually, a public key is - by definition - public, so this should not be necessary. + // The specific use-case for this function is to enable rotateable key sets, where + // the "public key" is not public, with the intent of preventing the server from being able + // to overwrite the user key unlocked by the rotateable keyset. + let encrypted_encapsulation_key = + ctx.wrap_public_key(SymmetricKeyId::User, encapsulation_key_id)?; + + Ok(RotateableKeySet { + encapsulated_user_key, + encrypted_encapsulation_key, + wrapped_decapsulation_key, + }) +} + /// Request for `verify_asymmetric_keys`. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -824,7 +884,7 @@ pub(crate) fn get_v2_rotated_account_keys( mod tests { use std::num::NonZeroU32; - use bitwarden_crypto::RsaKeyPair; + use bitwarden_crypto::{AsymmetricPublicCryptoKey, RsaKeyPair}; use super::*; use crate::{Client, client::internal::UserKeyState}; @@ -1513,4 +1573,76 @@ mod tests { assert!(get_v2_rotated_account_keys(&client).is_ok()); } + + #[tokio::test] + async fn test_rotateable_key_set() { + let client = Client::new(None); + let original_user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + #[allow(deprecated)] + client + .internal + .get_key_store() + .context_mut() + .set_symmetric_key(SymmetricKeyId::User, original_user_key.clone()) + .unwrap(); + initialize_user_crypto( + &client, + InitUserCryptoRequest { + user_id: Some(UserId::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 100_000.try_into().unwrap(), + }, + email: "test@bitwarden.com".into(), + private_key: TEST_VECTOR_PRIVATE_KEY_V2.parse().unwrap(), + signing_key: Some(TEST_VECTOR_SIGNING_KEY_V2.parse().unwrap()), + security_state: Some(TEST_VECTOR_SECURITY_STATE_V2.parse().unwrap()), + method: InitUserCryptoMethod::DecryptedKey { + decrypted_user_key: TEST_VECTOR_USER_KEY_V2_B64.to_string(), + }, + }, + ) + .await + .unwrap(); + let wrapping_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let keyset = create_rotateable_key_set(&client, wrapping_key.clone()).unwrap(); + let decapsulation_key_bytes: Vec = keyset + .wrapped_decapsulation_key + .decrypt_with_key(&wrapping_key) + .unwrap(); + + // Rotate keys + let new_user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + #[allow(deprecated)] + client + .internal + .get_key_store() + .context_mut() + .set_symmetric_key(SymmetricKeyId::User, new_user_key.clone()) + .unwrap(); + let encapsulation_key_bytes: Vec = keyset + .encrypted_encapsulation_key + .decrypt_with_key(&original_user_key) + .unwrap(); + let encapsulation_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(encapsulation_key_bytes)) + .unwrap(); + let new_encapsulated_user_key = + UnsignedSharedKey::encapsulate_key_unsigned(&new_user_key, &encapsulation_key).unwrap(); + + // User key is accessible from wrapping key + let decapsulation_key = + AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(decapsulation_key_bytes)) + .unwrap(); + + // Both old and new keys should be accessible + let user_key_unwrapped = keyset + .encapsulated_user_key + .decapsulate_key_unsigned(&decapsulation_key) + .unwrap(); + let new_user_key_unwrapped = new_encapsulated_user_key + .decapsulate_key_unsigned(&decapsulation_key) + .unwrap(); + assert_eq!(user_key_unwrapped, original_user_key); + assert_eq!(new_user_key, new_user_key_unwrapped); + } } From e74f17cb6cd0eb7ac729a354bd09a3142c8c073c Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Wed, 1 Oct 2025 11:53:29 -0500 Subject: [PATCH 2/2] Fix tests and formatting --- .../bitwarden-core/src/key_management/crypto.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index f65bd0531..18fc5896c 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -594,9 +594,8 @@ pub(super) fn make_key_pair(user_key: B64) -> Result