Skip to content

Commit a004d82

Browse files
iinuwaquexten
andauthored
[PM-26354] Add methods to create rotateable key sets from PRF (#494)
## 🎟️ Tracking [PM-26354](https://bitwarden.atlassian.net/browse/PM-26354) ## πŸ“” Objective In order to set up unlock passkeys on mobile clients, this PR adds a method to create a rotateable key set derived from a PRF value. This is based on existing code in the TypeScript library and web vault: - https://github.com/bitwarden/clients/blob/main/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.ts#L15-L17 - https://github.com/bitwarden/clients/blob/main/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts [PM-26177]: https://bitwarden.atlassian.net/browse/PM-26177?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [PM-26354]: https://bitwarden.atlassian.net/browse/PM-26354?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Bernd Schoolmann <accounts@quexten.com>
1 parent 26df671 commit a004d82

File tree

7 files changed

+293
-7
lines changed

7 files changed

+293
-7
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,
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,
1415
safe::{PasswordProtectedKeyEnvelope, PasswordProtectedKeyEnvelopeError},
1516
};
1617
use bitwarden_encoding::B64;
@@ -508,6 +509,16 @@ fn derive_pin_protected_user_key(
508509
Ok(derived_key.encrypt_user_key(user_key)?)
509510
}
510511

