Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions crates/bitwarden-core/src/key_management/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use std::collections::HashMap;

use bitwarden_crypto::{
AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm,
SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey,
UserKey, dangerous_get_v2_rotated_account_keys, safe::PasswordProtectedKeyEnvelopeError,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, RotateableKeySet,
SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey,
UnsignedSharedKey, UserKey, dangerous_get_v2_rotated_account_keys,
derive_symmetric_key_from_prf, safe::PasswordProtectedKeyEnvelopeError,
};
use bitwarden_encoding::B64;
use bitwarden_error::bitwarden_error;
Expand Down Expand Up @@ -498,6 +499,16 @@ fn derive_pin_protected_user_key(
Ok(derived_key.encrypt_user_key(user_key)?)
}

pub(super) fn make_prf_user_key_set(
client: &Client,
prf: B64,
) -> Result<RotateableKeySet, CryptoClientError> {
let prf_key = derive_symmetric_key_from_prf(prf.as_bytes())?;
let ctx = client.internal.get_key_store().context();
let key_set = RotateableKeySet::new(&ctx, &prf_key, SymmetricKeyId::User)?;
Ok(key_set)
}

#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
Expand Down
10 changes: 8 additions & 2 deletions crates/bitwarden-core/src/key_management/crypto_client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitwarden_crypto::{CryptoError, Decryptable, Kdf};
use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet};
#[cfg(feature = "internal")]
use bitwarden_crypto::{EncString, UnsignedSharedKey};
use bitwarden_encoding::B64;
Expand All @@ -18,7 +18,7 @@ use crate::key_management::{
crypto::{
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
initialize_org_crypto, initialize_user_crypto,
initialize_org_crypto, initialize_user_crypto, make_prf_user_key_set,
},
};
use crate::{
Expand Down Expand Up @@ -172,6 +172,12 @@ impl CryptoClient {
derive_pin_user_key(&self.client, encrypted_pin)
}

/// Creates a new rotateable key set for the current user key protected
/// by a key derived from the given PRF.
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet, CryptoClientError> {
make_prf_user_key_set(&self.client, prf)
}

/// Prepares the account for being enrolled in the admin password reset feature. This encrypts
/// the users [UserKey][bitwarden_crypto::UserKey] with the organization's public key.
pub fn enroll_admin_password_reset(
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ pub use kdf::{
default_pbkdf2_iterations,
};
pub(crate) use key_id::{KEY_ID_SIZE, KeyId};
mod prf;
pub(crate) mod utils;
pub use prf::derive_symmetric_key_from_prf;
56 changes: 56 additions & 0 deletions crates/bitwarden-crypto/src/keys/prf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key};

/// Takes the output of a PRF and derives a symmetric key.
///
/// The PRF output must be at least 32 bytes long.
pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result<SymmetricCryptoKey, CryptoError> {
let (secret, _) = prf.split_at_checked(32).ok_or(CryptoError::InvalidKeyLen)?;
let secret: [u8; 32] = secret.try_into().expect("length to be 32 bytes");
// Don't allow uninitialized PRFs
if secret.iter().all(|b| *b == b'\0') {
return Err(CryptoError::ZeroNumber);
}
Ok(SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(
&Box::pin(secret.into()),
)?))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_prf_succeeds() {
let prf = pseudorandom_bytes(32);
let key = derive_symmetric_key_from_prf(&prf).unwrap();
assert!(matches!(key, SymmetricCryptoKey::Aes256CbcHmacKey(_)));
}

#[test]
fn test_zero_key_fails() {
let prf: Vec<u8> = (0..32).map(|_| 0).collect();
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
assert!(matches!(err, CryptoError::ZeroNumber));
}

#[test]
fn test_short_prf_fails() {
let prf = pseudorandom_bytes(9);
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
assert!(matches!(err, CryptoError::InvalidKeyLen));
}

#[test]
fn test_long_prf_truncated_to_proper_length() {
let long_prf = pseudorandom_bytes(33);
let prf = pseudorandom_bytes(32);
let key1 = derive_symmetric_key_from_prf(&long_prf).unwrap();
let key2 = derive_symmetric_key_from_prf(&prf).unwrap();
assert_eq!(key1, key2);
}

/// This returns the same bytes deterministically for a given length.
fn pseudorandom_bytes(len: usize) -> Vec<u8> {
(0..len).map(|x| (x % 255) as u8).collect()
}
}
3 changes: 2 additions & 1 deletion crates/bitwarden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ mod wordlist;
pub use wordlist::EFF_LONG_WORD_LIST;
mod store;
pub use store::{
KeyStore, KeyStoreContext, RotatedUserKeys, dangerous_get_v2_rotated_account_keys,
KeyStore, KeyStoreContext, RotateableKeySet, RotatedUserKeys,
dangerous_get_v2_rotated_account_keys,
};
mod cose;
pub use cose::CoseSerializable;
Expand Down
200 changes: 197 additions & 3 deletions crates/bitwarden-crypto/src/store/key_rotation.rs
Copy link
Contributor

