Skip to main content

bitwarden_crypto/keys/
key_connector_key.rs

1use std::pin::Pin;
2
3use bitwarden_api_key_connector::models::user_key_response_model::UserKeyResponseModel;
4use bitwarden_encoding::B64;
5use hybrid_array::Array;
6use rand::RngExt;
7use typenum::U32;
8
9use crate::{
10    BitwardenLegacyKeyBytes, CryptoError, EncString, KeyDecryptable, KeySlotIds, KeyStoreContext,
11    SymmetricCryptoKey, keys::utils::stretch_key,
12};
13
14/// Key connector key, used to protect the user key.
15#[derive(Clone)]
16pub struct KeyConnectorKey(pub(super) Pin<Box<Array<u8, U32>>>);
17
18impl KeyConnectorKey {
19    /// Make a new random key for KeyConnector.
20    pub fn make() -> Self {
21        let mut rng = bitwarden_random::rng();
22        let mut key = Box::pin(Array::<u8, U32>::default());
23
24        rng.fill(key.as_mut_slice());
25        KeyConnectorKey(key)
26    }
27
28    /// Wraps (encrypts) a user key from the key store using this key connector key.
29    ///
30    /// The user key identified by `user_key_id` is read from the context and encrypted.
31    // Under `dangerous-crypto-debug` we intentionally log key material, so this arm uses
32    // `tracing::instrument` directly (the `bitwarden_logging` wrapper enforces `skip_all`).
33    // The production arm goes through the wrapper. The `allow` only applies when the dangerous
34    // arm is active.
35    #[cfg_attr(
36        feature = "dangerous-crypto-debug",
37        allow(unknown_lints, tracing_instrument)
38    )]
39    #[cfg_attr(
40        feature = "dangerous-crypto-debug",
41        tracing::instrument(skip(ctx), err)
42    )]
43    #[cfg_attr(
44        not(feature = "dangerous-crypto-debug"),
45        bitwarden_logging::instrument(err)
46    )]
47    pub fn wrap_user_key<Ids: KeySlotIds>(
48        &self,
49        user_key_id: Ids::Symmetric,
50        ctx: &KeyStoreContext<Ids>,
51    ) -> crate::error::Result<EncString> {
52        #[allow(deprecated)]
53        let user_key = ctx.dangerous_get_symmetric_key(user_key_id)?;
54        self.encrypt_user_key(user_key)
55    }
56
57    /// Unwraps (decrypts) a user key and stores it in the key store context.
58    ///
59    /// Returns the local key identifier for the unwrapped user key.
60    #[cfg_attr(
61        feature = "dangerous-crypto-debug",
62        allow(unknown_lints, tracing_instrument)
63    )]
64    #[cfg_attr(
65        feature = "dangerous-crypto-debug",
66        tracing::instrument(skip(ctx), err)
67    )]
68    #[cfg_attr(
69        not(feature = "dangerous-crypto-debug"),
70        bitwarden_logging::instrument(err)
71    )]
72    pub fn unwrap_user_key<Ids: KeySlotIds>(
73        &self,
74        wrapped_user_key: EncString,
75        ctx: &mut KeyStoreContext<Ids>,
76    ) -> crate::error::Result<Ids::Symmetric> {
77        let user_key = self.decrypt_user_key(wrapped_user_key)?;
78        Ok(ctx.add_local_symmetric_key(user_key))
79    }
80
81    /// Wraps the user key with this key connector key.
82    #[cfg_attr(
83        feature = "dangerous-crypto-debug",
84        allow(unknown_lints, tracing_instrument)
85    )]
86    #[cfg_attr(feature = "dangerous-crypto-debug", tracing::instrument(err))]
87    #[cfg_attr(
88        not(feature = "dangerous-crypto-debug"),
89        bitwarden_logging::instrument(err)
90    )]
91    pub fn encrypt_user_key(
92        &self,
93        user_key: &SymmetricCryptoKey,
94    ) -> crate::error::Result<EncString> {
95        let stretched_key = stretch_key(&self.0);
96        let user_key_bytes = user_key.to_encoded();
97        EncString::encrypt_aes256_hmac(user_key_bytes.as_ref(), &stretched_key)
98    }
99
100    /// Unwraps the user key with this key connector key.
101    #[cfg_attr(
102        feature = "dangerous-crypto-debug",
103        allow(unknown_lints, tracing_instrument)
104    )]
105    #[cfg_attr(feature = "dangerous-crypto-debug", tracing::instrument(err))]
106    #[cfg_attr(
107        not(feature = "dangerous-crypto-debug"),
108        bitwarden_logging::instrument(err)
109    )]
110    pub fn decrypt_user_key(
111        &self,
112        user_key: EncString,
113    ) -> crate::error::Result<SymmetricCryptoKey> {
114        let dec: Vec<u8> = match user_key {
115            // Legacy. user_keys were encrypted using `Aes256Cbc_B64` a long time ago. We've since
116            // moved to using `Aes256Cbc_HmacSha256_B64`. However, we still need to support
117            // decrypting these old keys.
118            EncString::Aes256Cbc_B64 { iv, ref data } => {
119                let legacy_key = self.0.clone();
120                crate::aes::decrypt_aes256(&iv, data.clone(), &legacy_key)
121                    .map_err(|_| CryptoError::Decrypt)?
122            }
123            EncString::Aes256Cbc_HmacSha256_B64 { .. } => {
124                let stretched_key = SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(&self.0));
125                user_key.decrypt_with_key(&stretched_key)?
126            }
127            _ => {
128                return Err(CryptoError::OperationNotSupported(
129                    crate::error::UnsupportedOperationError::EncryptionNotImplementedForKey,
130                ));
131            }
132        };
133
134        SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(dec))
135    }
136}
137
138impl std::fmt::Debug for KeyConnectorKey {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        let mut debug_struct = f.debug_struct("KeyConnectorKey");
141        #[cfg(feature = "dangerous-crypto-debug")]
142        debug_struct.field("key", &self.0.as_slice());
143        debug_struct.finish()
144    }
145}
146
147impl From<KeyConnectorKey> for B64 {
148    fn from(key: KeyConnectorKey) -> Self {
149        B64::from(key.0.as_slice())
150    }
151}
152
153impl TryFrom<UserKeyResponseModel> for KeyConnectorKey {
154    type Error = CryptoError;
155
156    fn try_from(s: UserKeyResponseModel) -> Result<Self, Self::Error> {
157        let bytes = B64::try_from(s.key).map_err(|_| CryptoError::InvalidKey)?;
158
159        Ok(KeyConnectorKey(Box::pin(
160            Array::<u8, U32>::try_from(bytes.as_bytes()).map_err(|_| CryptoError::InvalidKeyLen)?,
161        )))
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use bitwarden_encoding::B64;
168    use coset::iana::KeyOperation;
169    use rand_chacha::rand_core::SeedableRng;
170
171    use super::KeyConnectorKey;
172    use crate::{
173        BitwardenLegacyKeyBytes, EncString, SymmetricCryptoKey, UserKey,
174        store::KeyStore,
175        traits::tests::{TestIds, TestSymmKey},
176    };
177
178    const KEY_CONNECTOR_KEY_BYTES: [u8; 32] = [
179        31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, 69, 167,
180        254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75,
181    ];
182
183    #[test]
184    fn test_make_two_different_keys() {
185        let key1 = KeyConnectorKey::make();
186        let key2 = KeyConnectorKey::make();
187        assert_ne!(key1.0.as_slice(), key2.0.as_slice());
188    }
189
190    #[test]
191    fn test_into_base64() {
192        let key: B64 = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into())).into();
193
194        assert_eq!(
195            "H09o4pZHsVrCUKzREYGEUYqnRaf+lQIbJ8VAKhbDVks=",
196            key.to_string()
197        );
198    }
199
200    #[test]
201    fn test_decrypt_user_key_aes256_cbc() {
202        let key_connector_key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
203        let key_connector_key = SymmetricCryptoKey::try_from(key_connector_key).unwrap();
204        let SymmetricCryptoKey::Aes256CbcKey(key_connector_key) = &key_connector_key else {
205            panic!("Key Connector key is not an Aes256CbcKey");
206        };
207
208        let key_connector_key = KeyConnectorKey(key_connector_key.enc_key.clone());
209
210        let user_key: EncString = "0.tn/heK4HLbbEe+yEkC+kvw==|8QM94f7aVTtjm/bmvRdVxOxiLiiZtHYYO7+oBdjFCkilncesx0iVrXPl+tMKqW+Jo7+FtZdPNsTrL6RdoG7i5QbCRVwK+9010+xm7MTQY8s=".parse().unwrap();
211
212        let decrypted_user_key = key_connector_key.decrypt_user_key(user_key).unwrap();
213        let SymmetricCryptoKey::Aes256CbcHmacKey(user_key_unwrapped) = &decrypted_user_key else {
214            panic!("User key is not an Aes256CbcHmacKey");
215        };
216
217        assert_eq!(
218            user_key_unwrapped.enc_key.as_slice(),
219            [
220                116, 170, 187, 43, 80, 212, 193, 202, 234, 181, 57, 66, 151, 249, 59, 47, 70, 16,
221                57, 4, 170, 78, 85, 241, 152, 232, 91, 57, 9, 87, 209, 245,
222            ]
223        );
224        assert_eq!(
225            user_key_unwrapped.mac_key.as_slice(),
226            [
227                40, 245, 106, 140, 2, 225, 138, 213, 98, 223, 92, 168, 135, 208, 22, 194, 31, 21,
228                178, 252, 203, 198, 35, 174, 53, 218, 254, 151, 235, 57, 7, 98,
229            ]
230        );
231    }
232
233    #[test]
234    fn test_encrypt_decrypt_user_key_aes256_cbc_hmac() {
235        let rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
236
237        let key_connector_key = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into()));
238
239        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key_internal(rng);
240        let wrapped_user_key = key_connector_key.encrypt_user_key(&user_key).unwrap();
241        let user_key = UserKey::new(user_key);
242
243        let decrypted_user_key = key_connector_key
244            .decrypt_user_key(wrapped_user_key)
245            .unwrap();
246
247        let SymmetricCryptoKey::Aes256CbcHmacKey(user_key_unwrapped) = &decrypted_user_key else {
248            panic!("User key is not an Aes256CbcHmacKey");
249        };
250
251        assert_eq!(
252            user_key_unwrapped.enc_key.as_slice(),
253            [
254                62, 0, 239, 47, 137, 95, 64, 214, 127, 91, 184, 232, 31, 9, 165, 161, 44, 132, 14,
255                195, 206, 154, 127, 59, 24, 27, 225, 136, 239, 113, 26, 30
256            ]
257        );
258        assert_eq!(
259            user_key_unwrapped.mac_key.as_slice(),
260            [
261                152, 76, 225, 114, 185, 33, 111, 65, 159, 68, 83, 103, 69, 109, 86, 25, 49, 74, 66,
262                163, 218, 134, 176, 1, 56, 123, 253, 184, 14, 12, 254, 66
263            ]
264        );
265
266        assert_eq!(
267            decrypted_user_key, user_key.0,
268            "Decrypted key doesn't match user key"
269        );
270    }
271
272    #[test]
273    fn test_encrypt_decrypt_user_key_xchacha20_poly1305() {
274        let key_connector_key = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into()));
275
276        let user_key_b64: B64 = "pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB".parse()
277            .unwrap();
278        let user_key =
279            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&user_key_b64)).unwrap();
280        let wrapped_user_key = key_connector_key.encrypt_user_key(&user_key).unwrap();
281        let user_key = UserKey::new(user_key);
282
283        let decrypted_user_key = key_connector_key
284            .decrypt_user_key(wrapped_user_key)
285            .unwrap();
286
287        let SymmetricCryptoKey::XChaCha20Poly1305Key(user_key_unwrapped) = &decrypted_user_key
288        else {
289            panic!("User key is not an XChaCha20Poly1305Key");
290        };
291
292        assert_eq!(
293            user_key_unwrapped.enc_key.as_slice(),
294            [
295                183, 191, 108, 186, 178, 87, 154, 168, 79, 133, 139, 174, 170, 47, 89, 200, 247,
296                226, 121, 230, 230, 183, 13, 142, 178, 99, 121, 72, 58, 219, 227, 37
297            ]
298        );
299        assert_eq!(
300            user_key_unwrapped.key_id.as_slice(),
301            [
302                226, 111, 226, 113, 110, 163, 1, 149, 199, 119, 41, 53, 30, 177, 187, 159
303            ]
304        );
305        assert_eq!(
306            user_key_unwrapped.supported_operations,
307            [
308                KeyOperation::Encrypt,
309                KeyOperation::Decrypt,
310                KeyOperation::WrapKey,
311                KeyOperation::UnwrapKey
312            ]
313        );
314
315        assert_eq!(
316            decrypted_user_key, user_key.0,
317            "Decrypted key doesn't match user key"
318        );
319    }
320
321    #[test]
322    fn test_wrap_unwrap_user_key_aes256_cbc_hmac() {
323        let rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
324        let key_connector_key = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into()));
325
326        let store: KeyStore<TestIds> = KeyStore::default();
327        let mut ctx = store.context_mut();
328
329        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key_internal(rng);
330        #[allow(deprecated)]
331        ctx.set_symmetric_key(TestSymmKey::A(0), user_key.clone())
332            .expect("set_symmetric_key should succeed");
333
334        let wrapped = key_connector_key
335            .wrap_user_key(TestSymmKey::A(0), &ctx)
336            .expect("wrap_user_key should succeed");
337
338        let unwrapped_id = key_connector_key
339            .unwrap_user_key(wrapped, &mut ctx)
340            .expect("unwrap_user_key should succeed");
341
342        #[allow(deprecated)]
343        let unwrapped = ctx
344            .dangerous_get_symmetric_key(unwrapped_id)
345            .expect("unwrapped key should be in context");
346
347        assert_eq!(&user_key, unwrapped);
348    }
349}