Skip to content

Commit d33e203

Browse files
committed
Move RotatableKeySet to its own module.
1 parent eaf4279 commit d33e203

File tree

4 files changed

+213
-199
lines changed

4 files changed

+213
-199
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ pub use kdf::{
3434
};
3535
pub(crate) use key_id::{KEY_ID_SIZE, KeyId};
3636
mod prf;
37+
mod rotateable_key_set;
38+
pub use rotateable_key_set::RotateableKeySet;
3739
pub(crate) mod utils;
3840
pub use prf::derive_symmetric_key_from_prf;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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 = TestSymmKey::A(0);
141+
ctx.generate_symmetric_key(original_downstream_key_id)
142+
.unwrap();
143+
144+
// create key set
145+
let key_set =
146+
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
147+
148+
// unlock key set
149+
let unwrapped_downstream_key_id = TestSymmKey::A(1);
150+
key_set
151+
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
152+
.unwrap();
153+
154+
#[allow(deprecated)]
155+
let original_downstream_key = ctx
156+
.dangerous_get_symmetric_key(original_downstream_key_id)
157+
.unwrap();
158+
#[allow(deprecated)]
159+
let unwrapped_downstream_key = ctx
160+
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
161+
.unwrap();
162+
assert_eq!(original_downstream_key, unwrapped_downstream_key);
163+
}
164+
165+
#[test]
166+
fn test_rotateable_key_set_rotation() {
167+
// generate initial keys
168+
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
169+
// set up store
170+
let store: KeyStore<TestIds> = KeyStore::default();
171+
let mut ctx = store.context_mut();
172+
let original_downstream_key_id = TestSymmKey::A(1);
173+
ctx.generate_symmetric_key(original_downstream_key_id)
174+
.unwrap();
175+
176+
// create key set
177+
let key_set =
178+
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
179+
180+
// rotate
181+
let new_downstream_key_id = TestSymmKey::A(2_1);
182+
ctx.generate_symmetric_key(new_downstream_key_id).unwrap();
183+
let new_key_set = rotate_key_set(
184+
&ctx,
185+
key_set,
186+
original_downstream_key_id,
187+
new_downstream_key_id,
188+
)
189+
.unwrap();
190+
191+
// After rotation, the new key set should be unlocked by the same
192+
// upstream key and return the new downstream key.
193+
let unwrapped_downstream_key_id = TestSymmKey::A(2_2);
194+
new_key_set
195+
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
196+
.unwrap();
197+
#[allow(deprecated)]
198+
let new_downstream_key = ctx
199+
.dangerous_get_symmetric_key(new_downstream_key_id)
200+
.unwrap();
201+
#[allow(deprecated)]
202+
let unwrapped_downstream_key = ctx
203+
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
204+
.unwrap();
205+
assert_eq!(new_downstream_key, unwrapped_downstream_key);
206+
}
207+
}

crates/bitwarden-crypto/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ mod wordlist;
3232
pub use wordlist::EFF_LONG_WORD_LIST;
3333
mod store;
3434
pub use store::{
35-
KeyStore, KeyStoreContext, RotateableKeySet, RotatedUserKeys,
36-
dangerous_get_v2_rotated_account_keys,
35+
KeyStore, KeyStoreContext, RotatedUserKeys, dangerous_get_v2_rotated_account_keys,
3736
};
3837
mod cose;
3938
pub use cose::CoseSerializable;

0 commit comments

Comments
 (0)