@quexten quexten Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I'd recommend moving RotateableKeySet into it's own file, since it is a different cryptographic construction. key_rotation.rs is really just meant for "User Key Rotation", which may interact with RotateableKeySets, but is a different use-case.

(As a KM note, key_rotation.rs should probably be renamed to user_key_rotation.rs)

Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use serde::{Deserialize, Serialize};

use crate::{
CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyIds,
KeyStoreContext, SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes,
SymmetricCryptoKey,
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError,
EncString, KeyDecryptable, KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes,
SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey,
UnsignedSharedKey,
};

/// Rotated set of account keys
Expand Down Expand Up @@ -45,6 +48,122 @@ pub fn dangerous_get_v2_rotated_account_keys<Ids: KeyIds>(
})
}

/// A set of keys where a given `DownstreamKey` is protected by an encrypted public/private
/// key-pair. The `DownstreamKey` is used to encrypt/decrypt data, while the public/private key-pair
/// is used to rotate the `DownstreamKey`.
///
/// The `PrivateKey` is protected by an `UpstreamKey`, such as a `DeviceKey`, or `PrfKey`,
/// and the `PublicKey` is protected by the `DownstreamKey`. This setup allows:
///
/// - Access to `DownstreamKey` by knowing the `UpstreamKey`
/// - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing
/// access to the `UpstreamKey`
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(
feature = "wasm",
derive(tsify::Tsify),
tsify(into_wasm_abi, from_wasm_abi)
)]
pub struct RotateableKeySet {
/// `DownstreamKey` protected by encapsulation key
encapsulated_downstream_key: UnsignedSharedKey,
/// Encapsulation key protected by `DownstreamKey`
encrypted_encapsulation_key: EncString,
/// Decapsulation key protected by `UpstreamKey`
encrypted_decapsulation_key: EncString,
}

impl RotateableKeySet {
/// Create a set of keys to allow access to the downstream key via the provided
/// upstream key while allowing the downstream key to be rotated.
pub fn new<Ids: KeyIds>(
ctx: &KeyStoreContext<Ids>,
upstream_key: &SymmetricCryptoKey,
downstream_key_id: Ids::Symmetric,
) -> Result<Self, CryptoError> {
let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1);

// This uses this deprecated method and other methods directly on the other keys
// rather than the key store context because we don't want the keys to
// wind up being stored in the borrowed context.
#[allow(deprecated)]
let downstream_key = ctx.dangerous_get_symmetric_key(downstream_key_id)?;
// encapsulate downstream key
let encapsulated_downstream_key =
UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?;

// wrap decapsulation key with upstream key
let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?;

// wrap encapsulation key with downstream key
// Note: Usually, a public key is - by definition - public, so this should not be necessary.
// The specific use-case for this function is to enable rotateable key sets, where
// the "public key" is not public, with the intent of preventing the server from being able
// to overwrite the downstream key unlocked by the rotateable keyset.
let encrypted_encapsulation_key = key_pair
.to_public_key()
.to_der()?
.encrypt_with_key(downstream_key)?;

Ok(RotateableKeySet {
encapsulated_downstream_key,
encrypted_encapsulation_key,
encrypted_decapsulation_key,
})
}

// TODO: Eventually, the webauthn-login-strategy service should be migrated
// to use this method, and we can remove the #[allow(dead_code)] attribute.
#[allow(dead_code)]
fn unlock<Ids: KeyIds>(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is currently unused since we don't yet have plans to use the rotateable key set on mobile, only to create them. However, it implements functionality that is currently done in TypeScript that could be moved to the SDK.

I can remove it until we decide to either migrate the browser apps to this SDK method or start using it in the mobile apps.

&self,
ctx: &mut KeyStoreContext<Ids>,
upstream_key: &SymmetricCryptoKey,
downstream_key_id: Ids::Symmetric,
) -> Result<(), CryptoError> {
let priv_key_bytes: Vec<u8> = self
.encrypted_decapsulation_key
.decrypt_with_key(upstream_key)?;
let decapsulation_key =
AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?;
let downstream_key = self
.encapsulated_downstream_key
.decapsulate_key_unsigned(&decapsulation_key)?;
#[allow(deprecated)]
ctx.set_symmetric_key(downstream_key_id, downstream_key)?;
Ok(())
}
}

