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