bitwarden_crypto/keys/
rotateable_key_set.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    CryptoError, EncString, KeyDecryptable, KeyEncryptable, KeyIds, 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#[cfg_attr(
23    feature = "wasm",
24    derive(tsify::Tsify),
25    tsify(into_wasm_abi, from_wasm_abi)
26)]
27pub 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
36impl 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 = PrivateKey::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        #[expect(deprecated)]
53        let encapsulated_downstream_key =
54            UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?;
55
56        // wrap decapsulation key with upstream key
57        let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?;
58
59        // wrap encapsulation key with downstream key
60        // Note: Usually, a public key is - by definition - public, so this should not be necessary.
61        // The specific use-case for this function is to enable rotateable key sets, where
62        // the "public key" is not public, with the intent of preventing the server from being able
63        // to overwrite the downstream key unlocked by the rotateable keyset.
64        let encrypted_encapsulation_key = key_pair
65            .to_public_key()
66            .to_der()?
67            .encrypt_with_key(downstream_key)?;
68
69        Ok(RotateableKeySet {
70            encapsulated_downstream_key,
71            encrypted_encapsulation_key,
72            encrypted_decapsulation_key,
73        })
74    }
75
76    // TODO: Eventually, the webauthn-login-strategy service should be migrated
77    // to use this method, and we can remove the #[allow(dead_code)] attribute.
78    #[allow(dead_code)]
79    fn unlock<Ids: KeyIds>(
80        &self,
81        ctx: &mut KeyStoreContext<Ids>,
82        upstream_key: &SymmetricCryptoKey,
83        downstream_key_id: Ids::Symmetric,
84    ) -> Result<(), CryptoError> {
85        let priv_key_bytes: Vec<u8> = self
86            .encrypted_decapsulation_key
87            .decrypt_with_key(upstream_key)?;
88        let decapsulation_key = PrivateKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?;
89        #[expect(deprecated)]
90        let downstream_key = self
91            .encapsulated_downstream_key
92            .decapsulate_key_unsigned(&decapsulation_key)?;
93        #[allow(deprecated)]
94        ctx.set_symmetric_key(downstream_key_id, downstream_key)?;
95        Ok(())
96    }
97}
98
99#[allow(dead_code)]
100fn rotate_key_set<Ids: KeyIds>(
101    ctx: &KeyStoreContext<Ids>,
102    key_set: RotateableKeySet,
103    old_downstream_key_id: Ids::Symmetric,
104    new_downstream_key_id: Ids::Symmetric,
105) -> Result<RotateableKeySet, CryptoError> {
106    let pub_key_bytes = ctx.decrypt_data_with_symmetric_key(
107        old_downstream_key_id,
108        &key_set.encrypted_encapsulation_key,
109    )?;
110    let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
111    let encapsulation_key = PublicKey::from_der(&pub_key)?;
112    // TODO: There is no method to store only the public key in the store, so we
113    // have pull out the downstream key to encapsulate it manually.
114    #[allow(deprecated)]
115    let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?;
116    #[expect(deprecated)]
117    let new_encapsulated_key =
118        UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?;
119    let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?;
120    Ok(RotateableKeySet {
121        encapsulated_downstream_key: new_encapsulated_key,
122        encrypted_encapsulation_key: new_encrypted_encapsulation_key,
123        encrypted_decapsulation_key: key_set.encrypted_decapsulation_key,
124    })
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::{
131        KeyStore,
132        traits::tests::{TestIds, TestSymmKey},
133    };
134
135    #[test]
136    fn test_rotateable_key_set_can_unlock() {
137        // generate initial keys
138        let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
139        // set up store
140        let store: KeyStore<TestIds> = KeyStore::default();
141        let mut ctx = store.context_mut();
142        let original_downstream_key_id = ctx.generate_symmetric_key();
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 = ctx.generate_symmetric_key();
173
174        // create key set
175        let key_set =
176            RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();
177
178        // rotate
179        let new_downstream_key_id = ctx.generate_symmetric_key();
180        let new_key_set = rotate_key_set(
181            &ctx,
182            key_set,
183            original_downstream_key_id,
184            new_downstream_key_id,
185        )
186        .unwrap();
187
188        // After rotation, the new key set should be unlocked by the same
189        // upstream key and return the new downstream key.
190        let unwrapped_downstream_key_id = TestSymmKey::A(2_2);
191        new_key_set
192            .unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
193            .unwrap();
194        #[allow(deprecated)]
195        let new_downstream_key = ctx
196            .dangerous_get_symmetric_key(new_downstream_key_id)
197            .unwrap();
198        #[allow(deprecated)]
199        let unwrapped_downstream_key = ctx
200            .dangerous_get_symmetric_key(unwrapped_downstream_key_id)
201            .unwrap();
202        assert_eq!(new_downstream_key, unwrapped_downstream_key);
203    }
204}