Skip to content

Commit 32ca764

Browse files
committed
Add methods to create rotateable key sets from PRF
1 parent ae9b8b5 commit 32ca764

File tree

7 files changed

+275
-10
lines changed

7 files changed

+275
-10
lines changed

crates/bitwarden-core/src/key_management/crypto.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use std::collections::HashMap;
88

99
use bitwarden_crypto::{
1010
AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable,
11-
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm,
12-
SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey,
13-
UserKey, dangerous_get_v2_rotated_account_keys, safe::PasswordProtectedKeyEnvelopeError,
11+
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, RotateableKeySet,
12+
SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey,
13+
UnsignedSharedKey, UserKey, dangerous_get_v2_rotated_account_keys,
14+
derive_symmetric_key_from_prf, safe::PasswordProtectedKeyEnvelopeError,
1415
};
1516
use bitwarden_encoding::B64;
1617
use bitwarden_error::bitwarden_error;
@@ -498,6 +499,16 @@ fn derive_pin_protected_user_key(
498499
Ok(derived_key.encrypt_user_key(user_key)?)
499500
}
500501

502+
pub(super) fn make_prf_user_key_set(
503+
client: &Client,
504+
prf: B64,
505+
) -> Result<RotateableKeySet, CryptoClientError> {
506+
let prf_key = derive_symmetric_key_from_prf(prf.as_bytes())?;
507+
let ctx = client.internal.get_key_store().context();
508+
let key_set = RotateableKeySet::new(&ctx, &prf_key, SymmetricKeyId::User)?;
509+
Ok(key_set)
510+
}
511+
501512
#[allow(missing_docs)]
502513
#[bitwarden_error(flat)]
503514
#[derive(Debug, thiserror::Error)]

crates/bitwarden-core/src/key_management/crypto_client.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bitwarden_crypto::{CryptoError, Decryptable, Kdf};
1+
use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet};
22
#[cfg(feature = "internal")]
33
use bitwarden_crypto::{EncString, UnsignedSharedKey};
44
use bitwarden_encoding::B64;
@@ -18,7 +18,7 @@ use crate::key_management::{
1818
crypto::{
1919
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
2020
derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
21-
initialize_org_crypto, initialize_user_crypto,
21+
initialize_org_crypto, initialize_user_crypto, make_prf_user_key_set,
2222
},
2323
};
2424
use crate::{
@@ -172,6 +172,12 @@ impl CryptoClient {
172172
derive_pin_user_key(&self.client, encrypted_pin)
173173
}
174174

175+
/// Creates a new rotateable key set for the current user key protected
176+
/// by a key derived from the given PRF.
177+
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet, CryptoClientError> {
178+
make_prf_user_key_set(&self.client, prf)
179+
}
180+
175181
/// Prepares the account for being enrolled in the admin password reset feature. This encrypts
176182
/// the users [UserKey][bitwarden_crypto::UserKey] with the organization's public key.
177183
pub fn enroll_admin_password_reset(

crates/bitwarden-crypto/src/keys/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ pub use kdf::{
3333
default_pbkdf2_iterations,
3434
};
3535
pub(crate) use key_id::{KEY_ID_SIZE, KeyId};
36+
mod prf;
3637
pub(crate) mod utils;
38+
pub use prf::derive_symmetric_key_from_prf;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key};
2+
3+
/// Takes the output of a PRF and derives a symmetric key
4+
pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result<SymmetricCryptoKey, CryptoError> {
5+
let (secret, _) = prf
6+
.split_at_checked(32)
7+
.ok_or_else(|| CryptoError::InvalidKeyLen)?;
8+
let secret: [u8; 32] = secret.try_into().unwrap();
9+
if secret.iter().all(|b| *b == b'\0') {
10+
return Err(CryptoError::ZeroNumber);
11+
}
12+
Ok(SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(
13+
&Box::pin(secret.into()),
14+
)?))
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::*;
20+
#[test]
21+
fn test_prf_succeeds() {
22+
let prf = [
23+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24+
24, 25, 26, 27, 28, 29, 30, 31,
25+
];
26+
derive_symmetric_key_from_prf(&prf).unwrap();
27+
}
28+
29+
#[test]
30+
fn test_zero_key_fails() {
31+
let prf = [
32+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
33+
0, 0, 0,
34+
];
35+
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
36+
assert!(matches!(err, CryptoError::ZeroNumber));
37+
}
38+
#[test]
39+
fn test_short_prf_fails() {
40+
let prf = [0, 1, 2, 3, 4, 5, 6, 7, 8];
41+
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
42+
assert!(matches!(err, CryptoError::InvalidKeyLen));
43+
}
44+
}

