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