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, U64};
20use zeroize::{Zeroize, ZeroizeOnDrop};
21
22use super::{
23    key_encryptable::CryptoKey,
24    key_id::{KEY_ID_SIZE, KeyId},
25};
26use crate::{
27    BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, CoseKeyThumbprint, CryptoError, cose,
28    cose::{
29        CoseKeyThumbprintExt, symmetric::CoseContentEncryptionAlgorithm,
30        thumbprint_from_required_params,
31    },
32    error::EncodingError,
33};
34
35#[cfg(feature = "wasm")]
36#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
37const TS_CUSTOM_TYPES: &'static str = r#"
38export type SymmetricKey = Tagged<string, "SymmetricKey">;
39"#;
40
41bitwarden_ffi::impl_wire_string!(
42    SymmetricCryptoKey,
43    parse = |wire: String| {
44        let b64 = B64::try_from(wire).map_err(|err| err.to_string())?;
45        SymmetricCryptoKey::try_from(b64).map_err(|err| err.to_string())
46    },
47    format = |key: SymmetricCryptoKey| key.to_base64().to_string(),
48);
49
50#[cfg(feature = "wasm")]
51impl TryFrom<wasm_bindgen::JsValue> for SymmetricCryptoKey {
52    type Error = CryptoError;
53
54    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
55        let string = value.as_string().ok_or(CryptoError::InvalidKey)?;
56        Self::try_from(string)
57    }
58}
59
60/// The symmetric key algorithm to use when generating a new symmetric key.
61#[derive(Debug, PartialEq)]
62pub enum SymmetricKeyAlgorithm {
63    /// Used for V1 user keys and data encryption
64    Aes256CbcHmac,
65    /// Used by old PIN PasswordProtectedKeyEnvelope
66    XChaCha20Poly1305,
67    /// FIPS-approved AEAD.
68    /// Used as content encryption key in:
69    /// [`DataEnvelope`](crate::safe::DataEnvelope)s.
70    /// [`PasswordProtectedKeyEnvelope`](crate::safe::PasswordProtectedKeyEnvelope)
71    ///
72    /// May not be used for multi-device scoped keys such as the user-key or organization-key
73    Aes256Gcm,
74    /// Extended-nonce AES-256-GCM. Used for V2 user keys and as a general-purpose
75    /// encryption/wrapping key.
76    XAes256Gcm,
77}
78
79/// [Aes256CbcKey] is a symmetric encryption key, consisting of one 256-bit key,
80/// used to decrypt legacy type 0 enc strings. The data is not authenticated
81/// so this should be used with caution, and removed where possible.
82#[derive(ZeroizeOnDrop, Clone)]
83pub struct Aes256CbcKey {
84    /// Uses a pinned heap data structure, as noted in [Pinned heap data][crate#pinned-heap-data]
85    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
86}
87
88impl ConstantTimeEq for Aes256CbcKey {
89    fn ct_eq(&self, other: &Self) -> Choice {
90        self.enc_key.ct_eq(&other.enc_key)
91    }
92}
93
94impl PartialEq for Aes256CbcKey {
95    fn eq(&self, other: &Self) -> bool {
96        self.ct_eq(other).into()
97    }
98}
99
100/// The size of the encryption half of an [`Aes256CbcHmacKey`].
101pub(crate) const AES256_CBC_HMAC_ENC_KEY_SIZE: usize = 32;
102/// The size of the MAC half of an [`Aes256CbcHmacKey`].
103pub(crate) const AES256_CBC_HMAC_MAC_KEY_SIZE: usize = 32;
104/// The size of the composite `enc_key || mac_key` buffer of an [`Aes256CbcHmacKey`].
105pub(crate) const AES256_CBC_HMAC_KEY_SIZE: usize =
106    AES256_CBC_HMAC_ENC_KEY_SIZE + AES256_CBC_HMAC_MAC_KEY_SIZE;
107
108/// [Aes256CbcHmacKey] is a symmetric encryption key consisting
109/// of two 256-bit keys, one for encryption and one for MAC
110#[derive(ZeroizeOnDrop, Clone)]
111pub struct Aes256CbcHmacKey {
112    /// The encryption and MAC keys, stored contiguously as `enc_key || mac_key`.
113    ///
114    /// They are kept in one buffer rather than two so that the 64-byte composite form — the layout
115    /// used by the byte serialization, by the
116    /// [`Aes256CbcHmacSha256Aead`](crate::hazmat::symmetric_encryption::Aes256CbcHmacSha256Aead)
117    /// cipher, and by this key's COSE key view — can be borrowed instead of copied. Use
118    /// [`Aes256CbcHmacKey::enc_key`] and [`Aes256CbcHmacKey::mac_key`] for zero-copy views of the
119    /// individual halves.
120    ///
121    /// Uses a pinned heap data structure, as noted in [Pinned heap data][crate#pinned-heap-data]
122    pub(crate) key: Pin<Box<Array<u8, U64>>>,
123}
124
125impl ConstantTimeEq for Aes256CbcHmacKey {
126    fn ct_eq(&self, other: &Self) -> Choice {
127        self.key.ct_eq(&other.key)
128    }
129}
130
131impl Aes256CbcHmacKey {
132    /// Builds a key from its two halves.
133    pub(crate) fn new(
134        enc_key: &[u8; AES256_CBC_HMAC_ENC_KEY_SIZE],
135        mac_key: &[u8; AES256_CBC_HMAC_MAC_KEY_SIZE],
136    ) -> Self {
137        let mut key = Box::pin(Array::<u8, U64>::default());
138        let (enc, mac) = key.split_at_mut(AES256_CBC_HMAC_ENC_KEY_SIZE);
139        enc.copy_from_slice(enc_key);
140        mac.copy_from_slice(mac_key);
141        Self { key }
142    }
143
144    /// Returns the 64-byte composite key (`enc_key || mac_key`), which is the layout expected by
145    /// the [`Aes256CbcHmacSha256`](crate::hazmat::symmetric_encryption::Aes256CbcHmacSha256) and
146    /// [`Aes256CbcHmacSha256Aead`](crate::hazmat::symmetric_encryption::Aes256CbcHmacSha256Aead)
147    /// ciphers.
148    pub(crate) fn as_composite_key(&self) -> &[u8; AES256_CBC_HMAC_KEY_SIZE] {
149        &self.key.0
150    }
151
152    /// Returns the composite key as a slice. This is also this key's legacy byte serialization.
153    pub(crate) fn as_slice(&self) -> &[u8] {
154        self.key.as_slice()
155    }
156
157    /// Returns a zero-copy view of the 256-bit encryption key.
158    pub(crate) fn enc_key(&self) -> &[u8; AES256_CBC_HMAC_ENC_KEY_SIZE] {
159        let (enc_key, _) = self.key.split_at(AES256_CBC_HMAC_ENC_KEY_SIZE);
160        enc_key
161            .try_into()
162            .expect("first half of a 64-byte key is always 32 bytes")
163    }
164
165    /// Returns a zero-copy view of the 256-bit MAC key.
166    pub(crate) fn mac_key(&self) -> &[u8; AES256_CBC_HMAC_MAC_KEY_SIZE] {
167        let (_, mac_key) = self.key.split_at(AES256_CBC_HMAC_ENC_KEY_SIZE);
168        mac_key
169            .try_into()
170            .expect("second half of a 64-byte key is always 32 bytes")
171    }
172
173    /// Returns the key ID of this key.
174    ///
175    /// Unlike the other COSE key types, this key has no field to store a key ID in — it is
176    /// serialized as a bare 64-byte blob with no `kid`. The ID is therefore *derived*: it is the
177    /// key's own [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679) thumbprint truncated to
178    /// [`KEY_ID_SIZE`] bytes. Two keys with identical key material consequently share an ID, and a
179    /// key's ID is stable across serialization round-trips.
180    pub(crate) fn key_id(&self) -> KeyId {
181        let thumbprint = symmetric_key_thumbprint(self.as_slice());
182        let mut key_id = [0u8; KEY_ID_SIZE];
183        key_id.copy_from_slice(&thumbprint.as_bytes()[..KEY_ID_SIZE]);
184        KeyId::from(key_id)
185    }
186}
187
188/// Computes the [RFC 9679](https://www.rfc-editor.org/rfc/rfc9679) thumbprint of a COSE `Symmetric`
189/// key with the given `k` bytes. `kty` and `k` are the only required parameters for this key type,
190/// so they are the only ones hashed.
191fn symmetric_key_thumbprint(key_bytes: &[u8]) -> CoseKeyThumbprint {
192    thumbprint_from_required_params(vec![
193        (
194            KeyParameter::Kty.to_i64(),
195            Value::Integer(Integer::from(KeyType::Symmetric.to_i64())),
196        ),
197        (
198            SymmetricKeyParameter::K.to_i64(),
199            Value::Bytes(key_bytes.to_vec()),
200        ),
201    ])
202}
203
204impl PartialEq for Aes256CbcHmacKey {
205    fn eq(&self, other: &Self) -> bool {
206        self.ct_eq(other).into()
207    }
208}
209
210/// [XChaCha20Poly1305Key] is a symmetric encryption key consisting
211/// of one 256-bit key, and contains a key id. In contrast to the
212/// [Aes256CbcKey] and [Aes256CbcHmacKey], this key type is used to create
213/// CoseEncrypt0 messages.
214#[derive(Zeroize, Clone)]
215pub struct XChaCha20Poly1305Key {
216    pub(crate) key_id: KeyId,
217    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
218    /// Controls which key operations are allowed with this key. Note: Only checking decrypt is
219    /// implemented right now, and implementing is tracked here <https://bitwarden.atlassian.net/browse/PM-27513>.
220    /// Further, disabling decrypt will also disable unwrap. The only use-case so far is
221    /// `DataEnvelope`.
222    #[zeroize(skip)]
223    pub(crate) supported_operations: Vec<KeyOperation>,
224}
225
226impl XChaCha20Poly1305Key {
227    /// Creates a new XChaCha20Poly1305Key with a securely sampled cryptographic key and key id.
228    pub fn make() -> Self {
229        let mut rng = bitwarden_random::rng();
230        let mut enc_key = Box::pin(Array::<u8, U32>::default());
231        rng.fill(enc_key.as_mut_slice());
232        let key_id = KeyId::make();
233
234        Self {
235            enc_key,
236            key_id,
237            supported_operations: vec![
238                KeyOperation::Decrypt,
239                KeyOperation::Encrypt,
240                KeyOperation::WrapKey,
241                KeyOperation::UnwrapKey,
242            ],
243        }
244    }
245}
246
247impl ConstantTimeEq for XChaCha20Poly1305Key {
248    fn ct_eq(&self, other: &Self) -> Choice {
249        self.enc_key.ct_eq(&other.enc_key) & self.key_id.ct_eq(&other.key_id)
250    }
251}
252
253impl PartialEq for XChaCha20Poly1305Key {
254    fn eq(&self, other: &Self) -> bool {
255        self.ct_eq(other).into()
256    }
257}
258
259/// [Aes256GcmKey] is a symmetric AEAD key consisting of one 256-bit key
260#[derive(Zeroize, Clone)]
261pub struct Aes256GcmKey {
262    pub(crate) key_id: KeyId,
263    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
264    /// Controls which key operations are allowed with this key. See
265    /// [`XChaCha20Poly1305Key::supported_operations`].
266    #[zeroize(skip)]
267    pub(crate) supported_operations: Vec<KeyOperation>,
268}
269
270impl Aes256GcmKey {
271    /// Creates a new Aes256GcmKey with a securely sampled cryptographic key and key id.
272    pub fn make() -> Self {
273        let mut rng = bitwarden_random::rng();
274        let mut enc_key = Box::pin(Array::<u8, U32>::default());
275        rng.fill(enc_key.as_mut_slice());
276        let key_id = KeyId::make();
277
278        Self {
279            enc_key,
280            key_id,
281            supported_operations: vec![
282                KeyOperation::Decrypt,
283                KeyOperation::Encrypt,
284                KeyOperation::WrapKey,
285                KeyOperation::UnwrapKey,
286            ],
287        }
288    }
289
290    pub(crate) fn disable_key_operation(&mut self, op: KeyOperation) -> &mut Self {
291        self.supported_operations.retain(|k| *k != op);
292        self
293    }
294}
295
296impl ConstantTimeEq for Aes256GcmKey {
297    fn ct_eq(&self, other: &Self) -> Choice {
298        self.enc_key.ct_eq(&other.enc_key) & self.key_id.ct_eq(&other.key_id)
299    }
300}
301
302impl PartialEq for Aes256GcmKey {
303    fn eq(&self, other: &Self) -> bool {
304        self.ct_eq(other).into()
305    }
306}
307
308/// An XAES-256-GCM key consisting of one 256-bit key and a key ID.
309#[derive(Zeroize, Clone)]
310pub struct XAes256GcmKey {
311    pub(crate) key_id: KeyId,
312    pub(crate) enc_key: Pin<Box<Array<u8, U32>>>,
313    #[zeroize(skip)]
314    pub(crate) supported_operations: Vec<KeyOperation>,
315}
316
317impl XAes256GcmKey {
318    /// Creates a new XAES-256-GCM key with securely sampled key bytes and key ID.
319    pub fn make() -> Self {
320        let mut rng = bitwarden_random::rng();
321        let mut enc_key = Box::pin(Array::<u8, U32>::default());
322        rng.fill(enc_key.as_mut_slice());
323
324        Self {
325            key_id: KeyId::make(),
326            enc_key,
327            supported_operations: vec![
328                KeyOperation::Decrypt,
329                KeyOperation::Encrypt,
330                KeyOperation::WrapKey,
331                KeyOperation::UnwrapKey,
332            ],
333        }
334    }
335}
336
337impl ConstantTimeEq for XAes256GcmKey {
338    fn ct_eq(&self, other: &Self) -> Choice {
339        self.enc_key.ct_eq(&other.enc_key) & self.key_id.ct_eq(&other.key_id)
340    }
341}
342
343impl PartialEq for XAes256GcmKey {
344    fn eq(&self, other: &Self) -> bool {
345        self.ct_eq(other).into()
346    }
347}
348
349/// A borrowed view over a symmetric key that can be used as a COSE content-encryption key.
350pub(crate) enum CoseKeyView<'a> {
351    Aes256Gcm(&'a Aes256GcmKey),
352    XChaCha20Poly1305(&'a XChaCha20Poly1305Key),
353    XAes256Gcm(&'a XAes256GcmKey),
354    Aes256CbcHmac(&'a Aes256CbcHmacKey),
355}
356
357impl CoseKeyView<'_> {
358    /// Returns the key ID of the viewed key.
359    ///
360    /// This is returned by value rather than by reference because
361    /// [`CoseKeyView::Aes256CbcHmac`] derives its ID from the key material instead of storing one;
362    /// see [`Aes256CbcHmacKey::key_id`].
363    pub(crate) fn key_id(&self) -> KeyId {
364        match self {
365            CoseKeyView::Aes256Gcm(k) => k.key_id.clone(),
366            CoseKeyView::XChaCha20Poly1305(k) => k.key_id.clone(),
367            CoseKeyView::XAes256Gcm(k) => k.key_id.clone(),
368            CoseKeyView::Aes256CbcHmac(k) => k.key_id(),
369        }
370    }
371
372    /// Returns the key material in the layout the corresponding [`algorithm`](Self::algorithm)
373    /// expects as its content-encryption key.
374    pub(crate) fn key_bytes(&self) -> &[u8] {
375        match self {
376            CoseKeyView::Aes256Gcm(k) => k.enc_key.as_slice(),
377            CoseKeyView::XChaCha20Poly1305(k) => k.enc_key.as_slice(),
378            CoseKeyView::XAes256Gcm(k) => k.enc_key.as_slice(),
379            CoseKeyView::Aes256CbcHmac(k) => k.as_slice(),
380        }
381    }
382
383    pub(crate) fn algorithm(&self) -> CoseContentEncryptionAlgorithm {
384        match self {
385            CoseKeyView::Aes256Gcm(_) => CoseContentEncryptionAlgorithm::Aes256Gcm,
386            CoseKeyView::XChaCha20Poly1305(_) => CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
387            CoseKeyView::XAes256Gcm(_) => CoseContentEncryptionAlgorithm::XAes256Gcm,
388            CoseKeyView::Aes256CbcHmac(_) => CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256,
389        }
390    }
391}
392
393/// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::EncString)
394#[derive(ZeroizeOnDrop, Clone)]
395pub enum SymmetricCryptoKey {
396    #[allow(missing_docs)]
397    Aes256CbcKey(Aes256CbcKey),
398    #[allow(missing_docs)]
399    Aes256CbcHmacKey(Aes256CbcHmacKey),
400    /// Data encrypted by XChaCha20Poly1305Key keys has type
401    /// [`Cose_Encrypt0_B64`](crate::EncString::Cose_Encrypt0_B64)
402    XChaCha20Poly1305Key(XChaCha20Poly1305Key),
403    /// FIPS-approved AES-256-GCM key, used as the content-encryption key for FIPS
404    /// [`DataEnvelope`](crate::safe::DataEnvelope)s. Encoded as a COSE key.
405    Aes256GcmKey(Aes256GcmKey),
406    /// Extended-nonce AES-256-GCM key. Encoded as a COSE key and used with
407    /// type-7 EncStrings.
408    XAes256GcmKey(XAes256GcmKey),
409}
410
411impl SymmetricCryptoKey {
412    // enc type 0 old static format
413    const AES256_CBC_KEY_LEN: usize = 32;
414    // enc type 2 old static format
415    const AES256_CBC_HMAC_KEY_LEN: usize = 64;
416
417    /// Generate a new random AES256_CBC [SymmetricCryptoKey]
418    ///
419    /// WARNING: This function should only be used with a proper cryptographic RNG. If you do not
420    /// have a good reason for using this function, use
421    /// [SymmetricCryptoKey::make_aes256_cbc_hmac_key] instead.
422    pub(crate) fn make_aes256_cbc_hmac_key_internal(mut rng: impl rand::CryptoRng) -> Self {
423        let mut key = Box::pin(Array::<u8, U64>::default());
424        // Drawn as two separate 32-byte fills, matching the order and sizes used before the two
425        // halves shared one buffer, so seeded generation stays byte-for-byte identical.
426        let (enc_key, mac_key) = key.split_at_mut(AES256_CBC_HMAC_ENC_KEY_SIZE);
427        rng.fill(enc_key);
428        rng.fill(mac_key);
429
430        Self::Aes256CbcHmacKey(Aes256CbcHmacKey { key })
431    }
432
433    /// Make a new [SymmetricCryptoKey] for the specified algorithm
434    pub fn make(algorithm: SymmetricKeyAlgorithm) -> Self {
435        match algorithm {
436            SymmetricKeyAlgorithm::Aes256CbcHmac => Self::make_aes256_cbc_hmac_key(),
437            SymmetricKeyAlgorithm::XChaCha20Poly1305 => Self::make_xchacha20_poly1305_key(),
438            SymmetricKeyAlgorithm::Aes256Gcm => Self::Aes256GcmKey(Aes256GcmKey::make()),
439            SymmetricKeyAlgorithm::XAes256Gcm => Self::XAes256GcmKey(XAes256GcmKey::make()),
440        }
441    }
442
443    /// Generate a new random AES256_CBC_HMAC [SymmetricCryptoKey]
444    pub(crate) fn make_aes256_cbc_hmac_key() -> Self {
445        let rng = bitwarden_random::rng();
446        Self::make_aes256_cbc_hmac_key_internal(rng)
447    }
448
449    /// Generate a new random XChaCha20Poly1305 [SymmetricCryptoKey]
450    pub(crate) fn make_xchacha20_poly1305_key() -> Self {
451        let mut rng = bitwarden_random::rng();
452        let mut enc_key = Box::pin(Array::<u8, U32>::default());
453        rng.fill(enc_key.as_mut_slice());
454        Self::XChaCha20Poly1305Key(XChaCha20Poly1305Key {
455            enc_key,
456            key_id: KeyId::make(),
457            supported_operations: vec![
458                KeyOperation::Decrypt,
459                KeyOperation::Encrypt,
460                KeyOperation::WrapKey,
461                KeyOperation::UnwrapKey,
462            ],
463        })
464    }
465
466    /// Encodes the key to a byte array representation, that is separated by size.
467    /// [SymmetricCryptoKey::Aes256CbcHmacKey] and [SymmetricCryptoKey::Aes256CbcKey] are
468    /// encoded as 64 and 32 bytes respectively. COSE-serialized variants are encoded as at least
469    /// 65 bytes using padding.
470    ///
471    /// This can be used for storage and transmission in the old byte array format.
472    /// When the wrapping key is a COSE key, and the wrapped key is a COSE key, then this should
473    /// not use the byte representation but instead use the COSE key representation.
474    pub fn to_encoded(&self) -> BitwardenLegacyKeyBytes {
475        let encoded_key = self.to_encoded_raw();
476        match encoded_key {
477            EncodedSymmetricKey::BitwardenLegacyKey(_) => {
478                let encoded_key: Vec<u8> = encoded_key.into();
479                BitwardenLegacyKeyBytes::from(encoded_key)
480            }
481            EncodedSymmetricKey::CoseKey(_) => {
482                let mut encoded_key: Vec<u8> = encoded_key.into();
483                pad_key(&mut encoded_key, (Self::AES256_CBC_HMAC_KEY_LEN + 1) as u8); // This is less than 255
484                BitwardenLegacyKeyBytes::from(encoded_key)
485            }
486        }
487    }
488
489    /// Generate a new random [SymmetricCryptoKey] for unit tests. Note: DO NOT USE THIS
490    /// IN PRODUCTION CODE.
491    #[cfg(test)]
492    pub fn generate_seeded_for_unit_tests(seed: &str) -> Self {
493        // Keep this separate from the other generate function to not break test vectors.
494        let mut seeded_rng = ChaChaRng::from_seed(sha2::Sha256::digest(seed.as_bytes()).into());
495        let mut key = Box::pin(Array::<u8, U64>::default());
496        // Two separate 32-byte fills, as above, so the generated key is unchanged.
497        let (enc_key, mac_key) = key.split_at_mut(AES256_CBC_HMAC_ENC_KEY_SIZE);
498        seeded_rng.fill(enc_key);
499        seeded_rng.fill(mac_key);
500
501        SymmetricCryptoKey::Aes256CbcHmacKey(Aes256CbcHmacKey { key })
502    }
503
504    /// Creates the byte representation of the key, without any padding. This should not
505    /// be used directly for creating serialized key representations, instead,
506    /// [SymmetricCryptoKey::to_encoded] should be used.
507    ///
508    /// [SymmetricCryptoKey::Aes256CbcHmacKey] and
509    /// [SymmetricCryptoKey::Aes256CbcKey] are encoded as 64 and 32 byte arrays
510    /// respectively, representing the key bytes directly. XChaCha20-Poly1305,
511    /// AES-256-GCM, and XAES-256-GCM keys are encoded as serialized COSE keys.
512    /// A COSE key can be directly encrypted with a COSE key content format or
513    /// represented as a byte array. When represented as a byte array, the array
514    /// is padded to be larger than the byte array representation of the other
515    /// aforementioned key types.
516    pub(crate) fn to_encoded_raw(&self) -> EncodedSymmetricKey {
517        match self {
518            Self::Aes256CbcKey(key) => {
519                EncodedSymmetricKey::BitwardenLegacyKey(key.enc_key.to_vec().into())
520            }
521            Self::Aes256CbcHmacKey(key) => {
522                EncodedSymmetricKey::BitwardenLegacyKey(key.as_slice().to_vec().into())
523            }
524            Self::XChaCha20Poly1305Key(key) => {
525                let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec());
526                let mut cose_key = builder.key_id((&key.key_id).into());
527                for op in &key.supported_operations {
528                    cose_key = cose_key.add_key_op(*op);
529                }
530                let mut cose_key = cose_key.build();
531                cose_key.alg = Some(RegisteredLabelWithPrivate::PrivateUse(
532                    cose::XCHACHA20_POLY1305,
533                ));
534                EncodedSymmetricKey::CoseKey(
535                    cose_key
536                        .to_vec()
537                        .expect("cose key serialization should not fail")
538                        .into(),
539                )
540            }
541            Self::XAes256GcmKey(key) => {
542                let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec());
543                let mut cose_key = builder.key_id((&key.key_id).into());
544                for op in &key.supported_operations {
545                    cose_key = cose_key.add_key_op(*op);
546                }
547                let mut cose_key = cose_key.build();
548                cose_key.alg = Some(RegisteredLabelWithPrivate::PrivateUse(cose::XAES_256_GCM));
549                EncodedSymmetricKey::CoseKey(
550                    cose_key
551                        .to_vec()
552                        .expect("cose key serialization should not fail")
553                        .into(),
554                )
555            }
556            Self::Aes256GcmKey(key) => {
557                let builder = coset::CoseKeyBuilder::new_symmetric_key(key.enc_key.to_vec());
558                let mut cose_key = builder.key_id((&key.key_id).into());
559                for op in &key.supported_operations {
560                    cose_key = cose_key.add_key_op(*op);
561                }
562                let mut cose_key = cose_key.build();
563                cose_key.alg = Some(RegisteredLabelWithPrivate::Assigned(
564                    coset::iana::Algorithm::A256GCM,
565                ));
566                EncodedSymmetricKey::CoseKey(
567                    cose_key
568                        .to_vec()
569                        .expect("cose key serialization should not fail")
570                        .into(),
571                )
572            }
573        }
574    }
575
576    pub(crate) fn try_from_cose(serialized_key: &[u8]) -> Result<Self, CryptoError> {
577        let cose_key =
578            coset::CoseKey::from_slice(serialized_key).map_err(|_| CryptoError::InvalidKey)?;
579        let key = SymmetricCryptoKey::try_from(&cose_key)?;
580        Ok(key)
581    }
582
583    #[allow(missing_docs)]
584    pub fn to_base64(&self) -> B64 {
585        B64::from(self.to_encoded().as_ref())
586    }
587
588    /// Returns the key ID of the key, if it has one. Every authenticated key variant has a key ID;
589    /// only the unauthenticated [`SymmetricCryptoKey::Aes256CbcKey`] does not.
590    ///
591    /// [`SymmetricCryptoKey::Aes256CbcHmacKey`] has no field to store a key ID in, and derives one
592    /// from its key material instead: its RFC 9679 thumbprint, truncated to the key ID length. Two
593    /// such keys with identical key material therefore share an ID.
594    pub fn key_id(&self) -> Option<KeyId> {
595        match self {
596            Self::Aes256CbcKey(_) => None,
597            Self::Aes256CbcHmacKey(key) => Some(key.key_id()),
598            Self::XChaCha20Poly1305Key(key) => Some(key.key_id.clone()),
599            Self::Aes256GcmKey(key) => Some(key.key_id.clone()),
600            Self::XAes256GcmKey(key) => Some(key.key_id.clone()),
601        }
602    }
603
604    /// Returns a [`CoseKeyView`] for every symmetric key variant usable as a COSE
605    /// content-encryption key. Only the unauthenticated [`SymmetricCryptoKey::Aes256CbcKey`]
606    /// returns `None`.
607    pub(crate) fn as_cose_key_view(&self) -> Option<CoseKeyView<'_>> {
608        match self {
609            Self::Aes256GcmKey(k) => Some(CoseKeyView::Aes256Gcm(k)),
610            Self::XChaCha20Poly1305Key(k) => Some(CoseKeyView::XChaCha20Poly1305(k)),
611            Self::XAes256GcmKey(k) => Some(CoseKeyView::XAes256Gcm(k)),
612            Self::Aes256CbcHmacKey(k) => Some(CoseKeyView::Aes256CbcHmac(k)),
613            Self::Aes256CbcKey(_) => None,
614        }
615    }
616}
617
618impl CoseKeyThumbprintExt for SymmetricCryptoKey {
619    /// Computes the RFC 9679 thumbprint of this symmetric key.
620    ///
621    /// Returns an error for the unauthenticated legacy AES-CBC key, which is not representable as
622    /// a COSE key.
623    fn thumbprint(&self) -> Result<CoseKeyThumbprint, CryptoError> {
624        let view = self
625            .as_cose_key_view()
626            .ok_or(EncodingError::UnsupportedValue(
627                "unauthenticated AES-CBC keys are not COSE keys and have no thumbprint",
628            ))?;
629        Ok(symmetric_key_thumbprint(view.key_bytes()))
630    }
631}
632
633impl ConstantTimeEq for SymmetricCryptoKey {
634    /// Note: This is constant time with respect to comparing two keys of the same type, but not
635    /// constant type with respect to the fact that different keys are compared. If two types of
636    /// different keys are compared, then this does have different timing.
637    fn ct_eq(&self, other: &SymmetricCryptoKey) -> Choice {
638        use SymmetricCryptoKey::*;
639        match (self, other) {
640            (Aes256CbcKey(a), Aes256CbcKey(b)) => a.ct_eq(b),
641            (Aes256CbcKey(_), _) => Choice::from(0),
642
643            (Aes256CbcHmacKey(a), Aes256CbcHmacKey(b)) => a.ct_eq(b),
644            (Aes256CbcHmacKey(_), _) => Choice::from(0),
645
646            (XChaCha20Poly1305Key(a), XChaCha20Poly1305Key(b)) => a.ct_eq(b),
647            (XChaCha20Poly1305Key(_), _) => Choice::from(0),
648
649            (Aes256GcmKey(a), Aes256GcmKey(b)) => a.ct_eq(b),
650            (Aes256GcmKey(_), _) => Choice::from(0),
651
652            (XAes256GcmKey(a), XAes256GcmKey(b)) => a.ct_eq(b),
653            (XAes256GcmKey(_), _) => Choice::from(0),
654        }
655    }
656}
657
658impl PartialEq for SymmetricCryptoKey {
659    fn eq(&self, other: &Self) -> bool {
660        self.ct_eq(other).into()
661    }
662}
663
664impl TryFrom<String> for SymmetricCryptoKey {
665    type Error = CryptoError;
666
667    fn try_from(value: String) -> Result<Self, Self::Error> {
668        let bytes = B64::try_from(value).map_err(|_| CryptoError::InvalidKey)?;
669        Self::try_from(bytes)
670    }
671}
672
673impl TryFrom<B64> for SymmetricCryptoKey {
674    type Error = CryptoError;
675
676    fn try_from(value: B64) -> Result<Self, Self::Error> {
677        Self::try_from(&BitwardenLegacyKeyBytes::from(&value))
678    }
679}
680
681impl TryFrom<&BitwardenLegacyKeyBytes> for SymmetricCryptoKey {
682    type Error = CryptoError;
683
684    fn try_from(value: &BitwardenLegacyKeyBytes) -> Result<Self, Self::Error> {
685        let slice = value.as_ref();
686
687        // Raw byte serialized keys are either 32, 64, or more bytes long. If they are 32/64, they
688        // are the raw serializations of the AES256-CBC, and AES256-CBC-HMAC keys. If they
689        // are longer, they are COSE keys. The COSE keys are padded to the minimum length of
690        // 65 bytes, when serialized to raw byte arrays.
691
692        if slice.len() == Self::AES256_CBC_HMAC_KEY_LEN || slice.len() == Self::AES256_CBC_KEY_LEN {
693            Self::try_from(EncodedSymmetricKey::BitwardenLegacyKey(value.clone()))
694        } else if slice.len() > Self::AES256_CBC_HMAC_KEY_LEN {
695            let unpadded_value = unpad_key(slice)?;
696            Ok(Self::try_from_cose(unpadded_value)?)
697        } else {
698            Err(CryptoError::InvalidKeyLen)
699        }
700    }
701}
702
703impl TryFrom<EncodedSymmetricKey> for SymmetricCryptoKey {
704    type Error = CryptoError;
705
706    fn try_from(value: EncodedSymmetricKey) -> Result<Self, Self::Error> {
707        match value {
708            EncodedSymmetricKey::BitwardenLegacyKey(key)
709                if key.as_ref().len() == Self::AES256_CBC_KEY_LEN =>
710            {
711                let mut enc_key = Box::pin(Array::<u8, U32>::default());
712                enc_key.copy_from_slice(&key.as_ref()[..Self::AES256_CBC_KEY_LEN]);
713                Ok(Self::Aes256CbcKey(Aes256CbcKey { enc_key }))
714            }
715            EncodedSymmetricKey::BitwardenLegacyKey(key)
716                if key.as_ref().len() == Self::AES256_CBC_HMAC_KEY_LEN =>
717            {
718                // The legacy byte serialization is defined as `enc_key || mac_key`
719                let mut composite_key = Box::pin(Array::<u8, U64>::default());
720                composite_key.copy_from_slice(key.as_ref());
721
722                Ok(Self::Aes256CbcHmacKey(Aes256CbcHmacKey {
723                    key: composite_key,
724                }))
725            }
726            EncodedSymmetricKey::CoseKey(key) => Self::try_from_cose(key.as_ref()),
727            _ => Err(CryptoError::InvalidKey),
728        }
729    }
730}
731
732impl CryptoKey for SymmetricCryptoKey {}
733
734// We manually implement these to make sure we don't print any sensitive data
735impl std::fmt::Debug for SymmetricCryptoKey {
736    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737        match self {
738            SymmetricCryptoKey::Aes256CbcKey(key) => key.fmt(f),
739            SymmetricCryptoKey::Aes256CbcHmacKey(key) => key.fmt(f),
740            SymmetricCryptoKey::XChaCha20Poly1305Key(key) => key.fmt(f),
741            SymmetricCryptoKey::Aes256GcmKey(key) => key.fmt(f),
742            SymmetricCryptoKey::XAes256GcmKey(key) => key.fmt(f),
743        }
744    }
745}
746
747impl std::fmt::Debug for Aes256CbcKey {
748    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
749        let mut debug_struct = f.debug_struct("SymmetricKey::Aes256Cbc");
750        #[cfg(feature = "dangerous-crypto-debug")]
751        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
752        debug_struct.finish()
753    }
754}
755
756impl std::fmt::Debug for Aes256CbcHmacKey {
757    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758        let mut debug_struct = f.debug_struct("SymmetricKey::Aes256CbcHmac");
759        #[cfg(feature = "dangerous-crypto-debug")]
760        debug_struct
761            .field("enc_key", &hex::encode(self.enc_key()))
762            .field("mac_key", &hex::encode(self.mac_key()));
763        debug_struct.finish()
764    }
765}
766
767impl std::fmt::Debug for XChaCha20Poly1305Key {
768    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
769        let mut debug_struct = f.debug_struct("SymmetricKey::XChaCha20Poly1305");
770        debug_struct.field("key_id", &self.key_id);
771        debug_struct.field(
772            "supported_operations",
773            &self
774                .supported_operations
775                .iter()
776                .map(|key_operation: &KeyOperation| cose::debug_key_operation(*key_operation))
777                .collect::<Vec<_>>(),
778        );
779        #[cfg(feature = "dangerous-crypto-debug")]
780        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
781        debug_struct.finish()
782    }
783}
784
785impl std::fmt::Debug for XAes256GcmKey {
786    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787        let mut debug_struct = f.debug_struct("SymmetricKey::XAes256Gcm");
788        debug_struct.field("key_id", &self.key_id);
789        debug_struct.field(
790            "supported_operations",
791            &self
792                .supported_operations
793                .iter()
794                .map(|key_operation: &KeyOperation| cose::debug_key_operation(*key_operation))
795                .collect::<Vec<_>>(),
796        );
797        #[cfg(feature = "dangerous-crypto-debug")]
798        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
799        debug_struct.finish()
800    }
801}
802
803impl std::fmt::Debug for Aes256GcmKey {
804    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805        let mut debug_struct = f.debug_struct("SymmetricKey::Aes256Gcm");
806        debug_struct.field("key_id", &self.key_id);
807        debug_struct.field(
808            "supported_operations",
809            &self
810                .supported_operations
811                .iter()
812                .map(|key_operation: &KeyOperation| cose::debug_key_operation(*key_operation))
813                .collect::<Vec<_>>(),
814        );
815        #[cfg(feature = "dangerous-crypto-debug")]
816        debug_struct.field("key", &hex::encode(self.enc_key.as_slice()));
817        debug_struct.finish()
818    }
819}
820
821/// Pad a key to a minimum length using PKCS7-like padding.
822/// The last N bytes of the padded bytes all have the value N.
823/// For example, padded to size 4, the value 0,0 becomes 0,0,2,2.
824///
825/// COSE-serialized keys must be distinguishable from
826/// [SymmetricCryptoKey::Aes256CbcHmacKey] keys when encoded as byte arrays with no additional
827/// content format. Padding ensures that the byte representation uniquely separates keys by size.
828/// The previous key types [SymmetricCryptoKey::Aes256CbcHmacKey] and
829/// [SymmetricCryptoKey::Aes256CbcKey] are 64 and 32 bytes long respectively.
830fn pad_key(key_bytes: &mut Vec<u8>, min_length: u8) {
831    crate::keys::utils::pad_bytes(key_bytes, min_length as usize)
832        .expect("Padding cannot fail since the min_length is < 255")
833}
834
835/// Unpad a key that is padded using the PKCS7-like padding defined by [pad_key].
836/// The last N bytes of the padded bytes all have the value N.
837/// For example, padded to size 4, the value 0,0 becomes 0,0,2,2.
838///
839/// COSE-serialized keys must be distinguishable from
840/// [SymmetricCryptoKey::Aes256CbcHmacKey] keys when encoded as byte arrays with no additional
841/// content format. Padding ensures that the byte representation uniquely separates keys by size.
842/// The previous key types [SymmetricCryptoKey::Aes256CbcHmacKey] and
843/// [SymmetricCryptoKey::Aes256CbcKey] are 64 and 32 bytes long respectively.
844fn unpad_key(key_bytes: &[u8]) -> Result<&[u8], CryptoError> {
845    crate::keys::utils::unpad_bytes(key_bytes).map_err(|_| CryptoError::InvalidKey)
846}
847
848/// Encoded representation of [SymmetricCryptoKey]
849pub enum EncodedSymmetricKey {
850    /// An Aes256-CBC-HMAC key, or a Aes256-CBC key
851    BitwardenLegacyKey(BitwardenLegacyKeyBytes),
852    /// A symmetric key encoded as a COSE key
853    CoseKey(CoseKeyBytes),
854}
855impl From<EncodedSymmetricKey> for Vec<u8> {
856    fn from(val: EncodedSymmetricKey) -> Self {
857        match val {
858            EncodedSymmetricKey::BitwardenLegacyKey(key) => key.to_vec(),
859            EncodedSymmetricKey::CoseKey(key) => key.to_vec(),
860        }
861    }
862}
863impl EncodedSymmetricKey {
864    /// Returns the content format of the encoded symmetric key.
865    #[allow(private_interfaces)]
866    pub fn content_format(&self) -> ContentFormat {
867        match self {
868            EncodedSymmetricKey::BitwardenLegacyKey(_) => ContentFormat::BitwardenLegacyKey,
869            EncodedSymmetricKey::CoseKey(_) => ContentFormat::CoseKey,
870        }
871    }
872}
873
874// Note: Deserialize and Serialize are only implemented until external usages of
875// symmetric crypto keys are removed. We do not want to support these, but while
876// these have to be supported, we want to have type-safety over having raw byte
877// arrays.
878impl<'de> Deserialize<'de> for SymmetricCryptoKey {
879    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
880    where
881        D: serde::Deserializer<'de>,
882    {
883        deserializer.deserialize_str(FromStrVisitor::new())
884    }
885}
886
887impl FromStr for SymmetricCryptoKey {
888    type Err = CryptoError;
889
890    fn from_str(s: &str) -> Result<Self, Self::Err> {
891        let bytes = B64::try_from(s.to_string()).map_err(|_| CryptoError::InvalidKey)?;
892        Self::try_from(bytes).map_err(|_| CryptoError::InvalidKey)
893    }
894}
895
896impl Serialize for SymmetricCryptoKey {
897    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
898    where
899        S: serde::Serializer,
900    {
901        serializer.serialize_str(&self.to_base64().to_string())
902    }
903}
904
905/// Test only helper for deriving a symmetric key.
906#[cfg(test)]
907pub fn derive_symmetric_key(name: &str) -> Aes256CbcHmacKey {
908    use zeroize::Zeroizing;
909
910    use crate::{derive_shareable_key, generate_random_bytes};
911
912    let secret: Zeroizing<[u8; 16]> = generate_random_bytes();
913    derive_shareable_key(secret, name, None)
914}
915
916#[cfg(test)]
917mod tests {
918    use bitwarden_encoding::B64;
919    use coset::{CborSerializable, iana::KeyOperation};
920    use hybrid_array::Array;
921    use typenum::U32;
922
923    use super::{
924        AES256_CBC_HMAC_ENC_KEY_SIZE, EncodedSymmetricKey, KEY_ID_SIZE, SymmetricCryptoKey,
925        derive_symmetric_key,
926    };
927    use crate::{
928        Aes256CbcHmacKey, Aes256CbcKey, BitwardenLegacyKeyBytes, CoseKeyThumbprintExt,
929        SymmetricKeyAlgorithm, XAes256GcmKey, XChaCha20Poly1305Key,
930        keys::{
931            KeyId,
932            symmetric_crypto_key::{pad_key, unpad_key},
933        },
934    };
935
936    #[test]
937    #[ignore = "Manual test to verify debug format"]
938    fn test_key_debug() {
939        let aes_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
940        println!("{:?}", aes_key);
941        let xchacha_key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
942        println!("{:?}", xchacha_key);
943    }
944
945    #[test]
946    fn test_serialize_deserialize_symmetric_crypto_key() {
947        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
948        let serialized = serde_json::to_string(&key).unwrap();
949        let deserialized: SymmetricCryptoKey = serde_json::from_str(&serialized).unwrap();
950        assert_eq!(key, deserialized);
951    }
952
953    #[test]
954    fn test_symmetric_crypto_key() {
955        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
956        let key2 = SymmetricCryptoKey::try_from(key.to_base64()).unwrap();
957
958        assert_eq!(key, key2);
959
960        let key = "UY4B5N4DA4UisCNClgZtRr6VLy9ZF5BXXC7cDZRqourKi4ghEMgISbCsubvgCkHf5DZctQjVot11/vVvN9NNHQ==".to_string();
961        let key2 = SymmetricCryptoKey::try_from(key.clone()).unwrap();
962        assert_eq!(key, key2.to_base64().to_string());
963    }
964
965    #[test]
966    fn test_encode_decode_old_symmetric_crypto_key() {
967        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
968        let encoded = key.to_encoded();
969        let decoded = SymmetricCryptoKey::try_from(&encoded).unwrap();
970        assert_eq!(key, decoded);
971    }
972
973    #[test]
974    fn test_decode_new_symmetric_crypto_key() {
975        let key: B64 = ("pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB").parse()
976        .unwrap();
977        let key = BitwardenLegacyKeyBytes::from(&key);
978        let key = SymmetricCryptoKey::try_from(&key).unwrap();
979        match key {
980            SymmetricCryptoKey::XChaCha20Poly1305Key(_) => (),
981            _ => panic!("Invalid key type"),
982        }
983    }
984
985    #[test]
986    fn test_encode_xchacha20_poly1305_key() {
987        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
988        let encoded = key.to_encoded();
989        let decoded = SymmetricCryptoKey::try_from(&encoded).unwrap();
990        assert_eq!(key, decoded);
991    }
992
993    #[test]
994    fn test_pad_unpad_key_63() {
995        let original_key = vec![1u8; 63];
996        let mut key_bytes = original_key.clone();
997        let mut encoded_bytes = vec![1u8; 65];
998        encoded_bytes[63] = 2;
999        encoded_bytes[64] = 2;
1000        pad_key(&mut key_bytes, 65);
1001        assert_eq!(encoded_bytes, key_bytes);
1002        let unpadded_key = unpad_key(&key_bytes).unwrap();
1003        assert_eq!(original_key, unpadded_key);
1004    }
1005
1006    #[test]
1007    fn test_pad_unpad_key_64() {
1008        let original_key = vec![1u8; 64];
1009        let mut key_bytes = original_key.clone();
1010        let mut encoded_bytes = vec![1u8; 65];
1011        encoded_bytes[64] = 1;
1012        pad_key(&mut key_bytes, 65);
1013        assert_eq!(encoded_bytes, key_bytes);
1014        let unpadded_key = unpad_key(&key_bytes).unwrap();
1015        assert_eq!(original_key, unpadded_key);
1016    }
1017
1018    #[test]
1019    fn test_pad_unpad_key_65() {
1020        let original_key = vec![1u8; 65];
1021        let mut key_bytes = original_key.clone();
1022        let mut encoded_bytes = vec![1u8; 66];
1023        encoded_bytes[65] = 1;
1024        pad_key(&mut key_bytes, 65);
1025        assert_eq!(encoded_bytes, key_bytes);
1026        let unpadded_key = unpad_key(&key_bytes).unwrap();
1027        assert_eq!(original_key, unpadded_key);
1028    }
1029
1030    #[test]
1031    fn test_eq_aes_cbc_hmac() {
1032        let key1 = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1033        let key2 = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1034        assert_ne!(key1, key2);
1035        let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap();
1036        assert_eq!(key1, key3);
1037    }
1038
1039    #[test]
1040    fn test_eq_aes_cbc() {
1041        let key1 =
1042            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(vec![1u8; 32])).unwrap();
1043        let key2 =
1044            SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(vec![2u8; 32])).unwrap();
1045        assert_ne!(key1, key2);
1046        let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap();
1047        assert_eq!(key1, key3);
1048    }
1049
1050    #[test]
1051    fn test_eq_xchacha20_poly1305() {
1052        let key1 = SymmetricCryptoKey::make_xchacha20_poly1305_key();
1053        let key2 = SymmetricCryptoKey::make_xchacha20_poly1305_key();
1054        assert_ne!(key1, key2);
1055        let key3 = SymmetricCryptoKey::try_from(key1.to_base64()).unwrap();
1056        assert_eq!(key1, key3);
1057    }
1058
1059    #[test]
1060    fn test_neq_different_key_types() {
1061        let key1 = SymmetricCryptoKey::Aes256CbcKey(Aes256CbcKey {
1062            enc_key: Box::pin(Array::<u8, U32>::default()),
1063        });
1064        let key2 = SymmetricCryptoKey::XChaCha20Poly1305Key(XChaCha20Poly1305Key {
1065            enc_key: Box::pin(Array::<u8, U32>::default()),
1066            key_id: KeyId::from([0; 16]),
1067            supported_operations: vec![
1068                KeyOperation::Decrypt,
1069                KeyOperation::Encrypt,
1070                KeyOperation::WrapKey,
1071                KeyOperation::UnwrapKey,
1072            ],
1073        });
1074        assert_ne!(key1, key2);
1075    }
1076
1077    #[test]
1078    fn test_eq_variant_aes256_cbc() {
1079        let key1 = Aes256CbcKey {
1080            enc_key: Box::pin(Array::from([1u8; 32])),
1081        };
1082        let key2 = Aes256CbcKey {
1083            enc_key: Box::pin(Array::from([1u8; 32])),
1084        };
1085        let key3 = Aes256CbcKey {
1086            enc_key: Box::pin(Array::from([2u8; 32])),
1087        };
1088        assert_eq!(key1, key2);
1089        assert_ne!(key1, key3);
1090    }
1091
1092    #[test]
1093    fn test_eq_variant_aes256_cbc_hmac() {
1094        let key1 = Aes256CbcHmacKey::new(&[1u8; 32], &[2u8; 32]);
1095        let key2 = Aes256CbcHmacKey::new(&[1u8; 32], &[2u8; 32]);
1096        let key3 = Aes256CbcHmacKey::new(&[3u8; 32], &[4u8; 32]);
1097        assert_eq!(key1, key2);
1098        assert_ne!(key1, key3);
1099    }
1100
1101    #[test]
1102    fn test_eq_variant_xchacha20_poly1305() {
1103        let key1 = XChaCha20Poly1305Key {
1104            enc_key: Box::pin(Array::from([1u8; 32])),
1105            key_id: KeyId::from([0; 16]),
1106            supported_operations: vec![
1107                KeyOperation::Decrypt,
1108                KeyOperation::Encrypt,
1109                KeyOperation::WrapKey,
1110                KeyOperation::UnwrapKey,
1111            ],
1112        };
1113        let key2 = XChaCha20Poly1305Key {
1114            enc_key: Box::pin(Array::from([1u8; 32])),
1115            key_id: KeyId::from([0; 16]),
1116            supported_operations: vec![
1117                KeyOperation::Decrypt,
1118                KeyOperation::Encrypt,
1119                KeyOperation::WrapKey,
1120                KeyOperation::UnwrapKey,
1121            ],
1122        };
1123        let key3 = XChaCha20Poly1305Key {
1124            enc_key: Box::pin(Array::from([2u8; 32])),
1125            key_id: KeyId::from([1; 16]),
1126            supported_operations: vec![
1127                KeyOperation::Decrypt,
1128                KeyOperation::Encrypt,
1129                KeyOperation::WrapKey,
1130                KeyOperation::UnwrapKey,
1131            ],
1132        };
1133        assert_eq!(key1, key2);
1134        assert_ne!(key1, key3);
1135    }
1136
1137    fn fixed_xaes_key_inner() -> XAes256GcmKey {
1138        XAes256GcmKey {
1139            enc_key: Box::pin(Array::from([
1140                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
1141                0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
1142                0x1c, 0x1d, 0x1e, 0x1f,
1143            ])),
1144            key_id: KeyId::from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
1145            supported_operations: vec![KeyOperation::Encrypt, KeyOperation::Decrypt],
1146        }
1147    }
1148
1149    fn fixed_xaes_key() -> SymmetricCryptoKey {
1150        SymmetricCryptoKey::XAes256GcmKey(fixed_xaes_key_inner())
1151    }
1152
1153    #[test]
1154    fn test_make_xaes256_gcm_key() {
1155        assert!(matches!(
1156            SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XAes256Gcm),
1157            SymmetricCryptoKey::XAes256GcmKey(_)
1158        ));
1159    }
1160
1161    #[test]
1162    fn test_xaes256_gcm_encoding_roundtrips() {
1163        const PADDED_KEY: &str = "pQEEAlAAAQIDBAUGBwgJCgsMDQ4PAzoAARF5BIIDBCBYIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fAQ==";
1164
1165        let key = fixed_xaes_key();
1166        assert_eq!(key.to_base64().to_string(), PADDED_KEY);
1167        let padded = SymmetricCryptoKey::try_from(PADDED_KEY.to_owned()).unwrap();
1168        assert_eq!(padded, key);
1169        let SymmetricCryptoKey::XAes256GcmKey(ref padded) = padded else {
1170            panic!("expected XAES-256-GCM key");
1171        };
1172        assert_eq!(
1173            padded.supported_operations,
1174            [KeyOperation::Encrypt, KeyOperation::Decrypt]
1175        );
1176
1177        let EncodedSymmetricKey::CoseKey(raw) = key.to_encoded_raw() else {
1178            panic!("expected COSE key encoding");
1179        };
1180        assert_eq!(
1181            SymmetricCryptoKey::try_from_cose(raw.as_ref()).unwrap(),
1182            key
1183        );
1184
1185        let cose_key = coset::CoseKey::from_slice(raw.as_ref()).unwrap();
1186        assert_eq!(
1187            cose_key.alg,
1188            Some(coset::Algorithm::PrivateUse(crate::cose::XAES_256_GCM))
1189        );
1190        assert_eq!(cose_key.key_id, (0u8..16).collect::<Vec<_>>());
1191        assert_eq!(cose_key.key_ops.len(), 2);
1192        assert!(
1193            cose_key
1194                .key_ops
1195                .contains(&coset::RegisteredLabel::Assigned(KeyOperation::Encrypt))
1196        );
1197        assert!(
1198            cose_key
1199                .key_ops
1200                .contains(&coset::RegisteredLabel::Assigned(KeyOperation::Decrypt))
1201        );
1202    }
1203
1204    #[test]
1205    fn test_xaes256_gcm_equality() {
1206        let key = fixed_xaes_key();
1207        let same = fixed_xaes_key();
1208        assert_eq!(key, same);
1209
1210        let mut different_bytes = fixed_xaes_key_inner();
1211        different_bytes.enc_key[0] ^= 1;
1212        assert_ne!(key, SymmetricCryptoKey::XAes256GcmKey(different_bytes));
1213
1214        let mut different_id = fixed_xaes_key_inner();
1215        different_id.key_id = KeyId::from([1; 16]);
1216        assert_ne!(key, SymmetricCryptoKey::XAes256GcmKey(different_id));
1217
1218        for other in [
1219            SymmetricCryptoKey::Aes256CbcKey(Aes256CbcKey {
1220                enc_key: Box::pin(Array::default()),
1221            }),
1222            SymmetricCryptoKey::Aes256CbcHmacKey(Aes256CbcHmacKey::new(&[0u8; 32], &[0u8; 32])),
1223            SymmetricCryptoKey::Aes256GcmKey(crate::Aes256GcmKey::make()),
1224            SymmetricCryptoKey::XChaCha20Poly1305Key(XChaCha20Poly1305Key::make()),
1225        ] {
1226            assert_ne!(key, other);
1227        }
1228    }
1229
1230    #[test]
1231    fn test_neq_different_key_id() {
1232        let key1 = XChaCha20Poly1305Key {
1233            enc_key: Box::pin(Array::<u8, U32>::default()),
1234            key_id: KeyId::from([0; 16]),
1235            supported_operations: vec![
1236                KeyOperation::Decrypt,
1237                KeyOperation::Encrypt,
1238                KeyOperation::WrapKey,
1239                KeyOperation::UnwrapKey,
1240            ],
1241        };
1242        let key2 = XChaCha20Poly1305Key {
1243            enc_key: Box::pin(Array::<u8, U32>::default()),
1244            key_id: KeyId::from([1; 16]),
1245            supported_operations: vec![
1246                KeyOperation::Decrypt,
1247                KeyOperation::Encrypt,
1248                KeyOperation::WrapKey,
1249                KeyOperation::UnwrapKey,
1250            ],
1251        };
1252        assert_ne!(key1, key2);
1253
1254        let key1 = SymmetricCryptoKey::XChaCha20Poly1305Key(key1);
1255        let key2 = SymmetricCryptoKey::XChaCha20Poly1305Key(key2);
1256        assert_ne!(key1, key2);
1257    }
1258
1259    const AES256_GCM_KEY: &str =
1260        "pQEEAlACAgICAgICAgICAgICAgICAwMEhAMEBQYgWCABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=";
1261    const AES256_GCM_KEY_THUMBPRINT: &str =
1262        "3810c7275ee292caca13d938a057a94c75210087d960d3eb6868c0ffe99b5643";
1263
1264    const XCHACHA20_POLY1305_KEY: &str = "pQEEAlDib+JxbqMBlcd3KTUesbufAzoAARFvBIQDBAUGIFggt79surJXmqhPhYuuqi9ZyPfieebmtw2OsmN5SDrb4yUB";
1265    const XCHACHA20_POLY1305_KEY_THUMBPRINT: &str =
1266        "64aec2d09ef5ba8b310ef9a70346b03422443e295b6f045e38169ae97e579d85";
1267
1268    #[test]
1269    fn test_decode_new_aes256_gcm_key() {
1270        let key: B64 = AES256_GCM_KEY.parse().unwrap();
1271        let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&key)).unwrap();
1272        match key {
1273            SymmetricCryptoKey::Aes256GcmKey(_) => (),
1274            _ => panic!("Invalid key type"),
1275        }
1276    }
1277
1278    #[test]
1279    fn test_thumbprint_aes256_gcm_vector() {
1280        // A fixed AES-256-GCM COSE key.
1281        let key: B64 = AES256_GCM_KEY.parse().unwrap();
1282        let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&key)).unwrap();
1283        assert_eq!(
1284            key.thumbprint().unwrap().to_hex(),
1285            AES256_GCM_KEY_THUMBPRINT
1286        );
1287    }
1288
1289    #[test]
1290    fn test_thumbprint_xchacha20_poly1305_vector() {
1291        // A fixed XChaCha20Poly1305 COSE key.
1292        let key: B64 = XCHACHA20_POLY1305_KEY.parse().unwrap();
1293        let key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(&key)).unwrap();
1294        assert_eq!(
1295            key.thumbprint().unwrap().to_hex(),
1296            XCHACHA20_POLY1305_KEY_THUMBPRINT
1297        );
1298    }
1299
1300    #[test]
1301    fn test_thumbprint_is_deterministic() {
1302        let key = SymmetricCryptoKey::make_xchacha20_poly1305_key();
1303        assert_eq!(key.thumbprint().unwrap(), key.thumbprint().unwrap());
1304    }
1305
1306    /// The unauthenticated AES-CBC key is the only variant with no COSE key view, and so the only
1307    /// one without a thumbprint.
1308    #[test]
1309    fn test_thumbprint_errors_for_unauthenticated_aes_cbc() {
1310        let key = SymmetricCryptoKey::Aes256CbcKey(Aes256CbcKey {
1311            enc_key: Box::pin(Array::from([1u8; 32])),
1312        });
1313        assert!(key.thumbprint().is_err());
1314    }
1315
1316    const AES256_CBC_HMAC_KEY_THUMBPRINT: &str =
1317        "ac4ecffb2e087a59180d1dd19950b8e9e634997f47bbfd7a2bb829edf7a9f900";
1318    const AES256_CBC_HMAC_KEY_ID: &str = "ac4ecffb2e087a59180d1dd19950b8e9";
1319
1320    /// A fixed AES-256-CBC-HMAC key: `enc_key` is `0x00..0x1f`, `mac_key` is `0x20..0x3f`.
1321    fn fixed_aes_cbc_hmac_key_inner() -> Aes256CbcHmacKey {
1322        Aes256CbcHmacKey::new(
1323            &std::array::from_fn(|i| i as u8),
1324            &std::array::from_fn(|i| (i + 32) as u8),
1325        )
1326    }
1327
1328    fn fixed_aes_cbc_hmac_key() -> SymmetricCryptoKey {
1329        SymmetricCryptoKey::Aes256CbcHmacKey(fixed_aes_cbc_hmac_key_inner())
1330    }
1331
1332    #[test]
1333    #[ignore = "Generates test vectors; run manually"]
1334    fn generate_aes256_cbc_hmac_thumbprint_vectors() {
1335        let key = fixed_aes_cbc_hmac_key();
1336        println!(
1337            "const AES256_CBC_HMAC_KEY_THUMBPRINT: &str = \"{}\";",
1338            key.thumbprint().unwrap().to_hex()
1339        );
1340        println!(
1341            "const AES256_CBC_HMAC_KEY_ID: &str = \"{}\";",
1342            hex::encode(key.key_id().unwrap().as_slice())
1343        );
1344    }
1345
1346    /// Locks in the thumbprint of an AES-256-CBC-HMAC key: the RFC 9679 thumbprint of a COSE
1347    /// `Symmetric` key whose `k` is the 64-byte `enc_key || mac_key` composite. The derived key ID
1348    /// depends on this, so it must never change.
1349    #[test]
1350    fn test_thumbprint_aes256_cbc_hmac_vector() {
1351        assert_eq!(
1352            fixed_aes_cbc_hmac_key().thumbprint().unwrap().to_hex(),
1353            AES256_CBC_HMAC_KEY_THUMBPRINT
1354        );
1355    }
1356
1357    #[test]
1358    fn test_key_id_aes256_cbc_hmac_vector() {
1359        let key_id = fixed_aes_cbc_hmac_key().key_id().unwrap();
1360        assert_eq!(hex::encode(key_id.as_slice()), AES256_CBC_HMAC_KEY_ID);
1361    }
1362
1363    /// Locks in the derivation rule itself: the key ID is the leading `KEY_ID_SIZE` bytes of the
1364    /// key's thumbprint.
1365    #[test]
1366    fn test_key_id_is_thumbprint_prefix() {
1367        let key = fixed_aes_cbc_hmac_key();
1368        let thumbprint = key.thumbprint().unwrap();
1369        assert_eq!(
1370            key.key_id().unwrap().as_slice(),
1371            &thumbprint.as_bytes()[..KEY_ID_SIZE]
1372        );
1373    }
1374
1375    #[test]
1376    fn test_key_id_aes256_cbc_hmac_is_deterministic() {
1377        assert_eq!(
1378            fixed_aes_cbc_hmac_key().key_id(),
1379            fixed_aes_cbc_hmac_key().key_id()
1380        );
1381    }
1382
1383    /// Both halves of the key must feed the ID, or two keys differing only in their MAC key would
1384    /// collide.
1385    #[test]
1386    fn test_key_id_aes256_cbc_hmac_covers_both_halves() {
1387        let key = fixed_aes_cbc_hmac_key();
1388
1389        // Flip the first byte of the encryption half, then of the MAC half.
1390        let mut different_enc = fixed_aes_cbc_hmac_key_inner();
1391        different_enc.key[0] ^= 1;
1392        assert_ne!(
1393            key.key_id(),
1394            SymmetricCryptoKey::Aes256CbcHmacKey(different_enc).key_id()
1395        );
1396
1397        let mut different_mac = fixed_aes_cbc_hmac_key_inner();
1398        different_mac.key[AES256_CBC_HMAC_ENC_KEY_SIZE] ^= 1;
1399        assert_ne!(
1400            key.key_id(),
1401            SymmetricCryptoKey::Aes256CbcHmacKey(different_mac).key_id()
1402        );
1403    }
1404
1405    /// Having a COSE key view must not change how these keys are serialized: they remain the raw
1406    /// 64-byte legacy encoding. Changing this would break every existing user key.
1407    #[test]
1408    fn test_aes256_cbc_hmac_still_encodes_as_legacy_64_bytes() {
1409        let key = fixed_aes_cbc_hmac_key();
1410
1411        let EncodedSymmetricKey::BitwardenLegacyKey(raw) = key.to_encoded_raw() else {
1412            panic!("expected legacy key encoding");
1413        };
1414        assert_eq!(raw.as_ref().len(), 64);
1415        assert_eq!(raw.as_ref(), (0u8..64).collect::<Vec<_>>());
1416
1417        assert_eq!(key.to_encoded().as_ref().len(), 64);
1418        assert_eq!(SymmetricCryptoKey::try_from(key.to_base64()).unwrap(), key);
1419    }
1420}