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, hazmat::symmetric_encryption::Aes256Cbc, 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                Aes256Cbc::decrypt(&iv, data, &(*self.0).into())
120                    .map_err(|_| CryptoError::Decrypt)?
121            }
122            EncString::Aes256Cbc_HmacSha256_B64 { .. } => {
123                let stretched_key = SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(&self.0));
124                user_key.decrypt_with_key(&stretched_key)?
125            }
126            _ => {
127                return Err(CryptoError::OperationNotSupported(
128                    crate::error::UnsupportedOperationError::EncryptionNotImplementedForKey,
129                ));
130            }
131        };
132
133        SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(dec))
134    }
135}
136
137impl std::fmt::Debug for KeyConnectorKey {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        let mut debug_struct = f.debug_struct("KeyConnectorKey");
140        #[cfg(feature = "dangerous-crypto-debug")]
141        debug_struct.field("key", &self.0.as_slice());
142        debug_struct.finish()
143    }
144}
145
146impl From<KeyConnectorKey> for B64 {
147    fn from(key: KeyConnectorKey) -> Self {
148        B64::from(key.0.as_slice())
149    }
150}
151
152impl TryFrom<UserKeyResponseModel> for KeyConnectorKey {
153    type Error = CryptoError;
154
155    fn try_from(s: UserKeyResponseModel) -> Result<Self, Self::Error> {
156        let bytes = B64::try_from(s.key).map_err(|_| CryptoError::InvalidKey)?;
157
158        Ok(KeyConnectorKey(Box::pin(
159            Array::<u8, U32>::try_from(bytes.as_bytes()).map_err(|_| CryptoError::InvalidKeyLen)?,
160        )))
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use bitwarden_encoding::B64;
167    use coset::iana::KeyOperation;
168    use rand_chacha::rand_core::SeedableRng;
169
170    use super::KeyConnectorKey;
171    use crate::{
172        BitwardenLegacyKeyBytes, EncString, SymmetricCryptoKey, UserKey,
173        store::KeyStore,
174        traits::tests::{TestIds, TestSymmKey},
175    };
176
177    const KEY_CONNECTOR_KEY_BYTES: [u8; 32] = [
178        31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, 69, 167,
179        254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75,
180    ];
181
182    #[test]
183    fn test_make_two_different_keys() {
184        let key1 = KeyConnectorKey::make();
185        let key2 = KeyConnectorKey::make();
186        assert_ne!(key1.0.as_slice(), key2.0.as_slice());
187    }
188
189    #[test]
190    fn test_into_base64() {
191        let key: B64 = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into())).into();
192
193        assert_eq!(
194            "H09o4pZHsVrCUKzREYGEUYqnRaf+lQIbJ8VAKhbDVks=",
195            key.to_string()
196        );
197    }
198
199    #[test]
200    fn test_decrypt_user_key_aes256_cbc() {
201        let key_connector_key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
202        let key_connector_key = SymmetricCryptoKey::try_from(key_connector_key).unwrap();
203        let SymmetricCryptoKey::Aes256CbcKey(key_connector_key) = &key_connector_key else {
204            panic!("Key Connector key is not an Aes256CbcKey");
205        };
206
207        let key_connector_key = KeyConnectorKey(key_connector_key.enc_key.clone());
208
209        let user_key: EncString = "0.tn/heK4HLbbEe+yEkC+kvw==|8QM94f7aVTtjm/bmvRdVxOxiLiiZtHYYO7+oBdjFCkilncesx0iVrXPl+tMKqW+Jo7+FtZdPNsTrL6RdoG7i5QbCRVwK+9010+xm7MTQY8s=".parse().unwrap();
210
211        let decrypted_user_key = key_connector_key.decrypt_user_key(user_key).unwrap();
212        let SymmetricCryptoKey::Aes256CbcHmacKey(user_key_unwrapped) = &decrypted_user_key else {
213            panic!("User key is not an Aes256CbcHmacKey");
214        };
215
216        assert_eq!(
217            user_key_unwrapped.enc_key.as_slice(),
218            [
219                116, 170, 187, 43, 80, 212, 193, 202, 234, 181, 57, 66, 151, 249, 59, 47, 70, 16,
220                57, 4, 170, 78, 85, 241, 152, 232, 91, 57, 9, 87, 209, 245,
221            ]
222        );
223        assert_eq!(
224            user_key_unwrapped.mac_key.as_slice(),
225            [
226                40, 245, 106, 140, 2, 225, 138, 213, 98, 223, 92, 168, 135, 208, 22, 194, 31, 21,
227                178, 252, 203, 198, 35, 174, 53, 218, 254, 151, 235, 57, 7, 98,
228            ]
229        );
230    }
231
232    #[test]
233    fn test_encrypt_decrypt_user_key_aes256_cbc_hmac() {
234        let rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
235
236        let key_connector_key = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into()));
237
238        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key_internal(rng);
239        let wrapped_user_key = key_connector_key.encrypt_user_key(&user_key).unwrap();
240        let user_key = UserKey::new(user_key);
241
242        let decrypted_user_key = key_connector_key
243            .decrypt_user_key(wrapped_user_key)
244            .unwrap();
245
246        let SymmetricCryptoKey::Aes256CbcHmacKey(user_key_unwrapped) = &decrypted_user_key else {
247            panic!("User key is not an Aes256CbcHmacKey");
248        };
249
250        assert_eq!(
251            user_key_unwrapped.enc_key.as_slice(),
252            [
253                62, 0, 239, 47, 137, 95, 64, 214, 127, 91, 184, 232, 31, 9, 165, 161, 44, 132, 14,
254                195, 206, 154, 127, 59, 24, 27, 225, 136, 239, 113, 26, 30
255            ]
256        );
257        assert_eq!(
258            user_key_unwrapped.mac_key.as_slice(),
259            [
260                152, 76, 225, 114, 185, 33, 111, 65, 159, 68, 83, 103, 69, 109, 86, 25, 49, 74, 66,
261                163, 218, 134, 176, 1, 56, 123, 253, 184, 14, 12, 254, 66
262            ]
263        );
264
265        assert_eq!(
266            decrypted_user_key, user_key.0,
267            "Decrypted key doesn't match user key"
268        );
269    }
270
271    #[test]
272    fn test_encrypt_decrypt_user_key_xchacha20_poly1305() {
273        let key_connector_key = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into()));
274
275        let user_key_b64: B64 = "pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB".parse()
276            .unwrap();
277        let user_key =
278            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&user_key_b64)).unwrap();
279        let wrapped_user_key = key_connector_key.encrypt_user_key(&user_key).unwrap();
280        let user_key = UserKey::new(user_key);
281
282        let decrypted_user_key = key_connector_key
283            .decrypt_user_key(wrapped_user_key)
284            .unwrap();
285
286        let SymmetricCryptoKey::XChaCha20Poly1305Key(user_key_unwrapped) = &decrypted_user_key
287        else {
288            panic!("User key is not an XChaCha20Poly1305Key");
289        };
290
291        assert_eq!(
292            user_key_unwrapped.enc_key.as_slice(),
293            [
294                183, 191, 108, 186, 178, 87, 154, 168, 79, 133, 139, 174, 170, 47, 89, 200, 247,
295                226, 121, 230, 230, 183, 13, 142, 178, 99, 121, 72, 58, 219, 227, 37
296            ]
297        );
298        assert_eq!(
299            user_key_unwrapped.key_id.as_slice(),
300            [
301                226, 111, 226, 113, 110, 163, 1, 149, 199, 119, 41, 53, 30, 177, 187, 159
302            ]
303        );
304        assert_eq!(
305            user_key_unwrapped.supported_operations,
306            [
307                KeyOperation::Encrypt,
308                KeyOperation::Decrypt,
309                KeyOperation::WrapKey,
310                KeyOperation::UnwrapKey
311            ]
312        );
313
314        assert_eq!(
315            decrypted_user_key, user_key.0,
316            "Decrypted key doesn't match user key"
317        );
318    }
319
320    #[test]
321    fn test_wrap_unwrap_user_key_aes256_cbc_hmac() {
322        let rng = rand_chacha::ChaCha8Rng::from_seed([0u8; 32]);
323        let key_connector_key = KeyConnectorKey(Box::pin(KEY_CONNECTOR_KEY_BYTES.into()));
324
325        let store: KeyStore<TestIds> = KeyStore::default();
326        let mut ctx = store.context_mut();
327
328        let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key_internal(rng);
329        #[allow(deprecated)]
330        ctx.set_symmetric_key(TestSymmKey::A(0), user_key.clone())
331            .expect("set_symmetric_key should succeed");
332
333        let wrapped = key_connector_key
334            .wrap_user_key(TestSymmKey::A(0), &ctx)
335            .expect("wrap_user_key should succeed");
336
337        let unwrapped_id = key_connector_key
338            .unwrap_user_key(wrapped, &mut ctx)
339            .expect("unwrap_user_key should succeed");
340
341        #[allow(deprecated)]
342        let unwrapped = ctx
343            .dangerous_get_symmetric_key(unwrapped_id)
344            .expect("unwrapped key should be in context");
345
346        assert_eq!(&user_key, unwrapped);
347    }
348}