Skip to content

Commit 2514070

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

File tree

7 files changed

+266
-10
lines changed

7 files changed

+266
-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: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
use serde::{Deserialize, Serialize};
2+
use tsify::Tsify;
3+
14
use crate::{
2-
CoseKeyBytes, CoseSerializable, CryptoError, EncString, KeyEncryptable, KeyIds,
3-
KeyStoreContext, SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes,
4-
SymmetricCryptoKey,
5+
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CoseKeyBytes, CoseSerializable, CryptoError,
6+
EncString, KeyDecryptable, KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes,
7+
SignedPublicKey, SignedPublicKeyMessage, SpkiPublicKeyBytes, SymmetricCryptoKey,
8+
UnsignedSharedKey,
59
};
610

711
/// Rotated set of account keys
@@ -45,6 +49,113 @@ pub fn dangerous_get_v2_rotated_account_keys<Ids: KeyIds>(
4549
})
4650
}
4751

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

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)