Skip to main content

bitwarden_crypto/keys/
symmetric_crypto_key.rs

1use std::{pin::Pin, str::FromStr};
2
3use bitwarden_encoding::{B64, FromStrVisitor};
4use ciborium::{Value, value::Integer};
5use coset::{
6    CborSerializable, RegisteredLabelWithPrivate,
7    iana::{EnumI64, KeyOperation, KeyParameter, KeyType, SymmetricKeyParameter},
8};
9use hybrid_array::Array;
10use rand::RngExt;
11#[cfg(test)]
12use rand::SeedableRng;
13#[cfg(test)]
14use rand_chacha::ChaChaRng;
15use serde::{Deserialize, Serialize};
16#[cfg(test)]
17use sha2::Digest;
18use subtle::{Choice, ConstantTimeEq};
19use typenum::U32;
20#[cfg(feature = "wasm")]
21use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
22use zeroize::{Zeroize, ZeroizeOnDrop};
23
24use super::{key_encryptable::CryptoKey, key_id::KeyId};
25use crate::{
26    BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, CoseKeyThumbprint, CryptoError, cose,
27    cose::{CoseKeyThumbprintExt, thumbprint_from_required_params},
28    error::EncodingError,
29};
30
31#[cfg(feature = "wasm")]
32#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
33const TS_CUSTOM_TYPES: &'static str = r#"
34export type SymmetricKey = Tagged<string, "SymmetricKey">;
35"#;
36
37#[cfg(feature = "wasm")]
38impl wasm_bindgen::describe::WasmDescribe for SymmetricCryptoKey {
39    fn describe() {
40        <String as wasm_bindgen::describe::WasmDescribe>::describe();
41    }
42}
43
44#[cfg(feature = "wasm")]
45impl FromWasmAbi for SymmetricCryptoKey {
46    type Abi = <String as FromWasmAbi>::Abi;
47
48    unsafe fn from_abi(abi: Self::Abi) -> Self {
49        use wasm_bindgen::UnwrapThrowExt;
50        let string = unsafe { String::from_abi(abi) };
51        let b64 = B64::try_from(string).unwrap_throw();
52        SymmetricCryptoKey::try_from(b64).unwrap_throw()
53    }
54}
55
56#[cfg(feature = "wasm")]
57impl OptionFromWasmAbi for SymmetricCryptoKey {
58    fn is_none(abi: &Self::Abi) -> bool {
59        <String as OptionFromWasmAbi>::is_none(abi)
60    }
61}
62
63#[cfg(feature = "wasm")]
64impl IntoWasmAbi for SymmetricCryptoKey {
65    type Abi = <String as IntoWasmAbi>::Abi;
66
67    fn into_abi(self) -> Self::Abi {
68        let string: String = self.to_base64().to_string();
69        string.into_abi()
70    }
71}
72
73#[cfg(feature = "wasm")]
74impl TryFrom<wasm_bindgen::JsValue> for SymmetricCryptoKey {
75    type Error = CryptoError;
76
77    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
78        let string = value.as_string().ok_or(CryptoError::InvalidKey)?;
79        Self::try_from(string)
80    }
81}
82
83/// The symmetric key algorithm to use when generating a new symmetric key.
84#[derive(Debug, PartialEq)]
85pub enum SymmetricKeyAlgorithm {
86    /// Used for V1 user keys and data encryption
87    Aes256CbcHmac,
88    /// Used for V2 user keys and data envelopes
89    XChaCha20Poly1305,
90    /// FIPS-approved AEAD.
91    /// Used as content encryption key in:
92    /// [`DataEnvelope`](crate::safe::DataEnvelope)s.
93    /// [`PasswordProtectedKeyEnvelope`](crate::safe::PasswordProtectedKeyEnvelope)
94    ///
95    /// May not be used for multi-device scoped keys such as the user-key or organization-key
96    Aes256Gcm,
97}
98
99/// [Aes256CbcKey] is a symmetric encryption key, consisting of one 256-bit key,
100/// used to decrypt legacy type 0 enc strings. The data is not authenticated
101/// so this should be used with caution, and removed where possible.
102#[derive(ZeroizeOnDrop, Clone)]
103pub struct Aes256CbcKey {
104    /// Uses a pinned heap data structure, as noted in [Pinned heap data][crate#pinned-heap-data]
105    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
106}
107
108impl ConstantTimeEq for Aes256CbcKey {
109    fn ct_eq(&self, other: &Self) -> Choice {
110        self.enc_key.ct_eq(&other.enc_key)
111    }
112}
113
114impl PartialEq for Aes256CbcKey {
115    fn eq(&self, other: &Self) -> bool {
116        self.ct_eq(other).into()
117    }
118}
119
120/// [Aes256CbcHmacKey] is a symmetric encryption key consisting
121/// of two 256-bit keys, one for encryption and one for MAC
122#[derive(ZeroizeOnDrop, Clone)]
123pub struct Aes256CbcHmacKey {
124    /// Uses a pinned heap data structure, as noted in [Pinned heap data][crate#pinned-heap-data]
125    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
126    /// Uses a pinned heap data structure, as noted in [Pinned heap data][crate#pinned-heap-data]
127    pub(crate) mac_key: Pin<Box<Array<u8, U32>>>,
128}
129
130impl ConstantTimeEq for Aes256CbcHmacKey {
131    fn ct_eq(&self, other: &Self) -> Choice {
132        self.enc_key.ct_eq(&other.enc_key) & self.mac_key.ct_eq(&other.mac_key)
133    }
134}
135
136impl PartialEq for Aes256CbcHmacKey {
137    fn eq(&self, other: &Self) -> bool {
138        self.ct_eq(other).into()
139    }
140}
141
142/// [XChaCha20Poly1305Key] is a symmetric encryption key consisting
143/// of one 256-bit key, and contains a key id. In contrast to the
144/// [Aes256CbcKey] and [Aes256CbcHmacKey], this key type is used to create
145/// CoseEncrypt0 messages.
146#[derive(Zeroize, Clone)]
147pub struct XChaCha20Poly1305Key {
148    pub(crate) key_id: KeyId,
149    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
150    /// Controls which key operations are allowed with this key. Note: Only checking decrypt is
151    /// implemented right now, and implementing is tracked here <https://bitwarden.atlassian.net/browse/PM-27513>.
152    /// Further, disabling decrypt will also disable unwrap. The only use-case so far is
153    /// `DataEnvelope`.
154    #[zeroize(skip)]
155    pub(crate) supported_operations: Vec<KeyOperation>,
156}
157
158impl XChaCha20Poly1305Key {
159    /// Creates a new XChaCha20Poly1305Key with a securely sampled cryptographic key and key id.
160    pub fn make() -> Self {
161        let mut rng = bitwarden_random::rng();
162        let mut enc_key = Box::pin(Array::<u8, U32>::default());
163        rng.fill(enc_key.as_mut_slice());
164        let key_id = KeyId::make();
165
166        Self {
167            enc_key,
168            key_id,
169            supported_operations: vec![
170                KeyOperation::Decrypt,
171                KeyOperation::Encrypt,
172                KeyOperation::WrapKey,
173                KeyOperation::UnwrapKey,
174            ],
175        }
176    }
177}
178
179impl ConstantTimeEq for XChaCha20Poly1305Key {
180    fn ct_eq(&self, other: &Self) -> Choice {
181        self.enc_key.ct_eq(&other.enc_key) & self.key_id.ct_eq(&other.key_id)
182    }
183}
184
185impl PartialEq for XChaCha20Poly1305Key {
186    fn eq(&self, other: &Self) -> bool {
187        self.ct_eq(other).into()
188    }
189}
190
191/// [Aes256GcmKey] is a symmetric AEAD key consisting of one 256-bit key
192#[derive(Zeroize, Clone)]
193pub struct Aes256GcmKey {
194    pub(crate) key_id: KeyId,
195    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
196    /// Controls which key operations are allowed with this key. See
197    /// [`XChaCha20Poly1305Key::supported_operations`].
198    #[zeroize(skip)]
199    pub(crate) supported_operations: Vec<KeyOperation>,
200}
201
202impl Aes256GcmKey {
203    /// Creates a new Aes256GcmKey with a securely sampled cryptographic key and key id.
204    pub fn make() -> Self {
205        let mut rng = bitwarden_random::rng();
206        let mut enc_key = Box::pin(Array::<u8, U32>::default());
207        rng.fill(enc_key.as_mut_slice());
208        let key_id = KeyId::make();
209
210        Self {
211            enc_key,
212            key_id,
213            supported_operations: vec![
214                KeyOperation::Decrypt,
215                KeyOperation::Encrypt,
216                KeyOperation::WrapKey,
217                KeyOperation::UnwrapKey,
218            ],
219        }
220    }
221
222    pub(crate) fn disable_key_operation(&mut self, op: KeyOperation) -> &mut Self {
223        self.supported_operations.retain(|k| *k != op);
224        self
225    }
226}
227
228impl ConstantTimeEq for Aes256GcmKey {
229    fn ct_eq(&self, other: &Self) -> Choice {
230        self.enc_key.ct_eq(&other.enc_key) & self.key_id.ct_eq(&other.key_id)
231    }
232}
233
234impl PartialEq for Aes256GcmKey {
235    fn eq(&self, other: &Self) -> bool {
236        self.ct_eq(other).into()
237    }
238}
239
240/// A borrowed view over a symmetric key that is encoded as a COSE key and used as the
241/// content-encryption key for CoseEncrypt0/CoseEncrypt messages.
242pub(crate) enum CoseKeyView<'a> {
243    Aes256Gcm(&'a Aes256GcmKey),
244    XChaCha20Poly1305(&'a XChaCha20Poly1305Key),
245}
246
247impl CoseKeyView<'_> {
248    pub(crate) fn key_id(&self) -> &KeyId {
249        match self {
250            CoseKeyView::Aes256Gcm(k) => &k.key_id,
251            CoseKeyView::XChaCha20Poly1305(k) => &k.key_id,
252        }
253    }
254
255    pub(crate) fn key_bytes(&self) -> &[u8] {
256        match self {
257            CoseKeyView::Aes256Gcm(k) => k.enc_key.as_slice(),
258            CoseKeyView::XChaCha20Poly1305(k) => k.enc_key.as_slice(),
259        }
260    }
261}
262
263/// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::EncString)
264#[derive(ZeroizeOnDrop, Clone)]
265pub enum SymmetricCryptoKey {
266    #[allow(missing_docs)]
267    Aes256CbcKey(Aes256CbcKey),
268    #[allow(missing_docs)]
269    Aes256CbcHmacKey(Aes256CbcHmacKey),
270    /// Data encrypted by XChaCha20Poly1305Key keys has type
271    /// [`Cose_Encrypt0_B64`](crate::EncString::Cose_Encrypt0_B64)
272    XChaCha20Poly1305Key(XChaCha20Poly1305Key),
273    /// FIPS-approved AES-256-GCM key, used as the content-encryption key for FIPS
274    /// [`DataEnvelope`](crate::safe::DataEnvelope)s. Encoded as a COSE key.
275    Aes256GcmKey(Aes256GcmKey),
276}
277
278impl SymmetricCryptoKey {
279    // enc type 0 old static format
280    const AES256_CBC_KEY_LEN: usize = 32;
281    // enc type 2 old static format
282    const AES256_CBC_HMAC_KEY_LEN: usize = 64;
283
284    /// Generate a new random AES256_CBC [SymmetricCryptoKey]
285    ///
286    /// WARNING: This function should only be used with a proper cryptographic RNG. If you do not
287    /// have a good reason for using this function, use
288    /// [SymmetricCryptoKey::make_aes256_cbc_hmac_key] instead.
289    pub(crate) fn make_aes256_cbc_hmac_key_internal(mut rng: impl rand::CryptoRng) -> Self {
290        let mut enc_key = Box::pin(Array::<u8, U32>::default());
291        let mut mac_key = Box::pin(Array::<u8, U32>::default());
292
293        rng.fill(enc_key.as_mut_slice());
294        rng.fill(mac_key.as_mut_slice());
295
296        Self::Aes256CbcHmacKey(Aes256CbcHmacKey { enc_key, mac_key })
297    }
298
299    /// Make a new [SymmetricCryptoKey] for the specified algorithm
300    pub fn make(algorithm: SymmetricKeyAlgorithm) -> Self {
301        match algorithm {
302            SymmetricKeyAlgorithm::Aes256CbcHmac => Self::make_aes256_cbc_hmac_key(),
303            SymmetricKeyAlgorithm::XChaCha20Poly1305 => Self::make_xchacha20_poly1305_key(),
304            SymmetricKeyAlgorithm::Aes256Gcm => Self::Aes256GcmKey(Aes256GcmKey::make()),
305        }
306    }
307
308    /// Generate a new random AES256_CBC_HMAC [SymmetricCryptoKey]
309    pub(crate) fn make_aes256_cbc_hmac_key() -> Self {
310        let rng = bitwarden_random::rng();
311        Self::make_aes256_cbc_hmac_key_internal(rng)
312    }
313
314    /// Generate a new random XChaCha20Poly1305 [SymmetricCryptoKey]
315    pub(crate) fn make_xchacha20_poly1305_key() -> Self {
316        let mut rng = bitwarden_random::rng();
317        let mut enc_key = Box::pin(Array::<u8, U32>::default());
318        rng.fill(enc_key.as_mut_slice());
319        Self::XChaCha20Poly1305Key(XChaCha20Poly1305Key {
320            enc_key,
321            key_id: KeyId::make(),
322            supported_operations: vec![
323                KeyOperation::Decrypt,
324                KeyOperation::Encrypt,
325                KeyOperation::WrapKey,
326                KeyOperation::UnwrapKey,
327            ],
328        })
329    }
330
331    /// Encodes the key to a byte array representation, that is separated by size.
332    /// [SymmetricCryptoKey::Aes256CbcHmacKey] and [SymmetricCryptoKey::Aes256CbcKey] are
333    /// encoded as 64 and 32 bytes respectively. [SymmetricCryptoKey::XChaCha20Poly1305Key]
334    /// is encoded as at least 65 bytes, using padding.
335    ///
336    /// This can be used for storage and transmission in the old byte array format.
337    /// When the wrapping key is a COSE key, and the wrapped key is a COSE key, then this should
338    /// not use the byte representation but instead use the COSE key representation.
339    pub fn to_encoded(&self) -> BitwardenLegacyKeyBytes {
340        let encoded_key = self.to_encoded_raw();
341        match encoded_key {
342            EncodedSymmetricKey::BitwardenLegacyKey(_) => {
343                let encoded_key: Vec<u8> = encoded_key.into();
344                BitwardenLegacyKeyBytes::from(encoded_key)
345            }
346            EncodedSymmetricKey::CoseKey(_) => {
347                let mut encoded_key: Vec<u8> = encoded_key.into();
348                pad_key(&mut encoded_key, (Self::AES256_CBC_HMAC_KEY_LEN + 1) as u8); // This is less than 255
349                BitwardenLegacyKeyBytes::from(encoded_key)
350            }
351        }
352    }
353
354    /// Generate a new random [SymmetricCryptoKey] for unit tests. Note: DO NOT USE THIS
355    /// IN PRODUCTION CODE.
356    #[cfg(test)]
357    pub fn generate_seeded_for_unit_tests(seed: &str) -> Self {
358        // Keep this separate from the other generate function to not break test vectors.
359        let mut seeded_rng = ChaChaRng::from_seed(sha2::Sha256::digest(seed.as_bytes()).into());
360        let mut enc_key = Box::pin(Array::<u8, U32>::default());
361        let mut mac_key = Box::pin(Array::<u8, U32>::default());
362
363        seeded_rng.fill(enc_key.as_mut_slice());
364        seeded_rng.fill(mac_key.as_mut_slice());
365
366        SymmetricCryptoKey::Aes256CbcHmacKey(Aes256CbcHmacKey { enc_key, mac_key })
367    }
368
369    /// Creates the byte representation of the key, without any padding. This should not
370    /// be used directly for creating serialized key representations, instead,
371    /// [SymmetricCryptoKey::to_encoded] should be used.
372    ///
373    /// [SymmetricCryptoKey::Aes256CbcHmacKey] and [SymmetricCryptoKey::Aes256CbcKey] are
374    /// encoded as 64 and 32 byte arrays respectively, representing the key bytes directly.
375    /// [SymmetricCryptoKey::XChaCha20Poly1305Key] is encoded as a COSE key, serialized to a byte
376    /// array. The COSE key can be either directly encrypted using COSE, where the content
377    /// format hints an the key type, or can be represented as a byte array, if padded to be
378    /// larger than the byte array representation of the other key types using the
379    /// aforementioned [SymmetricCryptoKey::to_encoded] function.
380    pub(crate) fn to_encoded_raw(&self) -> EncodedSymmetricKey {
381        match self {
382            Self::Aes256CbcKey(key) => {
383                EncodedSymmetricKey::BitwardenLegacyKey(key.enc_key.to_vec().into())
384            }
385            Self::Aes256CbcHmacKey(key) => {
386                let mut buf = Vec::with_capacity(64);
387                buf.extend_from_slice(&key.enc_key);
388                buf.extend_from_slice(&key.mac_key);
389                EncodedSymmetricKey::BitwardenLegacyKey(buf.into())
390            }
391            Self::XChaCha20Poly1305Key(key) => {
392                let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec());
393                let mut cose_key = builder.key_id((&key.key_id).into());
394                for op in &key.supported_operations {
395                    cose_key = cose_key.add_key_op(*op);
396                }
397                let mut cose_key = cose_key.build();
398                cose_key.alg = Some(RegisteredLabelWithPrivate::PrivateUse(
399                    cose::XCHACHA20_POLY1305,
400                ));
401                EncodedSymmetricKey::CoseKey(
402                    cose_key
403                        .to_vec()
404                        .expect("cose key serialization should not fail")
405                        .into(),
406                )
407            }
408            Self::Aes256GcmKey(key) => {
409                let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec());
410                let mut cose_key = builder.key_id((&key.key_id).into());
411                for op in &key.supported_operations {
412                    cose_key = cose_key.add_key_op(*op);
413                }
414                let mut cose_key = cose_key.build();
415                cose_key.alg = Some(RegisteredLabelWithPrivate::Assigned(
416                    coset::iana::Algorithm::A256GCM,
417                ));
418                EncodedSymmetricKey::CoseKey(
419                    cose_key
420                        .to_vec()
421                        .expect("cose key serialization should not fail")
422                        .into(),
423                )
424            }
425        }
426    }
427
428    pub(crate) fn try_from_cose(serialized_key: &[u8]) -> Result<Self, CryptoError> {
429        let cose_key =
430            coset::CoseKey::from_slice(serialized_key).map_err(|_| CryptoError::InvalidKey)?;
431        let key = SymmetricCryptoKey::try_from(&cose_key)?;
432        Ok(key)
433    }
434
435    #[allow(missing_docs)]
436    pub fn to_base64(&self) -> B64 {
437        B64::from(self.to_encoded().as_ref())
438    }
439
440    /// Returns the key ID of the key, if it has one. Only
441    /// [SymmetricCryptoKey::XChaCha20Poly1305Key] has a key ID.
442    pub fn key_id(&self) -> Option<KeyId> {
443        match self {
444            Self::Aes256CbcKey(_) => None,
445            Self::Aes256CbcHmacKey(_) => None,
446            Self::XChaCha20Poly1305Key(key) => Some(key.key_id.clone()),
447            Self::Aes256GcmKey(key) => Some(key.key_id.clone()),
448        }
449    }
450
451    /// Returns a [`CoseKeyView`] for the COSE-key symmetric variants (AES-256-GCM,
452    /// XChaCha20-Poly1305), or `None` for the legacy AES-CBC variants.
453    pub(crate) fn as_cose_key_view(&self) -> Option<CoseKeyView<'_>> {
454        match self {
455            Self::Aes256GcmKey(k) => Some(CoseKeyView::Aes256Gcm(k)),
456            Self::XChaCha20Poly1305Key(k) => Some(CoseKeyView::XChaCha20Poly1305(k)),
457            Self::Aes256CbcKey(_) | Self::Aes256CbcHmacKey(_) => None,
458        }
459    }
460}
461
462impl CoseKeyThumbprintExt for SymmetricCryptoKey {
463    /// Computes the RFC 9679 thumbprint of this symmetric key.
464    ///
465    /// Returns an error for the legacy AES-CBC keys, which are not representable as COSE keys
466    /// currently.
467    fn thumbprint(&self) -> Result<CoseKeyThumbprint, CryptoError> {
468        let view = self
469            .as_cose_key_view()
470            .ok_or(EncodingError::UnsupportedValue(
471                "legacy AES-CBC keys are not COSE keys and have no thumbprint",
472            ))?;
473        let params = vec![
474            (
475                KeyParameter::Kty.to_i64(),
476                Value::Integer(Integer::from(KeyType::Symmetric.to_i64())),
477            ),
478            (
479                SymmetricKeyParameter::K.to_i64(),
480                Value::Bytes(view.key_bytes().to_vec()),
481            ),
482        ];
483        Ok(thumbprint_from_required_params(params))
484    }
485}
486
487impl ConstantTimeEq for SymmetricCryptoKey {
488    /// Note: This is constant time with respect to comparing two keys of the same type, but not
489    /// constant type with respect to the fact that different keys are compared. If two types of
490    /// different keys are compared, then this does have different timing.
491    fn ct_eq(&self, other: &SymmetricCryptoKey) -> Choice {
492        use SymmetricCryptoKey::*;
493        match (self, other) {
494            (Aes256CbcKey(a), Aes256CbcKey(b)) => a.ct_eq(b),
495            (Aes256CbcKey(_), _) => Choice::from(0),
496
497            (Aes256CbcHmacKey(a), Aes256CbcHmacKey(b)) => a.ct_eq(b),
498            (Aes256CbcHmacKey(_), _) => Choice::from(0),
499
500            (XChaCha20Poly1305Key(a), XChaCha20Poly1305Key(b)) => a.ct_eq(b),
501            (XChaCha20Poly1305Key(_), _) => Choice::from(0),
502
503            (Aes256GcmKey(a), Aes256GcmKey(b)) => a.ct_eq(b),
504            (Aes256GcmKey(_), _) => Choice::from(0),
505        }
506    }
507}
508
509impl PartialEq for SymmetricCryptoKey {
510    fn eq(&self, other: &Self) -> bool {
511        self.ct_eq(other).into()
512    }
513}
514
515impl TryFrom<String> for SymmetricCryptoKey {
516    type Error = CryptoError;
517
518    fn try_from(value: String) -> Result<Self, Self::Error> {
519        let bytes = B64::try_from(value).map_err(|_| CryptoError::InvalidKey)?;
520        Self::try_from(bytes)
521    }
522}
523
524impl TryFrom<B64> for SymmetricCryptoKey {
525    type Error = CryptoError;
526
527    fn try_from(value: B64) -> Result<Self, Self::Error> {
528        Self::try_from(&BitwardenLegacyKeyBytes::from(&value))
529    }
530}
531
532impl TryFrom<&BitwardenLegacyKeyBytes> for SymmetricCryptoKey {
533    type Error = CryptoError;
534
535    fn try_from(value: &BitwardenLegacyKeyBytes) -> Result<Self, Self::Error> {
536        let slice = value.as_ref();
537
538        // Raw byte serialized keys are either 32, 64, or more bytes long. If they are 32/64, they
539        // are the raw serializations of the AES256-CBC, and AES256-CBC-HMAC keys. If they
540        // are longer, they are COSE keys. The COSE keys are padded to the minimum length of
541        // 65 bytes, when serialized to raw byte arrays.
542
543        if slice.len() == Self::AES256_CBC_HMAC_KEY_LEN || slice.len() == Self::AES256_CBC_KEY_LEN {
544            Self::try_from(EncodedSymmetricKey::BitwardenLegacyKey(value.clone()))
545        } else if slice.len() > Self::AES256_CBC_HMAC_KEY_LEN {
546            let unpadded_value = unpad_key(slice)?;
547            Ok(Self::try_from_cose(unpadded_value)?)
548        } else {
549            Err(CryptoError::InvalidKeyLen)
550        }
551    }
552}
553
554impl TryFrom<EncodedSymmetricKey> for SymmetricCryptoKey {
555    type Error = CryptoError;
556
557    fn try_from(value: EncodedSymmetricKey) -> Result<Self, Self::Error> {
558        match value {
559            EncodedSymmetricKey::BitwardenLegacyKey(key)
560                if key.as_ref().len() == Self::AES256_CBC_KEY_LEN =>
561            {
562                let mut enc_key = Box::pin(Array::<u8, U32>::default());
563                enc_key.copy_from_slice(&key.as_ref()[..Self::AES256_CBC_KEY_LEN]);
564                Ok(Self::Aes256CbcKey(Aes256CbcKey { enc_key }))
565            }
566            EncodedSymmetricKey::BitwardenLegacyKey(key)
567                if key.as_ref().len() == Self::AES256_CBC_HMAC_KEY_LEN =>
568            {
569                let mut enc_key = Box::pin(Array::<u8, U32>::default());
570                enc_key.copy_from_slice(&key.as_ref()[..32]);
571
572                let mut mac_key = Box::pin(Array::<u8, U32>::default());
573                mac_key.copy_from_slice(&key.as_ref()[32..]);
574
575                Ok(Self::Aes256CbcHmacKey(Aes256CbcHmacKey {
576                    enc_key,
577                    mac_key,
578                }))
579            }
580            EncodedSymmetricKey::CoseKey(key) => Self::try_from_cose(key.as_ref()),
581            _ => Err(CryptoError::InvalidKey),
582        }
583    }
584}
585
586impl CryptoKey for SymmetricCryptoKey {}
587
588// We manually implement these to make sure we don't print any sensitive data
589impl std::fmt::Debug for SymmetricCryptoKey {
590    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591        match self {
592            SymmetricCryptoKey::Aes256CbcKey(key) => key.fmt(f),
593            SymmetricCryptoKey::Aes256CbcHmacKey(key) => key.fmt(f),
594            SymmetricCryptoKey::XChaCha20Poly1305Key(key) => key.fmt(f),
595            SymmetricCryptoKey::Aes256GcmKey(key) => key.fmt(f),
596        }
597    }
598}
599
600impl std::fmt::Debug for Aes256CbcKey {
601    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602        let mut debug_struct = f.debug_struct("SymmetricKey::Aes256Cbc");
603        #[cfg(feature = "dangerous-crypto-debug")]
604        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
605        debug_struct.finish()
606    }
607}
608
609impl std::fmt::Debug for Aes256CbcHmacKey {
610    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611        let mut debug_struct = f.debug_struct("SymmetricKey::Aes256CbcHmac");
612        #[cfg(feature = "dangerous-crypto-debug")]
613        debug_struct
614            .field("enc_key", &hex::encode(self.enc_key.as_slice()))
615            .field("mac_key", &hex::encode(self.mac_key.as_slice()));
616        debug_struct.finish()
617    }
618}
619
620impl std::fmt::Debug for XChaCha20Poly1305Key {
621    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622        let mut debug_struct = f.debug_struct("SymmetricKey::XChaCha20Poly1305");
623        debug_struct.field("key_id", &self.key_id);
624        debug_struct.field(
625            "supported_operations",
626            &self
627                .supported_operations
628                .iter()
629                .map(|key_operation: &KeyOperation| cose::debug_key_operation(*key_operation))
630                .collect::<Vec<_>>(),
631        );
632        #[cfg(feature = "dangerous-crypto-debug")]
633        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
634        debug_struct.finish()
635    }
636}
637
638impl std::fmt::Debug for Aes256GcmKey {
639    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640        let mut debug_struct = f.debug_struct("SymmetricKey::Aes256Gcm");
641        debug_struct.field("key_id", &self.key_id);
642        debug_struct.field(
643            "supported_operations",
644            &self
645                .supported_operations
646                .iter()
647                .map(|key_operation: &KeyOperation| cose::debug_key_operation(*key_operation))
648                .collect::<Vec<_>>(),
649        );
650        #[cfg(feature = "dangerous-crypto-debug")]
651        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
652        debug_struct.finish()
653    }
654}
655
656/// Pad a key to a minimum length using PKCS7-like padding.
657/// The last N bytes of the padded bytes all have the value N.
658/// For example, padded to size 4, the value 0,0 becomes 0,0,2,2.
659///
660/// Keys that have the type [SymmetricCryptoKey::XChaCha20Poly1305Key] must be distinguishable
661/// from [SymmetricCryptoKey::Aes256CbcHmacKey] keys, when both are encoded as byte arrays
662/// with no additional content format included in the encoding message. For this reason, the
663/// padding is used to make sure that the byte representation uniquely separates the keys by
664/// size of the byte array. The previous key types [SymmetricCryptoKey::Aes256CbcHmacKey] and
665/// [SymmetricCryptoKey::Aes256CbcKey] are 64 and 32 bytes long respectively.
666fn pad_key(key_bytes: &mut Vec<u8>, min_length: u8) {
667    crate::keys::utils::pad_bytes(key_bytes, min_length as usize)
668        .expect("Padding cannot fail since the min_length is < 255")
669}
670
671/// Unpad a key that is padded using the PKCS7-like padding defined by [pad_key].
672/// The last N bytes of the padded bytes all have the value N.
673/// For example, padded to size 4, the value 0,0 becomes 0,0,2,2.
674///
675/// Keys that have the type [SymmetricCryptoKey::XChaCha20Poly1305Key] must be distinguishable
676/// from [SymmetricCryptoKey::Aes256CbcHmacKey] keys, when both are encoded as byte arrays
677/// with no additional content format included in the encoding message. For this reason, the
678/// padding is used to make sure that the byte representation uniquely separates the keys by
679/// size of the byte array the previous key types [SymmetricCryptoKey::Aes256CbcHmacKey] and
680/// [SymmetricCryptoKey::Aes256CbcKey] are 64 and 32 bytes long respectively.
681fn unpad_key(key_bytes: &[u8]) -> Result<&[u8], CryptoError> {
682    crate::keys::utils::unpad_bytes(key_bytes).map_err(|_| CryptoError::InvalidKey)
683}
684
685/// Encoded representation of [SymmetricCryptoKey]
686pub enum EncodedSymmetricKey {
687    /// An Aes256-CBC-HMAC key, or a Aes256-CBC key
688    BitwardenLegacyKey(BitwardenLegacyKeyBytes),
689    /// A symmetric key encoded as a COSE key
690    CoseKey(CoseKeyBytes),
691}
692impl From<EncodedSymmetricKey> for Vec<u8> {
693    fn from(val: EncodedSymmetricKey) -> Self {
694        match val {
695            EncodedSymmetricKey::BitwardenLegacyKey(key) => key.to_vec(),
696            EncodedSymmetricKey::CoseKey(key) => key.to_vec(),
697        }
698    }
699}
700impl EncodedSymmetricKey {
701    /// Returns the content format of the encoded symmetric key.
702    #[allow(private_interfaces)]
703    pub fn content_format(&self) -> ContentFormat {
704        match self {
705            EncodedSymmetricKey::BitwardenLegacyKey(_) => ContentFormat::BitwardenLegacyKey,
706            EncodedSymmetricKey::CoseKey(_) => ContentFormat::CoseKey,
707        }
708    }
709}
710
711// Note: Deserialize and Serialize are only implemented until external usages of
712// symmetric crypto keys are removed. We do not want to support these, but while
713// these have to be supported, we want to have type-safety over having raw byte
714// arrays.
715impl<'de> Deserialize<'de> for SymmetricCryptoKey {
716    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
717    where
718        D: serde::Deserializer<'de>,
719    {
720        deserializer.deserialize_str(FromStrVisitor::new())
721    }
722}
723
724impl FromStr for SymmetricCryptoKey {
725    type Err = CryptoError;
726
727    fn from_str(s: &str) -> Result<Self, Self::Err> {
728        let bytes = B64::try_from(s.to_string()).map_err(|_| CryptoError::InvalidKey)?;
729        Self::try_from(bytes).map_err(|_| CryptoError::InvalidKey)
730    }
731}
732
733impl Serialize for SymmetricCryptoKey {
734    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
735    where
736        S: serde::Serializer,
737    {
738        serializer.serialize_str(&self.to_base64().to_string())
739    }
740}
741
742/// Test only helper for deriving a symmetric key.
743#[cfg(test)]
744pub fn derive_symmetric_key(name: &str) -> Aes256CbcHmacKey {
745    use zeroize::Zeroizing;
746
747    use crate::{derive_shareable_key, generate_random_bytes};
748
749    let secret: Zeroizing<[u8; 16]> = generate_random_bytes();
750    derive_shareable_key(secret, name, None)
751}
752
753#[cfg(test)]
754mod tests {
755    use bitwarden_encoding::B64;
756    use coset::iana::KeyOperation;
757    use hybrid_array::Array;
758    use typenum::U32;
759
760    use super::{SymmetricCryptoKey, derive_symmetric_key};
761    use crate::{
762        Aes256CbcHmacKey, Aes256CbcKey, BitwardenLegacyKeyBytes, CoseKeyThumbprintExt,
763        XChaCha20Poly1305Key,
764        keys::{
765            KeyId,
766            symmetric_crypto_key::{pad_key, unpad_key},
767        },
768    };
769
770    #[test]
771    #[ignore = "Manual test to verify debug format"]
772    fn test_key_debug() {
773        let aes_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
774        println!("{:?}", aes_key);
775        let xchacha_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
776        println!("{:?}", xchacha_key);
777    }
778
779    #[test]
780    fn test_serialize_deserialize_symmetric_crypto_key() {
781        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
782        let serialized = serde_json::to_string(&key).unwrap();
783        let deserialized: SymmetricCryptoKey = serde_json::from_str(&serialized).unwrap();
784        assert_eq!(key, deserialized);
785    }
786
787    #[test]
788    fn test_symmetric_crypto_key() {
789        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
790        let key2 = SymmetricCryptoKey::try_from(key.to_base64()).unwrap();
791
792        assert_eq!(key, key2);
793
794        let key = "UY4B5N4DA4UisCNClgZtRr6VLy9ZF5BXXC7cDZRqourKi4ghEMgISbCsubvgCkHf5DZctQjVot11/vVvN9NNHQ==".to_string();
795        let key2 = SymmetricCryptoKey::try_from(key.clone()).unwrap();
796        assert_eq!(key, key2.to_base64().to_string());
797    }
798
799    #[test]
800    fn test_encode_decode_old_symmetric_crypto_key() {
801        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
802        let encoded = key.to_encoded();
803        let decoded = SymmetricCryptoKey::try_from(&encoded).unwrap();
804        assert_eq!(key, decoded);
805    }
806
807    #[test]
808    fn test_decode_new_symmetric_crypto_key() {
809        let key: B64 = ("pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB").parse()
810        .unwrap();
811        let key = BitwardenLegacyKeyBytes::from(&key);
812        let key = SymmetricCryptoKey::try_from(&key).unwrap();
813        match key {
814            SymmetricCryptoKey::XChaCha20Poly1305Key(_) => (),
815            _ => panic!("Invalid key type"),
816        }
817    }
818
819    #[test]
820    fn test_encode_xchacha20_poly1305_key() {
821        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
822        let encoded = key.to_encoded();
823        let decoded = SymmetricCryptoKey::try_from(&encoded).unwrap();
824        assert_eq!(key, decoded);
825    }
826
827    #[test]
828    fn test_pad_unpad_key_63() {
829        let original_key = vec![1u8; 63];
830        let mut key_bytes = original_key.clone();
831        let mut encoded_bytes = vec![1u8; 65];
832        encoded_bytes[63] = 2;
833        encoded_bytes[64] = 2;
834        pad_key(&mut key_bytes, 65);
835        assert_eq!(encoded_bytes, key_bytes);
836        let unpadded_key = unpad_key(&key_bytes).unwrap();
837        assert_eq!(original_key, unpadded_key);
838    }
839
840    #[test]
841    fn test_pad_unpad_key_64() {
842        let original_key = vec![1u8; 64];
843        let mut key_bytes = original_key.clone();
844        let mut encoded_bytes = vec![1u8; 65];
845        encoded_bytes[64] = 1;
846        pad_key(&mut key_bytes, 65);
847        assert_eq!(encoded_bytes, key_bytes);
848        let unpadded_key = unpad_key(&key_bytes).unwrap();
849        assert_eq!(original_key, unpadded_key);
850    }
851
852    #[test]
853    fn test_pad_unpad_key_65() {
854        let original_key = vec![1u8; 65];
855        let mut key_bytes = original_key.clone();
856        let mut encoded_bytes = vec![1u8; 66];
857        encoded_bytes[65] = 1;
858        pad_key(&mut key_bytes, 65);
859        assert_eq!(encoded_bytes, key_bytes);
860        let unpadded_key = unpad_key(&key_bytes).unwrap();
861        assert_eq!(original_key, unpadded_key);
862    }
863
864    #[test]
865    fn test_eq_aes_cbc_hmac() {
866        let key1 = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
867        let key2 = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
868        assert_ne!(key1, key2);
869        let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap();
870        assert_eq!(key1, key3);
871    }
872
873    #[test]
874    fn test_eq_aes_cbc() {
875        let key1 =
876            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(vec![1u8; 32])).unwrap();
877        let key2 =
878            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(vec![2u8; 32])).unwrap();
879        assert_ne!(key1, key2);
880        let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap();
881        assert_eq!(key1, key3);
882    }
883
884    #[test]
885    fn test_eq_xchacha20_poly1305() {
886        let key1 = SymmetricCryptoKey::make_xchacha20_poly1305_key();
887        let key2 = SymmetricCryptoKey::make_xchacha20_poly1305_key();
888        assert_ne!(key1, key2);
889        let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap();
890        assert_eq!(key1, key3);
891    }
892
893    #[test]
894    fn test_neq_different_key_types() {
895        let key1 = SymmetricCryptoKey::Aes256CbcKey(Aes256CbcKey {
896            enc_key: Box::pin(Array::<u8, U32>::default()),
897        });
898        let key2 = SymmetricCryptoKey::XChaCha20Poly1305Key(XChaCha20Poly1305Key {
899            enc_key: Box::pin(Array::<u8, U32>::default()),
900            key_id: KeyId::from([0; 16]),
901            supported_operations: vec![
902                KeyOperation::Decrypt,
903                KeyOperation::Encrypt,
904                KeyOperation::WrapKey,
905                KeyOperation::UnwrapKey,
906            ],
907        });
908        assert_ne!(key1, key2);
909    }
910
911    #[test]
912    fn test_eq_variant_aes256_cbc() {
913        let key1 = Aes256CbcKey {
914            enc_key: Box::pin(Array::from([1u8; 32])),
915        };
916        let key2 = Aes256CbcKey {
917            enc_key: Box::pin(Array::from([1u8; 32])),
918        };
919        let key3 = Aes256CbcKey {
920            enc_key: Box::pin(Array::from([2u8; 32])),
921        };
922        assert_eq!(key1, key2);
923        assert_ne!(key1, key3);
924    }
925
926    #[test]
927    fn test_eq_variant_aes256_cbc_hmac() {
928        let key1 = Aes256CbcHmacKey {
929            enc_key: Box::pin(Array::from([1u8; 32])),
930            mac_key: Box::pin(Array::from([2u8; 32])),
931        };
932        let key2 = Aes256CbcHmacKey {
933            enc_key: Box::pin(Array::from([1u8; 32])),
934            mac_key: Box::pin(Array::from([2u8; 32])),
935        };
936        let key3 = Aes256CbcHmacKey {
937            enc_key: Box::pin(Array::from([3u8; 32])),
938            mac_key: Box::pin(Array::from([4u8; 32])),
939        };
940        assert_eq!(key1, key2);
941        assert_ne!(key1, key3);
942    }
943
944    #[test]
945    fn test_eq_variant_xchacha20_poly1305() {
946        let key1 = XChaCha20Poly1305Key {
947            enc_key: Box::pin(Array::from([1u8; 32])),
948            key_id: KeyId::from([0; 16]),
949            supported_operations: vec![
950                KeyOperation::Decrypt,
951                KeyOperation::Encrypt,
952                KeyOperation::WrapKey,
953                KeyOperation::UnwrapKey,
954            ],
955        };
956        let key2 = XChaCha20Poly1305Key {
957            enc_key: Box::pin(Array::from([1u8; 32])),
958            key_id: KeyId::from([0; 16]),
959            supported_operations: vec![
960                KeyOperation::Decrypt,
961                KeyOperation::Encrypt,
962                KeyOperation::WrapKey,
963                KeyOperation::UnwrapKey,
964            ],
965        };
966        let key3 = XChaCha20Poly1305Key {
967            enc_key: Box::pin(Array::from([2u8; 32])),
968            key_id: KeyId::from([1; 16]),
969            supported_operations: vec![
970                KeyOperation::Decrypt,
971                KeyOperation::Encrypt,
972                KeyOperation::WrapKey,
973                KeyOperation::UnwrapKey,
974            ],
975        };
976        assert_eq!(key1, key2);
977        assert_ne!(key1, key3);
978    }
979
980    #[test]
981    fn test_neq_different_key_id() {
982        let key1 = XChaCha20Poly1305Key {
983            enc_key: Box::pin(Array::<u8, U32>::default()),
984            key_id: KeyId::from([0; 16]),
985            supported_operations: vec![
986                KeyOperation::Decrypt,
987                KeyOperation::Encrypt,
988                KeyOperation::WrapKey,
989                KeyOperation::UnwrapKey,
990            ],
991        };
992        let key2 = XChaCha20Poly1305Key {
993            enc_key: Box::pin(Array::<u8, U32>::default()),
994            key_id: KeyId::from([1; 16]),
995            supported_operations: vec![
996                KeyOperation::Decrypt,
997                KeyOperation::Encrypt,
998                KeyOperation::WrapKey,
999                KeyOperation::UnwrapKey,
1000            ],
1001        };
1002        assert_ne!(key1, key2);
1003
1004        let key1 = SymmetricCryptoKey::XChaCha20Poly1305Key(key1);
1005        let key2 = SymmetricCryptoKey::XChaCha20Poly1305Key(key2);
1006        assert_ne!(key1, key2);
1007    }
1008
1009    const AES256_GCM_KEY: &str =
1010        "pQEEAlACAgICAgICAgICAgICAgICAwMEhAMEBQYgWCABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=";
1011    const AES256_GCM_KEY_THUMBPRINT: &str =
1012        "3810c7275ee292caca13d938a057a94c75210087d960d3eb6868c0ffe99b5643";
1013
1014    const XCHACHA20_POLY1305_KEY: &str = "pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB";
1015    const XCHACHA20_POLY1305_KEY_THUMBPRINT: &str =
1016        "64aec2d09ef5ba8b310ef9a70346b03422443e295b6f045e38169ae97e579d85";
1017
1018    #[test]
1019    fn test_decode_new_aes256_gcm_key() {
1020        let key: B64 = AES256_GCM_KEY.parse().unwrap();
1021        let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&key)).unwrap();
1022        match key {
1023            SymmetricCryptoKey::Aes256GcmKey(_) => (),
1024            _ => panic!("Invalid key type"),
1025        }
1026    }
1027
1028    #[test]
1029    fn test_thumbprint_aes256_gcm_vector() {
1030        // A fixed AES-256-GCM COSE key.
1031        let key: B64 = AES256_GCM_KEY.parse().unwrap();
1032        let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&key)).unwrap();
1033        assert_eq!(
1034            key.thumbprint().unwrap().to_hex(),
1035            AES256_GCM_KEY_THUMBPRINT
1036        );
1037    }
1038
1039    #[test]
1040    fn test_thumbprint_xchacha20_poly1305_vector() {
1041        // A fixed XChaCha20Poly1305 COSE key.
1042        let key: B64 = XCHACHA20_POLY1305_KEY.parse().unwrap();
1043        let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&key)).unwrap();
1044        assert_eq!(
1045            key.thumbprint().unwrap().to_hex(),
1046            XCHACHA20_POLY1305_KEY_THUMBPRINT
1047        );
1048    }
1049
1050    #[test]
1051    fn test_thumbprint_is_deterministic() {
1052        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
1053        assert_eq!(key.thumbprint().unwrap(), key.thumbprint().unwrap());
1054    }
1055
1056    #[test]
1057    fn test_thumbprint_errors_for_legacy_aes_cbc() {
1058        assert!(
1059            SymmetricCryptoKey::make_aes256_cbc_hmac_key()
1060                .thumbprint()
1061                .is_err()
1062        );
1063    }
1064}