512+
pub(super) fn make_prf_user_key_set(
513+
client: &Client,
514+
prf: B64,
515+
) -> Result<RotateableKeySet, CryptoClientError> {
516+
let prf_key = derive_symmetric_key_from_prf(prf.as_bytes())?;
517+
let ctx = client.internal.get_key_store().context();
518+
let key_set = RotateableKeySet::new(&ctx, &prf_key, SymmetricKeyId::User)?;
519+
Ok(key_set)
520+
}
521+
511522
#[allow(missing_docs)]
512523
#[bitwarden_error(flat)]
513524
#[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,6 +1,6 @@
11
#[cfg(feature = "wasm")]
22
use bitwarden_crypto::safe::PasswordProtectedKeyEnvelope;
3-
use bitwarden_crypto::{CryptoError, Decryptable, Kdf};
3+
use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet};
44
#[cfg(feature = "internal")]
55
use bitwarden_crypto::{EncString, UnsignedSharedKey};
66
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::{
@@ -171,6 +171,12 @@ impl CryptoClient {
171171
derive_pin_user_key(&self.client, encrypted_pin)
172172
}
173173

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

β€Žcrates/bitwarden-crypto/src/keys/mod.rsβ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ pub use kdf::{
3434
default_pbkdf2_iterations,
3535
};
3636
pub(crate) use key_id::{KEY_ID_SIZE, KeyId};
37+
mod prf;
38+
mod rotateable_key_set;
39+
pub use rotateable_key_set::RotateableKeySet;
3740
pub(crate) mod utils;
41+
pub use prf::derive_symmetric_key_from_prf;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key};
2+
3+
/// Takes the output of a PRF and derives a symmetric key.
4+
///
5+
/// The PRF output must be at least 32 bytes long. If longer, only the first 32
6+
/// bytes will be used, and the remainder is discarded.
7+
pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result<SymmetricCryptoKey, CryptoError> {
8+
let (secret, _) = prf.split_at_checked(32).ok_or(CryptoError::InvalidKeyLen)?;
9+
let secret: [u8; 32] = secret.try_into().expect("length to be 32 bytes");
10+
// Don't allow uninitialized PRFs
11+
if secret.iter().all(|b| *b == b'\0') {
12+
return Err(CryptoError::ZeroNumber);
13+
}
14+
Ok(SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(
15+
&Box::pin(secret.into()),
16+
)?))
17+
}
18+
19+
#[cfg(test)]
20+
mod tests {
21+
use super::*;
22+
23+
#[test]
24+
fn test_prf_succeeds() {
25+
let prf = pseudorandom_bytes(32);
26+
let key = derive_symmetric_key_from_prf(&prf).unwrap();
27+
assert!(matches!(key, SymmetricCryptoKey::Aes256CbcHmacKey(_)));
28+
}
29+
30+
#[test]
31+
fn test_zero_key_fails() {
32+
let prf: Vec<u8> = (0..32).map(|_| 0).collect();
33+
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
34+
assert!(matches!(err, CryptoError::ZeroNumber));
35+
}
36+
37+
#[test]
38+
fn test_short_prf_fails() {
39+
let prf = pseudorandom_bytes(9);
40+
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
41+
assert!(matches!(err, CryptoError::InvalidKeyLen));
42+
}
43+
44+
#[test]
45+
fn test_long_prf_truncated_to_proper_length() {
46+
let long_prf = pseudorandom_bytes(33);
47+
let prf = pseudorandom_bytes(32);
48+
let key1 = derive_symmetric_key_from_prf(&long_prf).unwrap();
49+
let key2 = derive_symmetric_key_from_prf(&prf).unwrap();
50+
assert_eq!(key1, key2);
51+
}
52+
53+
/// This returns the same bytes deterministically for a given length.
54+
fn pseudorandom_bytes(len: usize) -> Vec<u8> {
55+
(0..len).map(|x| (x % 255) as u8).collect()
56+
}
57+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
use crate::{
4+
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, EncString, KeyDecryptable,
5+
KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes,
6+
SymmetricCryptoKey, UnsignedSharedKey,
7+
};
8+
9+
/// A set of keys where a given `DownstreamKey` is protected by an encrypted public/private
10+
/// key-pair. The `DownstreamKey` is used to encrypt/decrypt data, while the public/private key-pair
11+
/// is used to rotate the `DownstreamKey`.
12+
///
13+
/// The `PrivateKey` is protected by an `UpstreamKey`, such as a `DeviceKey`, or `PrfKey`,
14+
/// and the `PublicKey` is protected by the `DownstreamKey`. This setup allows:
15+
///
16+
/// - Access to `DownstreamKey` by knowing the `UpstreamKey`
17+
/// - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing
18+
/// access to the `UpstreamKey`
19+
#[derive(Serialize, Deserialize, Debug)]
20+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
21+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
22+
#[cfg_attr(
23+
feature = "wasm",
24+
derive(tsify::Tsify),
25+
tsify(into_wasm_abi, from_wasm_abi)
26+
)]
27+
pub struct RotateableKeySet {
28+
/// `DownstreamKey` protected by encapsulation key
29+
encapsulated_downstream_key: UnsignedSharedKey,
30+
/// Encapsulation key protected by `DownstreamKey`
31+
encrypted_encapsulation_key: EncString,
32+
/// Decapsulation key protected by `UpstreamKey`
33+
encrypted_decapsulation_key: EncString,
34+
}
35+
36+
impl RotateableKeySet {
37+
/// Create a set of keys to allow access to the downstream key via the provided
38+
/// upstream key while allowing the downstream key to be rotated.
39+
pub fn new<Ids: KeyIds>(
40+
ctx: &KeyStoreContext<Ids>,
41+
upstream_key: &SymmetricCryptoKey,
42+
downstream_key_id: Ids::Symmetric,
43+
) -> Result<Self, CryptoError> {
44+
let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1);
45+
46+
// This uses this deprecated method and other methods directly on the other keys
47+
// rather than the key store context because we don't want the keys to
48+
// wind up being stored in the borrowed context.
49+
#[allow(deprecated)]
50+
let downstream_key = ctx.dangerous_get_symmetric_key(downstream_key_id)?;
51+
// encapsulate downstream key
52+
let encapsulated_downstream_key =
53+
UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?;
54+
55+
// wrap decapsulation key with upstream key
56+
let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?;
57+
58+
// wrap encapsulation key with downstream key
59+
// Note: Usually, a public key is - by definition - public, so this should not be necessary.
60+
// The specific use-case for this function is to enable rotateable key sets, where
61+
// the "public key" is not public, with the intent of preventing the server from being able
62+
// to overwrite the downstream key unlocked by the rotateable keyset.
63+
let encrypted_encapsulation_key = key_pair
64+
.to_public_key()
65+
.to_der()?
66+
.encrypt_with_key(downstream_key)?;
67+
68+
Ok(RotateableKeySet {
69+
encapsulated_downstream_key,
70+
encrypted_encapsulation_key,
71+
encrypted_decapsulation_key,
72+
})
73+
}
74+
75+
// TODO: Eventually, the webauthn-login-strategy service should be migrated
76+
// to use this method, and we can remove the #[allow(dead_code)] attribute.
77+
#[allow(dead_code)]
78+
fn unlock<Ids: KeyIds>(
79+
&self,
80+
ctx: &mut KeyStoreContext<Ids>,
81+
upstream_key: &SymmetricCryptoKey,
82+
downstream_key_id: Ids::Symmetric,
83+
) -> Result<(), CryptoError> {
84+
let priv_key_bytes: Vec<u8> = self
85+
.encrypted_decapsulation_key
86+
.decrypt_with_key(upstream_key)?;
87+
let decapsulation_key =
88+
AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?;
89+
let downstream_key = self
90+
.encapsulated_downstream_key
91+
.decapsulate_key_unsigned(&decapsulation_key)?;
92+
#[allow(deprecated)]
93+
ctx.set_symmetric_key(downstream_key_id, downstream_key)?;
94+
Ok(())
95+
}
96+
}
97+
98+
#[allow(dead_code)]
99+
fn rotate_key_set<Ids: KeyIds>(
100+
ctx: &KeyStoreContext<Ids>,
101+
key_set: RotateableKeySet,
102+
old_downstream_key_id: Ids::Symmetric,
103+
new_downstream_key_id: Ids::Symmetric,
104+
) -> Result<RotateableKeySet, CryptoError> {
105+
let pub_key_bytes = ctx.decrypt_data_with_symmetric_key(
106+
old_downstream_key_id,
107+
&key_set.encrypted_encapsulation_key,
108+
)?;
109+
let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
110+
let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?;
111+
// TODO: There is no method to store only the public key in the store, so we
112+
// have pull out the downstream key to encapsulate it manually.
113+
#[allow(deprecated)]
114+
let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?;
115+
let new_encapsulated_key =
116+
UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?;
117+
let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?;
118+
Ok(RotateableKeySet {
119+
encapsulated_downstream_key: new_encapsulated_key,
120+
encrypted_encapsulation_key: new_encrypted_encapsulation_key,
121+
encrypted_decapsulation_key: key_set.encrypted_decapsulation_key,
122+
})
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
use crate::{
129+
KeyStore,
130+
traits::tests::{TestIds, TestSymmKey},
131+
};
132+
133+
#[test]
134+
fn test_rotateable_key_set_can_unlock() {
135+
// generate initial keys
136+
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
137+
// set up store
138+
let store: KeyStore<TestIds> = KeyStore::default();
139+
let mut ctx = store.context_mut();
140+
let original_downstream_key_id = ctx.generate_symmetric_key();
141+
142+
// create key set
143+
let key_set =
144+
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
145+
146+
// unlock key set
147+
let unwrapped_downstream_key_id = TestSymmKey::A(1);
148+
key_set
149+
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
150+
.unwrap();
151+
152+
#[allow(deprecated)]
153+
let original_downstream_key = ctx
154+
.dangerous_get_symmetric_key(original_downstream_key_id)
155+
.unwrap();
156+
#[allow(deprecated)]
157+
let unwrapped_downstream_key = ctx
158+
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
159+
.unwrap();
160+
assert_eq!(original_downstream_key, unwrapped_downstream_key);
161+
}
162+
163+
#[test]
164+
fn test_rotateable_key_set_rotation() {
165+
// generate initial keys
166+
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
167+
// set up store
168+
let store: KeyStore<TestIds> = KeyStore::default();
169+
let mut ctx = store.context_mut();
170+
let original_downstream_key_id = ctx.generate_symmetric_key();
171+
172+
// create key set
173+
let key_set =
174+
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
175+
176+
// rotate
177+
let new_downstream_key_id = ctx.generate_symmetric_key();
178+
let new_key_set = rotate_key_set(
179+
&ctx,
180+
key_set,
181+
original_downstream_key_id,
182+
new_downstream_key_id,
183+
)
184+
.unwrap();
185+
186+
// After rotation, the new key set should be unlocked by the same
187+
// upstream key and return the new downstream key.
188+
let unwrapped_downstream_key_id = TestSymmKey::A(2_2);
189+
new_key_set
190+
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
191+
.unwrap();
192+
#[allow(deprecated)]
193+
let new_downstream_key = ctx
194+
.dangerous_get_symmetric_key(new_downstream_key_id)
195+
.unwrap();
196+
#[allow(deprecated)]
197+
let unwrapped_downstream_key = ctx
198+
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
199+
.unwrap();
200+
assert_eq!(new_downstream_key, unwrapped_downstream_key);
201+
}
202+
}

β€Ž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;
@@ -79,6 +79,12 @@ impl CryptoClient {
7979
Ok(self.0.derive_key_connector(request)?)
8080
}
8181

82+
/// Creates the a new rotateable key set for the current user key protected
83+
/// by a key derived from the given PRF.
84+
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet> {
85+
Ok(self.0.make_prf_user_key_set(prf)?)
86+
}
87+
8288
/// Create the data necessary to update the user's kdf settings. The user's encryption key is
8389
/// re-encrypted for the password under the new kdf settings. This returns the new encrypted
8490
/// user key and the new password hash but does not update sdk state.

β€Žcrates/bitwarden-vault/src/cipher/cipher_client/get.rsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub enum GetCipherError {
1616
#[error(transparent)]
1717
Crypto(#[from] CryptoError),
1818
#[error(transparent)]
19-
RepositoryError(#[from] RepositoryError),
19+
Repository(#[from] RepositoryError),
2020
}
2121

2222
async fn get_cipher(

0 commit comments

Comments
Β (0)