crates/bitwarden-crypto/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ mod wordlist;
3232
pub use wordlist::EFF_LONG_WORD_LIST;
3333
mod store;
3434
pub use store::{
35-
KeyStore, KeyStoreContext, RotatedUserKeys, dangerous_get_v2_rotated_account_keys,
35+
KeyStore, KeyStoreContext, RotateableKeySet, RotatedUserKeys,
36+
dangerous_get_v2_rotated_account_keys,
3637
};
3738
mod cose;
3839
pub use cose::CoseSerializable;

crates/bitwarden-crypto/src/store/key_rotation.rs

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
use serde::{Deserialize, Serialize};
2+
13
use crate::{
2-
CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyIds,
3-
KeyStoreContext, SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes,
4-
SymmetricCryptoKey,
4+
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError,
5+
EncString, KeyDecryptable, KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes,
6+
SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey,
7+
UnsignedSharedKey,
58
};
69

710
/// Rotated set of account keys
@@ -45,6 +48,123 @@ pub fn dangerous_get_v2_rotated_account_keys<Ids: KeyIds>(
4548
})
4649
}
4750

51+
/// A set of keys where a given `EncryptionKey` is protected by an encrypted public/private
52+
/// key-pair. The `EncryptionKey` is used to encrypt/decrypt data, while the public/private key-pair
53+
/// is used to rotate the `EncryptionKey`.
54+
///
55+
/// The `PrivateKey` is protected by an `ExternalKey`, such as a `DeviceKey`, or `PrfKey`,
56+
/// and the `PublicKey` is protected by the `EncryptionKey`. This setup allows:
57+
///
58+
/// - Access to `EncryptionKey` by knowing the `ExternalKey`
59+
/// - Rotation to a `NewEncryptionKey` by knowing the current `EncryptionKey`, without needing access to
60+
/// the `ExternalKey`
61+
#[derive(Serialize, Deserialize, Debug)]
62+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
63+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
64+
#[cfg_attr(
65+
feature = "wasm",
66+
derive(tsify::Tsify),
67+
tsify(into_wasm_abi, from_wasm_abi)
68+
)]
69+
pub struct RotateableKeySet {
70+
/// `EncryptionKey` protected by encapsulation key
71+
encapsulated_encryption_key: UnsignedSharedKey,
72+
/// Encapsulation key protected by `EncryptionKey`
73+
encrypted_encapsulation_key: EncString,
74+
/// Decapsulation key protected by `ExternalKey`
75+
wrapped_decapsulation_key: EncString,
76+
}
77+
78+
impl RotateableKeySet {
79+
/// Create a set of keys to allow access to the user key via the provided
80+
/// symmetric wrapping key while allowing the user key to be rotated.
81+
pub fn new<Ids: KeyIds>(
82+
ctx: &KeyStoreContext<Ids>,
83+
wrapping_key: &SymmetricCryptoKey,
84+
key_to_wrap: Ids::Symmetric,
85+
) -> Result<Self, CryptoError> {
86+
let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1);
87+
88+
// This uses this deprecated method and other methods directly on the other keys
89+
// rather than the key store context because we don't want the keys to
90+
// wind up being stored in the borrowed context.
91+
#[allow(deprecated)]
92+
let key_to_wrap_instance = ctx.dangerous_get_symmetric_key(key_to_wrap)?;
93+
// encapsulate encryption key
94+
let encapsulated_encryption_key = UnsignedSharedKey::encapsulate_key_unsigned(
95+
key_to_wrap_instance,
96+
&key_pair.to_public_key(),
97+
)?;
98+
99+
// wrap decapsulation key
100+
let wrapped_decapsulation_key = key_pair.to_der()?.encrypt_with_key(wrapping_key)?;
101+
102+
// wrap encapsulation key with encryption key
103+
// Note: Usually, a public key is - by definition - public, so this should not be necessary.
104+
// The specific use-case for this function is to enable rotateable key sets, where
105+
// the "public key" is not public, with the intent of preventing the server from being able
106+
// to overwrite the user key unlocked by the rotateable keyset.
107+
let encrypted_encapsulation_key = key_pair
108+
.to_public_key()
109+
.to_der()?
110+
.encrypt_with_key(&key_to_wrap_instance)?;
111+
112+
Ok(RotateableKeySet {
113+
encapsulated_encryption_key,
114+
encrypted_encapsulation_key,
115+
wrapped_decapsulation_key,
116+
})
117+
}
118+
119+
// TODO: Eventually, the webauthn-login-strategy service should be migrated
120+
// to use this method, and we can remove the #[allow(dead_code)] attribute.
121+
#[allow(dead_code)]
122+
fn unlock<Ids: KeyIds>(
123+
&self,
124+
ctx: &mut KeyStoreContext<Ids>,
125+
unwrapping_key: &SymmetricCryptoKey,
126+
key_to_unwrap: Ids::Symmetric,
127+
) -> Result<(), CryptoError> {
128+
let priv_key_bytes: Vec<u8> = self
129+
.wrapped_decapsulation_key
130+
.decrypt_with_key(unwrapping_key)?;
131+
let decapsulation_key =
132+
AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?;
133+
let encryption_key = self
134+
.encapsulated_encryption_key
135+
.decapsulate_key_unsigned(&decapsulation_key)?;
136+
#[allow(deprecated)]
137+
ctx.set_symmetric_key(key_to_unwrap, encryption_key)?;
138+
Ok(())
139+
}
140+
}
141+
142+
fn rotate_key_set<Ids: KeyIds>(
143+
ctx: &KeyStoreContext<Ids>,
144+
key_set: RotateableKeySet,
145+
old_encryption_key_id: Ids::Symmetric,
146+
new_encryption_key_id: Ids::Symmetric,
147+
) -> Result<RotateableKeySet, CryptoError> {
148+
let pub_key_bytes = ctx.decrypt_data_with_symmetric_key(
149+
old_encryption_key_id,
150+
&key_set.encrypted_encapsulation_key,
151+
)?;
152+
let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
153+
let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?;
154+
// TODO: There is no method to store only the public key in the store, so we
155+
// have pull out the encryption key to encapsulate it manually.
156+
#[allow(deprecated)]
157+
let new_encryption_key = ctx.dangerous_get_symmetric_key(new_encryption_key_id)?;
158+
let new_encapsulated_key =
159+
UnsignedSharedKey::encapsulate_key_unsigned(new_encryption_key, &encapsulation_key)?;
160+
let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_encryption_key)?;
161+
Ok(RotateableKeySet {
162+
encapsulated_encryption_key: new_encapsulated_key,
163+
encrypted_encapsulation_key: new_encrypted_encapsulation_key,
164+
wrapped_decapsulation_key: key_set.wrapped_decapsulation_key,
165+
})
166+
}
167+
48168
#[cfg(test)]
49169
mod tests {
50170
use super::*;
@@ -137,4 +257,79 @@ mod tests {
137257
.unwrap()
138258
);
139259
}
260+
261+
#[test]
262+
fn test_rotateable_key_set_can_unlock() {
263+
// generate initial keys
264+
let external_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
265+
// set up store
266+
let store: KeyStore<TestIds> = KeyStore::default();
267+
let mut ctx = store.context_mut();
268+
let original_encryption_key_id = TestSymmKey::A(0);
269+
ctx.generate_symmetric_key(original_encryption_key_id)
270+
.unwrap();
271+
272+
// create key set
273+
let key_set =
274+
RotateableKeySet::new(&ctx, &external_key, original_encryption_key_id).unwrap();
275+
276+
// unlock key set
277+
let unwrapped_encryption_key_id = TestSymmKey::A(1);
278+
key_set
279+
.unlock(&mut ctx, &external_key, unwrapped_encryption_key_id)
280+
.unwrap();
281+
282+
#[allow(deprecated)]
283+
let original_key = ctx
284+
.dangerous_get_symmetric_key(original_encryption_key_id)
285+
.unwrap();
286+
#[allow(deprecated)]
287+
let unwrapped_key = ctx
288+
.dangerous_get_symmetric_key(unwrapped_encryption_key_id)
289+
.unwrap();
290+
assert_eq!(original_key, unwrapped_key);
291+
}
292+
293+
#[test]
294+
fn test_rotateable_key_set_rotation() {
295+
// generate initial keys
296+
let external_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
297+
// set up store
298+
let store: KeyStore<TestIds> = KeyStore::default();
299+
let mut ctx = store.context_mut();
300+
let original_encryption_key_id = TestSymmKey::A(1);
301+
ctx.generate_symmetric_key(original_encryption_key_id)
302+
.unwrap();
303+
304+
// create key set
305+
let key_set =
306+
RotateableKeySet::new(&ctx, &external_key, original_encryption_key_id).unwrap();
307+
308+
// rotate
309+
let new_encryption_key_id = TestSymmKey::A(2_1);
310+
ctx.generate_symmetric_key(new_encryption_key_id).unwrap();
311+
let new_key_set = rotate_key_set(
312+
&ctx,
313+
key_set,
314+
original_encryption_key_id,
315+
new_encryption_key_id,
316+
)
317+
.unwrap();
318+
319+
// After rotation, the new key set should be unlocked by the same
320+
// external key and return the new encryption key.
321+
let unwrapped_encryption_key_id = TestSymmKey::A(2_2);
322+
new_key_set
323+
.unlock(&mut ctx, &external_key, unwrapped_encryption_key_id)
324+
.unwrap();
325+
#[allow(deprecated)]
326+
let new_encryption_key = ctx
327+
.dangerous_get_symmetric_key(new_encryption_key_id)
328+
.unwrap();
329+
#[allow(deprecated)]
330+
let unwrapped_encryption_key = ctx
331+
.dangerous_get_symmetric_key(unwrapped_encryption_key_id)
332+
.unwrap();
333+
assert_eq!(new_encryption_key, unwrapped_encryption_key);
334+
}
140335
}

crates/bitwarden-uniffi/src/crypto.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bitwarden_core::key_management::crypto::{
22
DeriveKeyConnectorRequest, DerivePinKeyResponse, EnrollPinResponse, InitOrgCryptoRequest,
33
InitUserCryptoRequest, UpdateKdfResponse, UpdatePasswordResponse,
44
};
5-
use bitwarden_crypto::{EncString, Kdf, UnsignedSharedKey};
5+
use bitwarden_crypto::{EncString, Kdf, RotateableKeySet, UnsignedSharedKey};
66
use bitwarden_encoding::B64;
77

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

91+
/// Creates the a new rotateable key set for the current user key protected
92+
/// by a key derived from the given PRF.
93+
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet> {
94+
Ok(self.0.make_prf_user_key_set(prf)?)
95+
}
96+
9197
/// Create the data necessary to update the user's kdf settings. The user's encryption key is
9298
/// re-encrypted for the password under the new kdf settings. This returns the new encrypted
9399
/// user key and the new password hash but does not update sdk state.

0 commit comments

Comments
 (0)