bitwarden_crypto/keys/
rotateable_key_set.rs

1use serde::{Deserialize, Serialize};
2
3use 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)]
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 = 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)]
99fn 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)]
126mod 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}