Skip to main content

bitwarden_crypto/keys/
rotateable_key_set.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    CryptoError, EncString, KeyDecryptable, KeyEncryptable, KeySlotIds, KeyStoreContext,
5    Pkcs8PrivateKeyBytes, PrivateKey, PublicKey, SpkiPublicKeyBytes, SymmetricCryptoKey,
6    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#[bitwarden_ffi::wasm_record]
23pub struct RotateableKeySet {
24    /// `DownstreamKey` protected by encapsulation key
25    pub encapsulated_downstream_key: UnsignedSharedKey,
26    /// Encapsulation key protected by `DownstreamKey`
27    pub encrypted_encapsulation_key: EncString,
28    /// Decapsulation key protected by `UpstreamKey`
29    pub encrypted_decapsulation_key: EncString,
30}
31
32impl RotateableKeySet {
33    /// Create a set of keys to allow access to the downstream key via the provided
34    /// upstream key while allowing the downstream key to be rotated.
35    pub fn new<Ids: KeySlotIds>(
36        ctx: &KeyStoreContext<Ids>,
37        upstream_key: &SymmetricCryptoKey,
38        downstream_key_id: Ids::Symmetric,
39    ) -> Result<Self, CryptoError> {
40        let key_pair = PrivateKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1);
41
42        // This uses this deprecated method and other methods directly on the other keys
43        // rather than the key store context because we don't want the keys to
44        // wind up being stored in the borrowed context.
45        #[allow(deprecated)]
46        let downstream_key = ctx.dangerous_get_symmetric_key(downstream_key_id)?;
47        // encapsulate downstream key
48        #[expect(deprecated)]
49        let encapsulated_downstream_key =
50            UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?;
51
52        // wrap decapsulation key with upstream key
53        let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?;
54
55        // wrap encapsulation key with downstream key
56        // Note: Usually, a public key is - by definition - public, so this should not be necessary.
57        // The specific use-case for this function is to enable rotateable key sets, where
58        // the "public key" is not public, with the intent of preventing the server from being able
59        // to overwrite the downstream key unlocked by the rotateable keyset.
60        let encrypted_encapsulation_key = key_pair
61            .to_public_key()
62            .to_der()?
63            .encrypt_with_key(downstream_key)?;
64
65        Ok(RotateableKeySet {
66            encapsulated_downstream_key,
67            encrypted_encapsulation_key,
68            encrypted_decapsulation_key,
69        })
70    }
71
72    // TODO: Eventually, the webauthn-login-strategy service should be migrated
73    // to use this method, and we can remove the #[allow(dead_code)] attribute.
74    #[allow(dead_code)]
75    fn unlock<Ids: KeySlotIds>(
76        &self,
77        ctx: &mut KeyStoreContext<Ids>,
78        upstream_key: &SymmetricCryptoKey,
79        downstream_key_id: Ids::Symmetric,
80    ) -> Result<(), CryptoError> {
81        let priv_key_bytes: Vec<u8> = self
82            .encrypted_decapsulation_key
83            .decrypt_with_key(upstream_key)?;
84        let decapsulation_key = PrivateKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?;
85        #[expect(deprecated)]
86        let downstream_key = self
87            .encapsulated_downstream_key
88            .decapsulate_key_unsigned(&decapsulation_key)?;
89        #[allow(deprecated)]
90        ctx.set_symmetric_key(downstream_key_id, downstream_key)?;
91        Ok(())
92    }
93}
94
95#[allow(dead_code)]
96fn rotate_key_set<Ids: KeySlotIds>(
97    ctx: &KeyStoreContext<Ids>,
98    key_set: RotateableKeySet,
99    old_downstream_key_id: Ids::Symmetric,
100    new_downstream_key_id: Ids::Symmetric,
101) -> Result<RotateableKeySet, CryptoError> {
102    let pub_key_bytes = ctx.decrypt_data_with_symmetric_key(
103        old_downstream_key_id,
104        &key_set.encrypted_encapsulation_key,
105    )?;
106    let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
107    let encapsulation_key = PublicKey::from_der(&pub_key)?;
108    // TODO: There is no method to store only the public key in the store, so we
109    // have pull out the downstream key to encapsulate it manually.
110    #[allow(deprecated)]
111    let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?;
112    #[expect(deprecated)]
113    let new_encapsulated_key =
114        UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?;
115    let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?;
116    Ok(RotateableKeySet {
117        encapsulated_downstream_key: new_encapsulated_key,
118        encrypted_encapsulation_key: new_encrypted_encapsulation_key,
119        encrypted_decapsulation_key: key_set.encrypted_decapsulation_key,
120    })
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::{
127        KeyStore,
128        traits::tests::{TestIds, TestSymmKey},
129    };
130
131    #[test]
132    fn test_rotateable_key_set_can_unlock() {
133        // generate initial keys
134        let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
135        // set up store
136        let store: KeyStore<TestIds> = KeyStore::default();
137        let mut ctx = store.context_mut();
138        let original_downstream_key_id = ctx.generate_symmetric_key();
139
140        // create key set
141        let key_set =
142            RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
143
144        // unlock key set
145        let unwrapped_downstream_key_id = TestSymmKey::A(1);
146        key_set
147            .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
148            .unwrap();
149
150        #[allow(deprecated)]
151        let original_downstream_key = ctx
152            .dangerous_get_symmetric_key(original_downstream_key_id)
153            .unwrap();
154        #[allow(deprecated)]
155        let unwrapped_downstream_key = ctx
156            .dangerous_get_symmetric_key(unwrapped_downstream_key_id)
157            .unwrap();
158        assert_eq!(original_downstream_key, unwrapped_downstream_key);
159    }
160
161    #[test]
162    fn test_rotateable_key_set_rotation() {
163        // generate initial keys
164        let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
165        // set up store
166        let store: KeyStore<TestIds> = KeyStore::default();
167        let mut ctx = store.context_mut();
168        let original_downstream_key_id = ctx.generate_symmetric_key();
169
170        // create key set
171        let key_set =
172            RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
173
174        // rotate
175        let new_downstream_key_id = ctx.generate_symmetric_key();
176        let new_key_set = rotate_key_set(
177            &ctx,
178            key_set,
179            original_downstream_key_id,
180            new_downstream_key_id,
181        )
182        .unwrap();
183
184        // After rotation, the new key set should be unlocked by the same
185        // upstream key and return the new downstream key.
186        let unwrapped_downstream_key_id = TestSymmKey::A(2_2);
187        new_key_set
188            .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
189            .unwrap();
190        #[allow(deprecated)]
191        let new_downstream_key = ctx
192            .dangerous_get_symmetric_key(new_downstream_key_id)
193            .unwrap();
194        #[allow(deprecated)]
195        let unwrapped_downstream_key = ctx
196            .dangerous_get_symmetric_key(unwrapped_downstream_key_id)
197            .unwrap();
198        assert_eq!(new_downstream_key, unwrapped_downstream_key);
199    }
200}