#[allow(dead_code)]
fn rotate_key_set<Ids: KeyIds>(
ctx: &KeyStoreContext<Ids>,
key_set: RotateableKeySet,
old_downstream_key_id: Ids::Symmetric,
new_downstream_key_id: Ids::Symmetric,
) -> Result<RotateableKeySet, CryptoError> {
let pub_key_bytes = ctx.decrypt_data_with_symmetric_key(
old_downstream_key_id,
&key_set.encrypted_encapsulation_key,
)?;
let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?;
// TODO: There is no method to store only the public key in the store, so we
// have pull out the downstream key to encapsulate it manually.
#[allow(deprecated)]
let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?;
let new_encapsulated_key =
UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?;
let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?;
Ok(RotateableKeySet {
encapsulated_downstream_key: new_encapsulated_key,
encrypted_encapsulation_key: new_encrypted_encapsulation_key,
encrypted_decapsulation_key: key_set.encrypted_decapsulation_key,
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -137,4 +256,79 @@ mod tests {
.unwrap()
);
}

#[test]
fn test_rotateable_key_set_can_unlock() {
// generate initial keys
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
// set up store
let store: KeyStore<TestIds> = KeyStore::default();
let mut ctx = store.context_mut();
let original_downstream_key_id = TestSymmKey::A(0);
ctx.generate_symmetric_key(original_downstream_key_id)
.unwrap();

// create key set
let key_set =
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();

// unlock key set
let unwrapped_downstream_key_id = TestSymmKey::A(1);
key_set
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
.unwrap();

#[allow(deprecated)]
let original_downstream_key = ctx
.dangerous_get_symmetric_key(original_downstream_key_id)
.unwrap();
#[allow(deprecated)]
let unwrapped_downstream_key = ctx
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
.unwrap();
assert_eq!(original_downstream_key, unwrapped_downstream_key);
}

#[test]
fn test_rotateable_key_set_rotation() {
// generate initial keys
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
// set up store
let store: KeyStore<TestIds> = KeyStore::default();
let mut ctx = store.context_mut();
let original_downstream_key_id = TestSymmKey::A(1);
ctx.generate_symmetric_key(original_downstream_key_id)
.unwrap();

// create key set
let key_set =
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();

// rotate
let new_downstream_key_id = TestSymmKey::A(2_1);
ctx.generate_symmetric_key(new_downstream_key_id).unwrap();
let new_key_set = rotate_key_set(
&ctx,
key_set,
original_downstream_key_id,
new_downstream_key_id,
)
.unwrap();

// After rotation, the new key set should be unlocked by the same
// upstream key and return the new downstream key.
let unwrapped_downstream_key_id = TestSymmKey::A(2_2);
new_key_set
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
.unwrap();
#[allow(deprecated)]
let new_downstream_key = ctx
.dangerous_get_symmetric_key(new_downstream_key_id)
.unwrap();
#[allow(deprecated)]
let unwrapped_downstream_key = ctx
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
.unwrap();
assert_eq!(new_downstream_key, unwrapped_downstream_key);
}
}
8 changes: 7 additions & 1 deletion crates/bitwarden-uniffi/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use bitwarden_core::key_management::crypto::{
DeriveKeyConnectorRequest, DerivePinKeyResponse, EnrollPinResponse, InitOrgCryptoRequest,
InitUserCryptoRequest, UpdateKdfResponse, UpdatePasswordResponse,
};
use bitwarden_crypto::{EncString, Kdf, UnsignedSharedKey};
use bitwarden_crypto::{EncString, Kdf, RotateableKeySet, UnsignedSharedKey};
use bitwarden_encoding::B64;

use crate::error::Result;
Expand Down Expand Up @@ -88,6 +88,12 @@ impl CryptoClient {
Ok(self.0.derive_key_connector(request)?)
}

/// Creates the a new rotateable key set for the current user key protected
/// by a key derived from the given PRF.
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet> {
Ok(self.0.make_prf_user_key_set(prf)?)
}

/// Create the data necessary to update the user's kdf settings. The user's encryption key is
/// re-encrypted for the password under the new kdf settings. This returns the new encrypted
/// user key and the new password hash but does not update sdk state.
Expand Down
12 changes: 9 additions & 3 deletions crates/bitwarden-vault/src/cipher/cipher_client.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
use bitwarden_core::{Client, OrganizationId, key_management::SymmetricKeyId};
use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, SymmetricCryptoKey};
#[cfg(feature = "wasm")]
use bitwarden_core::key_management::SymmetricKeyId;
use bitwarden_core::{Client, OrganizationId};
use bitwarden_crypto::IdentifyKey;
#[cfg(feature = "wasm")]
use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey};
#[cfg(feature = "wasm")]
use bitwarden_encoding::B64;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;

use super::EncryptionContext;
#[cfg(feature = "wasm")]
use crate::Fido2CredentialFullView;
use crate::{
Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError,
Fido2CredentialFullView, cipher::cipher::DecryptCipherListResult,
cipher::cipher::DecryptCipherListResult,
};

#[allow(missing_docs)]
Expand Down
Loading
Loading