Skip to main content

bitwarden_crypto/cose/
symmetric.rs

1//! COSE symmetric encryption — the middle layer of the three-layer stack:
2//! - Lowest: Hazmat primitive (`crate::hazmat::symmetric_encryption`)
3//! - Mid: COSE framing (this module)
4//! - High: Consumer (`crate::safe`, `EncString`)
5
6use coset::{
7    Algorithm, CborSerializable, CoseEncrypt, CoseEncrypt0, CoseEncrypt0Builder,
8    CoseEncryptBuilder, Header, HeaderBuilder, iana,
9};
10
11use super::{AES_256_CBC_HMAC_SHA256_AEAD, XAES_256_GCM, XCHACHA20_POLY1305};
12use crate::{
13    ContentFormat, CoseEncrypt0Bytes, CryptoError, XAes256GcmKey, XChaCha20Poly1305Key,
14    error::EncStringParseError,
15    hazmat::symmetric_encryption::{
16        Aead,
17        aes_gcm::{Aes256Gcm, Aes256GcmCiphertext, Aes256GcmNonce},
18        aes256_cbc_hmac_sha256_aead::{
19            Aes256CbcHmacSha256Aead, Aes256CbcHmacSha256AeadCiphertext,
20            Aes256CbcHmacSha256AeadNonce,
21        },
22        xaes_256_gcm::{XAes256Gcm, XAes256GcmCiphertext, XAes256GcmNonce},
23        xchacha20::{XChaCha20Poly1305, XChaCha20Poly1305Ciphertext, XChaCha20Poly1305Nonce},
24    },
25};
26
27const TEXT_PAD_BLOCK_SIZE: usize = 32;
28
29fn should_pad_content(format: &ContentFormat) -> bool {
30    matches!(format, ContentFormat::Utf8)
31}
32
33/// The content-encryption algorithms that can seal the body of a COSE message.
34///
35/// This selects which [`CoseEncryptCipher`] the free `encrypt_cose`/`encrypt_cose0` functions
36/// dispatch to. On decryption, [`CoseAlgorithmPolicy`] determines whether the protected algorithm
37/// is required, must match an independently typed CEK, or may be replaced by a legacy default.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub(crate) enum CoseContentEncryptionAlgorithm {
40    /// AES-256-GCM (COSE `A256GCM`).
41    Aes256Gcm,
42    /// XAES-256-GCM (private-use [`XAES_256_GCM`]).
43    XAes256Gcm,
44    /// XChaCha20-Poly1305 (private-use [`XCHACHA20_POLY1305`]).
45    XChaCha20Poly1305,
46    /// AES-256-CBC-HMAC-SHA256 Encrypt-then-MAC (private-use [`AES_256_CBC_HMAC_SHA256_AEAD`]).
47    Aes256CbcHmacSha256,
48}
49
50/// Policy for resolving and validating a COSE message's content-encryption algorithm.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub(crate) enum CoseAlgorithmPolicy {
53    /// Require a supported algorithm in the protected header.
54    RequireProtectedHeaderAlgorithm,
55    /// Require the protected algorithm to exactly match the independently known CEK algorithm.
56    Exactly(CoseContentEncryptionAlgorithm),
57    /// Use the protected algorithm when present, or the supplied default for a legacy message.
58    ProtectedHeaderAlgorithmOrLegacyDefault(CoseContentEncryptionAlgorithm),
59}
60
61impl TryFrom<&Algorithm> for CoseContentEncryptionAlgorithm {
62    type Error = CryptoError;
63
64    fn try_from(algorithm: &Algorithm) -> Result<Self, Self::Error> {
65        match algorithm {
66            Algorithm::Assigned(iana::Algorithm::A256GCM) => Ok(Self::Aes256Gcm),
67            Algorithm::PrivateUse(XAES_256_GCM) => Ok(Self::XAes256Gcm),
68            Algorithm::PrivateUse(XCHACHA20_POLY1305) => Ok(Self::XChaCha20Poly1305),
69            Algorithm::PrivateUse(AES_256_CBC_HMAC_SHA256_AEAD) => Ok(Self::Aes256CbcHmacSha256),
70            _ => Err(CryptoError::WrongKeyType),
71        }
72    }
73}
74
75/// Resolves the content-encryption algorithm according to `policy`.
76///
77/// Some legacy envelopes
78/// (notably early [`PasswordProtectedKeyEnvelope`](crate::safe::PasswordProtectedKeyEnvelope)s)
79/// were sealed without declaring the content-encryption algorithm in their protected header.
80/// Only the legacy-default policy permits a missing protected algorithm. An exact policy rejects a
81/// supported but different algorithm before the CEK is converted or used.
82fn algorithm_from_header(
83    header: &Header,
84    policy: CoseAlgorithmPolicy,
85) -> Result<CoseContentEncryptionAlgorithm, CryptoError> {
86    let declared = header
87        .alg
88        .as_ref()
89        .map(CoseContentEncryptionAlgorithm::try_from)
90        .transpose()?;
91
92    match (policy, declared) {
93        (CoseAlgorithmPolicy::ProtectedHeaderAlgorithmOrLegacyDefault(default), None) => {
94            Ok(default)
95        }
96        (_, None) => Err(CryptoError::EncString(
97            EncStringParseError::CoseMissingAlgorithm,
98        )),
99        (CoseAlgorithmPolicy::Exactly(expected), Some(actual)) if actual != expected => {
100            Err(CryptoError::WrongKeyType)
101        }
102        (_, Some(actual)) => Ok(actual),
103    }
104}
105
106/// Validates that, if the protected `header` declares a content-encryption algorithm, it matches
107/// `C`'s.
108///
109/// A missing algorithm is tolerated to support legacy envelopes that were sealed without declaring
110/// it; in that case the dispatcher has already selected the cipher via its fallback. A
111/// present-but-wrong algorithm is rejected with [`CryptoError::WrongKeyType`].
112fn ensure_algorithm_matches<C: CoseEncryptCipher>(header: &Header) -> Result<(), CryptoError> {
113    match header.alg.as_ref() {
114        Some(algorithm) if algorithm != &C::COSE_ALGORITHM => Err(CryptoError::WrongKeyType),
115        _ => Ok(()),
116    }
117}
118
119/// Encrypts `plaintext` into a multi-recipient COSE [`CoseEncrypt`] message, dispatching to the
120/// [`CoseEncryptCipher`] selected by `algorithm`.
121///
122/// The chosen cipher declares its algorithm in the (authenticated) protected header, so
123/// [`decrypt_cose`] can recover it from the message without the caller specifying it. The caller is
124/// expected to have configured the recipient(s) on `builder`; `cek` is the content-encryption key
125/// and must match the selected cipher's key length.
126///
127/// If the `protected_header` declares a [`ContentFormat::Utf8`] content type, the plaintext is
128/// padded to a block boundary before encryption to hide its exact length. The corresponding
129/// [`decrypt_cose`] removes the padding transparently.
130pub(crate) fn encrypt_cose(
131    algorithm: CoseContentEncryptionAlgorithm,
132    builder: CoseEncryptBuilder,
133    protected_header: Header,
134    plaintext: &[u8],
135    cek: &[u8],
136) -> Result<CoseEncrypt, CryptoError> {
137    let mut plaintext = plaintext.to_vec();
138    if let Ok(content_format) = ContentFormat::try_from(&protected_header)
139        && should_pad_content(&content_format)
140    {
141        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
142        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
143    }
144    match algorithm {
145        CoseContentEncryptionAlgorithm::Aes256Gcm => {
146            let cek: &<Aes256Gcm as Aead>::Key =
147                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
148            Ok(Aes256Gcm::encrypt_cose(
149                builder,
150                protected_header,
151                &plaintext,
152                cek,
153            ))
154        }
155        CoseContentEncryptionAlgorithm::XAes256Gcm => {
156            let cek: &<XAes256Gcm as Aead>::Key =
157                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
158            Ok(XAes256Gcm::encrypt_cose(
159                builder,
160                protected_header,
161                &plaintext,
162                cek,
163            ))
164        }
165        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
166            let cek: &<XChaCha20Poly1305 as Aead>::Key =
167                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
168            Ok(XChaCha20Poly1305::encrypt_cose(
169                builder,
170                protected_header,
171                &plaintext,
172                cek,
173            ))
174        }
175        CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256 => {
176            let cek: &<Aes256CbcHmacSha256Aead as Aead>::Key =
177                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
178            Ok(Aes256CbcHmacSha256Aead::encrypt_cose(
179                builder,
180                protected_header,
181                &plaintext,
182                cek,
183            ))
184        }
185    }
186}
187
188/// Authenticates and decrypts a multi-recipient COSE [`CoseEncrypt`] message.
189///
190/// `policy` controls whether the protected algorithm is required, must exactly match a typed CEK,
191/// or may fall back only for a legacy message. Returns an error for a missing, unsupported, or
192/// policy-mismatched algorithm, an invalid CEK length, or failed authentication.
193pub(crate) fn decrypt_cose(
194    cose_encrypt: &CoseEncrypt,
195    policy: CoseAlgorithmPolicy,
196    cek: &[u8],
197) -> Result<Vec<u8>, CryptoError> {
198    let decrypted = match algorithm_from_header(&cose_encrypt.protected.header, policy)? {
199        CoseContentEncryptionAlgorithm::Aes256Gcm => {
200            let cek: &<Aes256Gcm as Aead>::Key =
201                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
202            Aes256Gcm::decrypt_cose(cose_encrypt, cek)?
203        }
204        CoseContentEncryptionAlgorithm::XAes256Gcm => {
205            let cek: &<XAes256Gcm as Aead>::Key =
206                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
207            XAes256Gcm::decrypt_cose(cose_encrypt, cek)?
208        }
209        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
210            let cek: &<XChaCha20Poly1305 as Aead>::Key =
211                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
212            XChaCha20Poly1305::decrypt_cose(cose_encrypt, cek)?
213        }
214        CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256 => {
215            let cek: &<Aes256CbcHmacSha256Aead as Aead>::Key =
216                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
217            Aes256CbcHmacSha256Aead::decrypt_cose(cose_encrypt, cek)?
218        }
219    };
220    if let Ok(content_format) = ContentFormat::try_from(&cose_encrypt.protected.header)
221        && should_pad_content(&content_format)
222    {
223        return Ok(crate::keys::utils::unpad_bytes(&decrypted)?.to_vec());
224    }
225    Ok(decrypted)
226}
227
228/// Encrypts `plaintext` into a single-recipient COSE [`CoseEncrypt0`] message, dispatching to the
229/// [`CoseEncryptCipher`] selected by `algorithm`.
230///
231/// As with [`encrypt_cose`], the cipher declares its algorithm in the (authenticated) protected
232/// header so [`decrypt_cose0`] can recover it. `cek` is the content-encryption key and must match
233/// the selected cipher's key length. Padding is applied for [`ContentFormat::Utf8`] content.
234pub(crate) fn encrypt_cose0(
235    algorithm: CoseContentEncryptionAlgorithm,
236    builder: CoseEncrypt0Builder,
237    protected_header: Header,
238    plaintext: &[u8],
239    cek: &[u8],
240) -> Result<CoseEncrypt0, CryptoError> {
241    let mut plaintext = plaintext.to_vec();
242    if let Ok(content_format) = ContentFormat::try_from(&protected_header)
243        && should_pad_content(&content_format)
244    {
245        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
246        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
247    }
248    match algorithm {
249        CoseContentEncryptionAlgorithm::Aes256Gcm => {
250            let cek: &<Aes256Gcm as Aead>::Key =
251                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
252            Ok(Aes256Gcm::encrypt_cose0(
253                builder,
254                protected_header,
255                &plaintext,
256                cek,
257            ))
258        }
259        CoseContentEncryptionAlgorithm::XAes256Gcm => {
260            let cek: &<XAes256Gcm as Aead>::Key =
261                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
262            Ok(XAes256Gcm::encrypt_cose0(
263                builder,
264                protected_header,
265                &plaintext,
266                cek,
267            ))
268        }
269        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
270            let cek: &<XChaCha20Poly1305 as Aead>::Key =
271                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
272            Ok(XChaCha20Poly1305::encrypt_cose0(
273                builder,
274                protected_header,
275                &plaintext,
276                cek,
277            ))
278        }
279        CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256 => {
280            let cek: &<Aes256CbcHmacSha256Aead as Aead>::Key =
281                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
282            Ok(Aes256CbcHmacSha256Aead::encrypt_cose0(
283                builder,
284                protected_header,
285                &plaintext,
286                cek,
287            ))
288        }
289    }
290}
291
292/// Authenticates and decrypts a single-recipient COSE [`CoseEncrypt0`] message.
293///
294/// `policy` controls whether the protected algorithm is required, must exactly match a typed CEK,
295/// or may fall back only for a legacy message. Returns an error for a missing, unsupported, or
296/// policy-mismatched algorithm, an invalid CEK length, or failed authentication.
297pub(crate) fn decrypt_cose0(
298    cose_encrypt0: &CoseEncrypt0,
299    policy: CoseAlgorithmPolicy,
300    cek: &[u8],
301) -> Result<Vec<u8>, CryptoError> {
302    let decrypted = match algorithm_from_header(&cose_encrypt0.protected.header, policy)? {
303        CoseContentEncryptionAlgorithm::Aes256Gcm => {
304            let cek: &<Aes256Gcm as Aead>::Key =
305                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
306            Aes256Gcm::decrypt_cose0(cose_encrypt0, cek)?
307        }
308        CoseContentEncryptionAlgorithm::XAes256Gcm => {
309            let cek: &<XAes256Gcm as Aead>::Key =
310                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
311            XAes256Gcm::decrypt_cose0(cose_encrypt0, cek)?
312        }
313        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
314            let cek: &<XChaCha20Poly1305 as Aead>::Key =
315                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
316            XChaCha20Poly1305::decrypt_cose0(cose_encrypt0, cek)?
317        }
318        CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256 => {
319            let cek: &<Aes256CbcHmacSha256Aead as Aead>::Key =
320                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
321            Aes256CbcHmacSha256Aead::decrypt_cose0(cose_encrypt0, cek)?
322        }
323    };
324    if let Ok(content_format) = ContentFormat::try_from(&cose_encrypt0.protected.header)
325        && should_pad_content(&content_format)
326    {
327        return Ok(crate::keys::utils::unpad_bytes(&decrypted)?.to_vec());
328    }
329    Ok(decrypted)
330}
331
332/// Encrypts and decrypts the content of COSE [`CoseEncrypt`]/[`CoseEncrypt0`] messages with an
333/// [`Aead`] cipher, using the cipher's key as the content-encryption key (CEK).
334pub(crate) trait CoseEncryptCipher: Aead {
335    /// The COSE algorithm identifier for this content-encryption cipher. It is written to the
336    /// protected header by the `encrypt_*` methods and validated by the `decrypt_*` methods.
337    const COSE_ALGORITHM: Algorithm;
338
339    /// Encrypts `plaintext` under `cek` into a [`CoseEncrypt`], declaring
340    /// [`COSE_ALGORITHM`](Self::COSE_ALGORITHM) in the (authenticated) protected header and storing
341    /// the freshly generated nonce in the unprotected `iv` header.
342    ///
343    /// The caller is expected to have already configured the recipient(s) on the builder. A fresh
344    /// random nonce is generated on every call. Callers are required to use per-message keys or
345    /// otherwise satisfy the selected algorithm's requirements to avoid nonce reuse.
346    fn encrypt_cose(
347        builder: CoseEncryptBuilder,
348        protected_header: Header,
349        plaintext: &[u8],
350        cek: &Self::Key,
351    ) -> CoseEncrypt;
352
353    /// Authenticates and decrypts the ciphertext of `cose_encrypt` under `cek`, reading the nonce
354    /// from the unprotected `iv` header.
355    ///
356    /// Returns an error if a present protected algorithm does not match
357    /// [`COSE_ALGORITHM`](Self::COSE_ALGORITHM), the `iv` header is missing or malformed, the
358    /// ciphertext is missing, or authentication fails (wrong key, tampered ciphertext, or wrong
359    /// associated data).
360    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError>;
361
362    /// Encrypts `plaintext` under `cek` into a [`CoseEncrypt0`]. Behaves like
363    /// [`encrypt_cose`](Self::encrypt_cose), but produces a single-recipient message.
364    fn encrypt_cose0(
365        builder: CoseEncrypt0Builder,
366        protected_header: Header,
367        plaintext: &[u8],
368        cek: &Self::Key,
369    ) -> CoseEncrypt0;
370
371    /// Authenticates and decrypts the ciphertext of `cose_encrypt0` under `cek`. Behaves like
372    /// [`decrypt_cose`](Self::decrypt_cose), but for a single-recipient message.
373    fn decrypt_cose0(cose_encrypt0: &CoseEncrypt0, cek: &Self::Key)
374    -> Result<Vec<u8>, CryptoError>;
375}
376
377impl CoseEncryptCipher for Aes256Gcm {
378    const COSE_ALGORITHM: Algorithm = Algorithm::Assigned(iana::Algorithm::A256GCM);
379
380    fn encrypt_cose(
381        builder: CoseEncryptBuilder,
382        mut protected_header: Header,
383        plaintext: &[u8],
384        cek: &Self::Key,
385    ) -> CoseEncrypt {
386        // Declare the content-encryption algorithm in the protected header so it is authenticated
387        // as part of the AEAD's associated data, and so the COSE object is self-describing.
388        protected_header.alg = Some(Self::COSE_ALGORITHM);
389
390        // AES-256-GCM requires a fresh nonce per message. The CEK is locally derived and unique per
391        // message, so a fresh random nonce is generated regardless. The nonce is stored in the
392        // unprotected `iv` header via the builder.
393        let nonce = Aes256GcmNonce::make();
394        builder
395            .protected(protected_header)
396            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
397            .create_ciphertext(plaintext, &[], |data, aad| {
398                Aes256Gcm::encrypt(cek, &nonce, data, aad)
399                    .encrypted_bytes()
400                    .to_vec()
401            })
402            .build()
403    }
404
405    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError> {
406        // If the protected header declares an algorithm it must be this cipher's; a missing
407        // algorithm is tolerated for legacy envelopes (the dispatcher selected the cipher). The
408        // header is authenticated as part of the AEAD associated data regardless.
409        ensure_algorithm_matches::<Self>(&cose_encrypt.protected.header)?;
410
411        let nonce = Aes256GcmNonce::try_from(cose_encrypt)?;
412        cose_encrypt.decrypt_ciphertext(
413            &[],
414            || CryptoError::MissingField("ciphertext"),
415            |data, aad| {
416                Aes256Gcm::decrypt(cek, &nonce, &Aes256GcmCiphertext::from(data.to_vec()), aad)
417            },
418        )
419    }
420
421    fn encrypt_cose0(
422        builder: CoseEncrypt0Builder,
423        mut protected_header: Header,
424        plaintext: &[u8],
425        cek: &Self::Key,
426    ) -> CoseEncrypt0 {
427        protected_header.alg = Some(Self::COSE_ALGORITHM);
428
429        let nonce = Aes256GcmNonce::make();
430        builder
431            .protected(protected_header)
432            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
433            .create_ciphertext(plaintext, &[], |data, aad| {
434                Aes256Gcm::encrypt(cek, &nonce, data, aad)
435                    .encrypted_bytes()
436                    .to_vec()
437            })
438            .build()
439    }
440
441    fn decrypt_cose0(
442        cose_encrypt0: &CoseEncrypt0,
443        cek: &Self::Key,
444    ) -> Result<Vec<u8>, CryptoError> {
445        ensure_algorithm_matches::<Self>(&cose_encrypt0.protected.header)?;
446
447        let nonce = Aes256GcmNonce::try_from(cose_encrypt0)?;
448        cose_encrypt0.decrypt_ciphertext(
449            &[],
450            || CryptoError::MissingField("ciphertext"),
451            |data, aad| {
452                Aes256Gcm::decrypt(cek, &nonce, &Aes256GcmCiphertext::from(data.to_vec()), aad)
453            },
454        )
455    }
456}
457
458impl CoseEncryptCipher for XAes256Gcm {
459    const COSE_ALGORITHM: Algorithm = Algorithm::PrivateUse(XAES_256_GCM);
460
461    fn encrypt_cose(
462        builder: CoseEncryptBuilder,
463        mut protected_header: Header,
464        plaintext: &[u8],
465        cek: &Self::Key,
466    ) -> CoseEncrypt {
467        protected_header.alg = Some(Self::COSE_ALGORITHM);
468
469        let nonce = XAes256GcmNonce::make();
470        builder
471            .protected(protected_header)
472            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
473            .create_ciphertext(plaintext, &[], |data, aad| {
474                XAes256Gcm::encrypt(cek, &nonce, data, aad)
475                    .encrypted_bytes()
476                    .to_vec()
477            })
478            .build()
479    }
480
481    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError> {
482        ensure_algorithm_matches::<Self>(&cose_encrypt.protected.header)?;
483
484        let nonce = XAes256GcmNonce::try_from(cose_encrypt)?;
485        cose_encrypt.decrypt_ciphertext(
486            &[],
487            || CryptoError::MissingField("ciphertext"),
488            |data, aad| {
489                XAes256Gcm::decrypt(cek, &nonce, &XAes256GcmCiphertext::from(data.to_vec()), aad)
490            },
491        )
492    }
493
494    fn encrypt_cose0(
495        builder: CoseEncrypt0Builder,
496        mut protected_header: Header,
497        plaintext: &[u8],
498        cek: &Self::Key,
499    ) -> CoseEncrypt0 {
500        protected_header.alg = Some(Self::COSE_ALGORITHM);
501
502        let nonce = XAes256GcmNonce::make();
503        builder
504            .protected(protected_header)
505            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
506            .create_ciphertext(plaintext, &[], |data, aad| {
507                XAes256Gcm::encrypt(cek, &nonce, data, aad)
508                    .encrypted_bytes()
509                    .to_vec()
510            })
511            .build()
512    }
513
514    fn decrypt_cose0(
515        cose_encrypt0: &CoseEncrypt0,
516        cek: &Self::Key,
517    ) -> Result<Vec<u8>, CryptoError> {
518        ensure_algorithm_matches::<Self>(&cose_encrypt0.protected.header)?;
519
520        let nonce = XAes256GcmNonce::try_from(cose_encrypt0)?;
521        cose_encrypt0.decrypt_ciphertext(
522            &[],
523            || CryptoError::MissingField("ciphertext"),
524            |data, aad| {
525                XAes256Gcm::decrypt(cek, &nonce, &XAes256GcmCiphertext::from(data.to_vec()), aad)
526            },
527        )
528    }
529}
530
531impl CoseEncryptCipher for XChaCha20Poly1305 {
532    const COSE_ALGORITHM: Algorithm = Algorithm::PrivateUse(XCHACHA20_POLY1305);
533
534    fn encrypt_cose(
535        builder: CoseEncryptBuilder,
536        mut protected_header: Header,
537        plaintext: &[u8],
538        cek: &Self::Key,
539    ) -> CoseEncrypt {
540        protected_header.alg = Some(Self::COSE_ALGORITHM);
541
542        let nonce = XChaCha20Poly1305Nonce::make();
543        builder
544            .protected(protected_header)
545            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
546            .create_ciphertext(plaintext, &[], |data, aad| {
547                XChaCha20Poly1305::encrypt(cek, &nonce, data, aad)
548                    .encrypted_bytes()
549                    .to_vec()
550            })
551            .build()
552    }
553
554    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError> {
555        ensure_algorithm_matches::<Self>(&cose_encrypt.protected.header)?;
556
557        let nonce = XChaCha20Poly1305Nonce::try_from(cose_encrypt)?;
558        cose_encrypt.decrypt_ciphertext(
559            &[],
560            || CryptoError::MissingField("ciphertext"),
561            |data, aad| {
562                XChaCha20Poly1305::decrypt(
563                    cek,
564                    &nonce,
565                    &XChaCha20Poly1305Ciphertext::from(data.to_vec()),
566                    aad,
567                )
568            },
569        )
570    }
571
572    fn encrypt_cose0(
573        builder: CoseEncrypt0Builder,
574        mut protected_header: Header,
575        plaintext: &[u8],
576        cek: &Self::Key,
577    ) -> CoseEncrypt0 {
578        protected_header.alg = Some(Self::COSE_ALGORITHM);
579
580        let nonce = XChaCha20Poly1305Nonce::make();
581        builder
582            .protected(protected_header)
583            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
584            .create_ciphertext(plaintext, &[], |data, aad| {
585                XChaCha20Poly1305::encrypt(cek, &nonce, data, aad)
586                    .encrypted_bytes()
587                    .to_vec()
588            })
589            .build()
590    }
591
592    fn decrypt_cose0(
593        cose_encrypt0: &CoseEncrypt0,
594        cek: &Self::Key,
595    ) -> Result<Vec<u8>, CryptoError> {
596        ensure_algorithm_matches::<Self>(&cose_encrypt0.protected.header)?;
597
598        let nonce = XChaCha20Poly1305Nonce::try_from(cose_encrypt0)?;
599        cose_encrypt0.decrypt_ciphertext(
600            &[],
601            || CryptoError::MissingField("ciphertext"),
602            |data, aad| {
603                XChaCha20Poly1305::decrypt(
604                    cek,
605                    &nonce,
606                    &XChaCha20Poly1305Ciphertext::from(data.to_vec()),
607                    aad,
608                )
609            },
610        )
611    }
612}
613
614impl CoseEncryptCipher for Aes256CbcHmacSha256Aead {
615    const COSE_ALGORITHM: Algorithm = Algorithm::PrivateUse(AES_256_CBC_HMAC_SHA256_AEAD);
616
617    fn encrypt_cose(
618        builder: CoseEncryptBuilder,
619        mut protected_header: Header,
620        plaintext: &[u8],
621        cek: &Self::Key,
622    ) -> CoseEncrypt {
623        protected_header.alg = Some(Self::COSE_ALGORITHM);
624
625        let nonce = Aes256CbcHmacSha256AeadNonce::make();
626        builder
627            .protected(protected_header)
628            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
629            .create_ciphertext(plaintext, &[], |data, aad| {
630                Aes256CbcHmacSha256Aead::encrypt(cek, &nonce, data, aad)
631                    .encrypted_bytes()
632                    .to_vec()
633            })
634            .build()
635    }
636
637    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError> {
638        ensure_algorithm_matches::<Self>(&cose_encrypt.protected.header)?;
639
640        let nonce = Aes256CbcHmacSha256AeadNonce::try_from(cose_encrypt)?;
641        cose_encrypt.decrypt_ciphertext(
642            &[],
643            || CryptoError::MissingField("ciphertext"),
644            |data, aad| {
645                Aes256CbcHmacSha256Aead::decrypt(
646                    cek,
647                    &nonce,
648                    &Aes256CbcHmacSha256AeadCiphertext::from(data.to_vec()),
649                    aad,
650                )
651            },
652        )
653    }
654
655    fn encrypt_cose0(
656        builder: CoseEncrypt0Builder,
657        mut protected_header: Header,
658        plaintext: &[u8],
659        cek: &Self::Key,
660    ) -> CoseEncrypt0 {
661        protected_header.alg = Some(Self::COSE_ALGORITHM);
662
663        let nonce = Aes256CbcHmacSha256AeadNonce::make();
664        builder
665            .protected(protected_header)
666            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
667            .create_ciphertext(plaintext, &[], |data, aad| {
668                Aes256CbcHmacSha256Aead::encrypt(cek, &nonce, data, aad)
669                    .encrypted_bytes()
670                    .to_vec()
671            })
672            .build()
673    }
674
675    fn decrypt_cose0(
676        cose_encrypt0: &CoseEncrypt0,
677        cek: &Self::Key,
678    ) -> Result<Vec<u8>, CryptoError> {
679        ensure_algorithm_matches::<Self>(&cose_encrypt0.protected.header)?;
680
681        let nonce = Aes256CbcHmacSha256AeadNonce::try_from(cose_encrypt0)?;
682        cose_encrypt0.decrypt_ciphertext(
683            &[],
684            || CryptoError::MissingField("ciphertext"),
685            |data, aad| {
686                Aes256CbcHmacSha256Aead::decrypt(
687                    cek,
688                    &nonce,
689                    &Aes256CbcHmacSha256AeadCiphertext::from(data.to_vec()),
690                    aad,
691                )
692            },
693        )
694    }
695}
696
697/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message.
698pub(crate) fn encrypt_xchacha20_poly1305(
699    plaintext: &[u8],
700    key: &XChaCha20Poly1305Key,
701    content_format: ContentFormat,
702) -> Result<CoseEncrypt0Bytes, CryptoError> {
703    let mut plaintext = plaintext.to_vec();
704
705    let header_builder: coset::HeaderBuilder = content_format.into();
706    let mut protected_header = header_builder
707        .key_id(key.key_id.as_slice().to_vec())
708        .build();
709    // This should be adjusted to use the builder pattern once implemented in coset.
710    // The related coset upstream issue is:
711    // https://github.com/google/coset/issues/105
712    protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
713
714    if should_pad_content(&content_format) {
715        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
716        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
717    }
718
719    let nonce = XChaCha20Poly1305Nonce::make();
720    let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
721        .protected(protected_header)
722        .create_ciphertext(&plaintext, &[], |data, aad| {
723            XChaCha20Poly1305::encrypt(&(*key.enc_key).into(), &nonce, data, aad)
724                .encrypted_bytes()
725                .to_vec()
726        })
727        .unprotected(
728            coset::HeaderBuilder::new()
729                .iv(nonce.as_bytes().to_vec())
730                .build(),
731        )
732        .build();
733
734    cose_encrypt0
735        .to_vec()
736        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
737        .map(CoseEncrypt0Bytes::from)
738}
739
740/// Decrypts a COSE Encrypt0 message using a XChaCha20Poly1305 key.
741pub(crate) fn decrypt_xchacha20_poly1305(
742    cose_encrypt0_message: &CoseEncrypt0Bytes,
743    key: &XChaCha20Poly1305Key,
744) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
745    let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message.as_ref())
746        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
747
748    let Some(ref alg) = msg.protected.header.alg else {
749        return Err(CryptoError::EncString(
750            EncStringParseError::CoseMissingAlgorithm,
751        ));
752    };
753
754    if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
755        return Err(CryptoError::WrongKeyType);
756    }
757
758    let content_format = ContentFormat::try_from(&msg.protected.header)
759        .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
760
761    if key.key_id.as_slice() != msg.protected.header.key_id {
762        return Err(CryptoError::WrongCoseKeyId);
763    }
764
765    let nonce = XChaCha20Poly1305Nonce::try_from(&msg)?;
766    let decrypted_message = msg.decrypt_ciphertext(
767        &[],
768        || CryptoError::MissingField("ciphertext"),
769        |data, aad| {
770            XChaCha20Poly1305::decrypt(
771                &(*key.enc_key).into(),
772                &nonce,
773                &XChaCha20Poly1305Ciphertext::from(data.to_vec()),
774                aad,
775            )
776        },
777    )?;
778
779    if should_pad_content(&content_format) {
780        let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
781        return Ok((data.to_vec(), content_format));
782    }
783
784    Ok((decrypted_message, content_format))
785}
786
787/// Encrypts plaintext with XAES-256-GCM and returns a typed COSE Encrypt0 message.
788pub(crate) fn encrypt_xaes256_gcm(
789    plaintext: &[u8],
790    key: &XAes256GcmKey,
791    content_format: ContentFormat,
792) -> Result<CoseEncrypt0Bytes, CryptoError> {
793    let mut plaintext = plaintext.to_vec();
794    let mut protected_header: Header = HeaderBuilder::from(content_format)
795        .key_id(key.key_id.as_slice().to_vec())
796        .build();
797    protected_header.alg = Some(Algorithm::PrivateUse(XAES_256_GCM));
798
799    // GCM as a stream cipher does not have a block size. We want to hide exact
800    // input plaintext length, and pad the plaintext size, if the input is a string.
801    if should_pad_content(&content_format) {
802        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
803        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
804    }
805
806    let nonce = XAes256GcmNonce::make();
807    CoseEncrypt0Builder::new()
808        .protected(protected_header)
809        .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
810        .create_ciphertext(&plaintext, &[], |data, aad| {
811            XAes256Gcm::encrypt(&(*key.enc_key).into(), &nonce, data, aad)
812                .encrypted_bytes()
813                .to_vec()
814        })
815        .build()
816        .to_vec()
817        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
818        .map(CoseEncrypt0Bytes::from)
819}
820
821/// Decrypts a typed COSE Encrypt0 message with an XAES-256-GCM key.
822pub(crate) fn decrypt_xaes256_gcm(
823    message: &CoseEncrypt0Bytes,
824    key: &XAes256GcmKey,
825) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
826    let msg = CoseEncrypt0::from_slice(message.as_ref())
827        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
828
829    let Some(ref algorithm) = msg.protected.header.alg else {
830        return Err(CryptoError::EncString(
831            EncStringParseError::CoseMissingAlgorithm,
832        ));
833    };
834    if *algorithm != Algorithm::PrivateUse(XAES_256_GCM) {
835        return Err(CryptoError::WrongKeyType);
836    }
837
838    let content_format = ContentFormat::try_from(&msg.protected.header)
839        .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
840    if key.key_id.as_slice() != msg.protected.header.key_id {
841        return Err(CryptoError::WrongCoseKeyId);
842    }
843
844    let nonce = XAes256GcmNonce::try_from(&msg)?;
845    let decrypted = msg.decrypt_ciphertext(
846        &[],
847        || CryptoError::MissingField("ciphertext"),
848        |data, aad| {
849            XAes256Gcm::decrypt(
850                &(*key.enc_key).into(),
851                &nonce,
852                &XAes256GcmCiphertext::from(data.to_vec()),
853                aad,
854            )
855        },
856    )?;
857
858    if should_pad_content(&content_format) {
859        return Ok((
860            crate::keys::utils::unpad_bytes(&decrypted)?.to_vec(),
861            content_format,
862        ));
863    }
864
865    Ok((decrypted, content_format))
866}
867
868#[cfg(test)]
869mod tests {
870    use coset::{CoseEncrypt0Builder, CoseEncryptBuilder, CoseRecipientBuilder, HeaderBuilder};
871    use hybrid_array::Array;
872    use iana::KeyOperation;
873
874    use super::*;
875    use crate::keys::KeyId;
876
877    const CEK: [u8; 32] = [7u8; 32];
878    /// AES-256-CBC-HMAC-SHA256 takes a 64-byte composite `enc_key || mac_key` CEK.
879    const CEK_64: [u8; 64] = [7u8; 64];
880    const PLAINTEXT: &[u8] = b"content-encryption test vector";
881
882    const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
883    const KEY_DATA: [u8; 32] = [
884        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
885        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
886        0x1e, 0x1f,
887    ];
888    const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
889    const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
890        131, 88, 28, 163, 1, 58, 0, 1, 17, 111, 3, 24, 42, 4, 80, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
891        11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
892        75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
893        172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
894        13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
895    ];
896
897    /// Every supported content-encryption algorithm, each paired with a CEK of the length that
898    /// algorithm requires.
899    fn algorithms() -> [(CoseContentEncryptionAlgorithm, &'static [u8]); 4] {
900        [
901            (CoseContentEncryptionAlgorithm::Aes256Gcm, &CEK),
902            (CoseContentEncryptionAlgorithm::XAes256Gcm, &CEK),
903            (CoseContentEncryptionAlgorithm::XChaCha20Poly1305, &CEK),
904            (CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256, &CEK_64),
905        ]
906    }
907
908    fn make_xaes_key() -> XAes256GcmKey {
909        XAes256GcmKey {
910            key_id: KeyId::from(KEY_ID),
911            enc_key: Box::pin(Array::from(KEY_DATA)),
912            supported_operations: vec![
913                KeyOperation::Decrypt,
914                KeyOperation::Encrypt,
915                KeyOperation::WrapKey,
916                KeyOperation::UnwrapKey,
917            ],
918        }
919    }
920
921    fn make_xchacha_key() -> XChaCha20Poly1305Key {
922        XChaCha20Poly1305Key {
923            key_id: KeyId::from(KEY_ID),
924            enc_key: Box::pin(Array::from(KEY_DATA)),
925            supported_operations: vec![
926                KeyOperation::Decrypt,
927                KeyOperation::Encrypt,
928                KeyOperation::WrapKey,
929                KeyOperation::UnwrapKey,
930            ],
931        }
932    }
933
934    #[test]
935    fn test_encrypt_decrypt_cose_roundtrip() {
936        for (algorithm, cek) in algorithms() {
937            let builder =
938                CoseEncryptBuilder::new().add_recipient(CoseRecipientBuilder::new().build());
939            let cose_encrypt = encrypt_cose(
940                algorithm,
941                builder,
942                HeaderBuilder::new().build(),
943                PLAINTEXT,
944                cek,
945            )
946            .unwrap();
947            let decrypted =
948                decrypt_cose(&cose_encrypt, CoseAlgorithmPolicy::Exactly(algorithm), cek).unwrap();
949            assert_eq!(decrypted, PLAINTEXT);
950        }
951    }
952
953    #[test]
954    fn test_encrypt_decrypt_cose0_roundtrip() {
955        for (algorithm, cek) in algorithms() {
956            let cose_encrypt0 = encrypt_cose0(
957                algorithm,
958                CoseEncrypt0Builder::new(),
959                HeaderBuilder::new().build(),
960                PLAINTEXT,
961                cek,
962            )
963            .unwrap();
964            let decrypted =
965                decrypt_cose0(&cose_encrypt0, CoseAlgorithmPolicy::Exactly(algorithm), cek)
966                    .unwrap();
967            assert_eq!(decrypted, PLAINTEXT);
968        }
969    }
970
971    /// The AES-256-CBC-HMAC-SHA256 CEK is a 64-byte composite; a 32-byte one must be rejected
972    /// rather than silently truncated or padded.
973    #[test]
974    fn test_aes256_cbc_hmac_rejects_wrong_length_cek() {
975        assert!(matches!(
976            encrypt_cose0(
977                CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256,
978                CoseEncrypt0Builder::new(),
979                HeaderBuilder::new().build(),
980                PLAINTEXT,
981                &CEK,
982            ),
983            Err(CryptoError::InvalidKeyLen)
984        ));
985    }
986
987    #[test]
988    fn test_aes256_cbc_hmac_cose0_fails_with_wrong_key() {
989        let message = encrypt_cose0(
990            CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256,
991            CoseEncrypt0Builder::new(),
992            HeaderBuilder::new().build(),
993            PLAINTEXT,
994            &CEK_64,
995        )
996        .unwrap();
997
998        let mut wrong_cek = CEK_64;
999        wrong_cek[0] ^= 1;
1000        assert!(
1001            decrypt_cose0(
1002                &message,
1003                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256),
1004                &wrong_cek,
1005            )
1006            .is_err()
1007        );
1008
1009        // Flipping only the MAC half must fail too — it is the half that authenticates.
1010        let mut wrong_mac_key = CEK_64;
1011        wrong_mac_key[32] ^= 1;
1012        assert!(
1013            decrypt_cose0(
1014                &message,
1015                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256),
1016                &wrong_mac_key,
1017            )
1018            .is_err()
1019        );
1020    }
1021
1022    #[test]
1023    fn test_aes256_cbc_hmac_cose0_fails_when_ciphertext_tampered() {
1024        let mut message = encrypt_cose0(
1025            CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256,
1026            CoseEncrypt0Builder::new(),
1027            HeaderBuilder::new().build(),
1028            PLAINTEXT,
1029            &CEK_64,
1030        )
1031        .unwrap();
1032
1033        message.ciphertext.as_mut().unwrap()[0] ^= 1;
1034        assert!(
1035            decrypt_cose0(
1036                &message,
1037                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256),
1038                &CEK_64,
1039            )
1040            .is_err()
1041        );
1042    }
1043
1044    /// The algorithm is declared in the protected header, which is authenticated as associated
1045    /// data, so a mismatched policy must be rejected before decryption is attempted.
1046    #[test]
1047    fn test_aes256_cbc_hmac_cose0_rejects_mismatched_policy() {
1048        let message = encrypt_cose0(
1049            CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256,
1050            CoseEncrypt0Builder::new(),
1051            HeaderBuilder::new().build(),
1052            PLAINTEXT,
1053            &CEK_64,
1054        )
1055        .unwrap();
1056
1057        assert_eq!(
1058            message.protected.header.alg,
1059            Some(Algorithm::PrivateUse(AES_256_CBC_HMAC_SHA256_AEAD))
1060        );
1061        assert!(matches!(
1062            decrypt_cose0(
1063                &message,
1064                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::Aes256Gcm),
1065                &CEK_64,
1066            ),
1067            Err(CryptoError::WrongKeyType)
1068        ));
1069    }
1070
1071    #[test]
1072    fn test_decrypt_cose_algorithm_policies() {
1073        let builder =
1074            || CoseEncryptBuilder::new().add_recipient(CoseRecipientBuilder::new().build());
1075        let message = encrypt_cose(
1076            CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
1077            builder(),
1078            HeaderBuilder::new().build(),
1079            PLAINTEXT,
1080            &CEK,
1081        )
1082        .unwrap();
1083
1084        assert_eq!(
1085            decrypt_cose(
1086                &message,
1087                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XChaCha20Poly1305,),
1088                &CEK,
1089            )
1090            .unwrap(),
1091            PLAINTEXT
1092        );
1093        assert!(matches!(
1094            decrypt_cose(
1095                &message,
1096                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::Aes256Gcm),
1097                &CEK,
1098            ),
1099            Err(CryptoError::WrongKeyType)
1100        ));
1101        assert_eq!(
1102            decrypt_cose(
1103                &message,
1104                CoseAlgorithmPolicy::ProtectedHeaderAlgorithmOrLegacyDefault(
1105                    CoseContentEncryptionAlgorithm::Aes256Gcm,
1106                ),
1107                &CEK,
1108            )
1109            .unwrap(),
1110            PLAINTEXT
1111        );
1112
1113        let missing_algorithm = builder()
1114            .protected(HeaderBuilder::new().build())
1115            .create_ciphertext(PLAINTEXT, &[], |data, _| data.to_vec())
1116            .build();
1117        for policy in [
1118            CoseAlgorithmPolicy::RequireProtectedHeaderAlgorithm,
1119            CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XChaCha20Poly1305),
1120        ] {
1121            assert!(matches!(
1122                decrypt_cose(&missing_algorithm, policy, &CEK),
1123                Err(CryptoError::EncString(
1124                    EncStringParseError::CoseMissingAlgorithm
1125                ))
1126            ));
1127        }
1128
1129        let nonce = XChaCha20Poly1305Nonce::make();
1130        let legacy_message = builder()
1131            .protected(HeaderBuilder::new().build())
1132            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
1133            .create_ciphertext(PLAINTEXT, &[], |data, aad| {
1134                XChaCha20Poly1305::encrypt(&CEK, &nonce, data, aad)
1135                    .encrypted_bytes()
1136                    .to_vec()
1137            })
1138            .build();
1139        assert_eq!(
1140            decrypt_cose(
1141                &legacy_message,
1142                CoseAlgorithmPolicy::ProtectedHeaderAlgorithmOrLegacyDefault(
1143                    CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
1144                ),
1145                &CEK,
1146            )
1147            .unwrap(),
1148            PLAINTEXT
1149        );
1150    }
1151
1152    #[test]
1153    fn test_decrypt_cose0_algorithm_policies() {
1154        let message = encrypt_cose0(
1155            CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
1156            CoseEncrypt0Builder::new(),
1157            HeaderBuilder::new().build(),
1158            PLAINTEXT,
1159            &CEK,
1160        )
1161        .unwrap();
1162
1163        assert_eq!(
1164            decrypt_cose0(
1165                &message,
1166                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XChaCha20Poly1305,),
1167                &CEK,
1168            )
1169            .unwrap(),
1170            PLAINTEXT
1171        );
1172        assert!(matches!(
1173            decrypt_cose0(
1174                &message,
1175                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::Aes256Gcm),
1176                &CEK,
1177            ),
1178            Err(CryptoError::WrongKeyType)
1179        ));
1180        assert_eq!(
1181            decrypt_cose0(
1182                &message,
1183                CoseAlgorithmPolicy::ProtectedHeaderAlgorithmOrLegacyDefault(
1184                    CoseContentEncryptionAlgorithm::Aes256Gcm,
1185                ),
1186                &CEK,
1187            )
1188            .unwrap(),
1189            PLAINTEXT
1190        );
1191
1192        let missing_algorithm = CoseEncrypt0Builder::new()
1193            .protected(HeaderBuilder::new().build())
1194            .create_ciphertext(PLAINTEXT, &[], |data, _| data.to_vec())
1195            .build();
1196        assert!(matches!(
1197            decrypt_cose0(
1198                &missing_algorithm,
1199                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XChaCha20Poly1305),
1200                &CEK,
1201            ),
1202            Err(CryptoError::EncString(
1203                EncStringParseError::CoseMissingAlgorithm
1204            ))
1205        ));
1206    }
1207
1208    #[test]
1209    fn test_decrypt_cose0_wrong_key_fails() {
1210        let cose_encrypt0 = encrypt_cose0(
1211            CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
1212            CoseEncrypt0Builder::new(),
1213            HeaderBuilder::new().build(),
1214            PLAINTEXT,
1215            &CEK,
1216        )
1217        .unwrap();
1218        let wrong_cek = [0u8; 32];
1219        assert!(
1220            decrypt_cose0(
1221                &cose_encrypt0,
1222                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XChaCha20Poly1305),
1223                &wrong_cek
1224            )
1225            .is_err()
1226        );
1227    }
1228
1229    #[test]
1230    fn test_decrypt_xaes256_gcm_wrong_key_fails() {
1231        let cose_encrypt0 = encrypt_cose0(
1232            CoseContentEncryptionAlgorithm::XAes256Gcm,
1233            CoseEncrypt0Builder::new(),
1234            HeaderBuilder::new().build(),
1235            PLAINTEXT,
1236            &CEK,
1237        )
1238        .unwrap();
1239
1240        assert!(matches!(
1241            decrypt_cose0(
1242                &cose_encrypt0,
1243                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XAes256Gcm),
1244                &[0u8; 32]
1245            ),
1246            Err(CryptoError::KeyDecrypt)
1247        ));
1248    }
1249
1250    #[test]
1251    fn test_xaes256_gcm_emits_24_byte_nonce() {
1252        let cose_encrypt = encrypt_cose(
1253            CoseContentEncryptionAlgorithm::XAes256Gcm,
1254            CoseEncryptBuilder::new().add_recipient(CoseRecipientBuilder::new().build()),
1255            HeaderBuilder::new().build(),
1256            PLAINTEXT,
1257            &CEK,
1258        )
1259        .unwrap();
1260        let cose_encrypt0 = encrypt_cose0(
1261            CoseContentEncryptionAlgorithm::XAes256Gcm,
1262            CoseEncrypt0Builder::new(),
1263            HeaderBuilder::new().build(),
1264            PLAINTEXT,
1265            &CEK,
1266        )
1267        .unwrap();
1268
1269        assert_eq!(cose_encrypt.unprotected.iv.len(), 24);
1270        assert_eq!(cose_encrypt0.unprotected.iv.len(), 24);
1271    }
1272
1273    #[test]
1274    fn test_decrypt_xaes256_gcm_wrong_nonce_fails() {
1275        let mut cose_encrypt0 = encrypt_cose0(
1276            CoseContentEncryptionAlgorithm::XAes256Gcm,
1277            CoseEncrypt0Builder::new(),
1278            HeaderBuilder::new().build(),
1279            PLAINTEXT,
1280            &CEK,
1281        )
1282        .unwrap();
1283        cose_encrypt0.unprotected.iv[0] ^= 1;
1284
1285        assert!(matches!(
1286            decrypt_cose0(
1287                &cose_encrypt0,
1288                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XAes256Gcm),
1289                &CEK
1290            ),
1291            Err(CryptoError::KeyDecrypt)
1292        ));
1293    }
1294
1295    #[test]
1296    fn test_decrypt_xaes256_gcm_malformed_nonce_fails() {
1297        let mut cose_encrypt0 = encrypt_cose0(
1298            CoseContentEncryptionAlgorithm::XAes256Gcm,
1299            CoseEncrypt0Builder::new(),
1300            HeaderBuilder::new().build(),
1301            PLAINTEXT,
1302            &CEK,
1303        )
1304        .unwrap();
1305        cose_encrypt0.unprotected.iv.pop();
1306
1307        assert!(matches!(
1308            decrypt_cose0(
1309                &cose_encrypt0,
1310                CoseAlgorithmPolicy::Exactly(CoseContentEncryptionAlgorithm::XAes256Gcm),
1311                &CEK
1312            ),
1313            Err(CryptoError::InvalidNonceLength)
1314        ));
1315    }
1316
1317    #[test]
1318    fn test_decrypt_cose0_missing_algorithm_fails_without_default() {
1319        // A message with no declared algorithm and no fallback cannot be dispatched.
1320        let cose_encrypt0 = CoseEncrypt0Builder::new()
1321            .protected(HeaderBuilder::new().build())
1322            .create_ciphertext(PLAINTEXT, &[], |data, _| data.to_vec())
1323            .build();
1324        assert!(matches!(
1325            decrypt_cose0(
1326                &cose_encrypt0,
1327                CoseAlgorithmPolicy::RequireProtectedHeaderAlgorithm,
1328                &CEK
1329            ),
1330            Err(CryptoError::EncString(
1331                EncStringParseError::CoseMissingAlgorithm
1332            ))
1333        ));
1334    }
1335
1336    #[test]
1337    fn test_decrypt_cose0_missing_algorithm_uses_default() {
1338        // A legacy message with no declared algorithm decrypts when a fallback algorithm is
1339        // provided. This is built by hand to omit the algorithm from the protected header, which
1340        // the `encrypt_cose0` helper would otherwise always set.
1341        let nonce = XChaCha20Poly1305Nonce::make();
1342        let cose_encrypt0 = CoseEncrypt0Builder::new()
1343            .protected(HeaderBuilder::new().build())
1344            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
1345            .create_ciphertext(PLAINTEXT, &[], |data, aad| {
1346                XChaCha20Poly1305::encrypt(&CEK, &nonce, data, aad)
1347                    .encrypted_bytes()
1348                    .to_vec()
1349            })
1350            .build();
1351
1352        let decrypted = decrypt_cose0(
1353            &cose_encrypt0,
1354            CoseAlgorithmPolicy::ProtectedHeaderAlgorithmOrLegacyDefault(
1355                CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
1356            ),
1357            &CEK,
1358        )
1359        .unwrap();
1360        assert_eq!(decrypted, PLAINTEXT);
1361    }
1362
1363    #[test]
1364    fn test_encrypt_decrypt_xchacha20_roundtrip_octetstream() {
1365        use crate::SymmetricCryptoKey;
1366        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
1367            SymmetricCryptoKey::make_xchacha20_poly1305_key()
1368        else {
1369            panic!("Failed to create XChaCha20Poly1305Key");
1370        };
1371
1372        let plaintext = b"Hello, world!";
1373        let encrypted =
1374            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
1375        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
1376        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
1377    }
1378
1379    #[test]
1380    fn test_encrypt_decrypt_xchacha20_roundtrip_utf8() {
1381        use crate::SymmetricCryptoKey;
1382        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
1383            SymmetricCryptoKey::make_xchacha20_poly1305_key()
1384        else {
1385            panic!("Failed to create XChaCha20Poly1305Key");
1386        };
1387
1388        let plaintext = b"Hello, world!";
1389        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
1390        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
1391        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
1392    }
1393
1394    #[test]
1395    fn test_encrypt_decrypt_xchacha20_roundtrip_pkcs8() {
1396        use crate::SymmetricCryptoKey;
1397        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
1398            SymmetricCryptoKey::make_xchacha20_poly1305_key()
1399        else {
1400            panic!("Failed to create XChaCha20Poly1305Key");
1401        };
1402
1403        let plaintext = b"Hello, world!";
1404        let encrypted =
1405            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
1406        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
1407        assert_eq!(
1408            decrypted,
1409            (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
1410        );
1411    }
1412
1413    #[test]
1414    fn test_encrypt_decrypt_xchacha20_roundtrip_cosekey() {
1415        use crate::SymmetricCryptoKey;
1416        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
1417            SymmetricCryptoKey::make_xchacha20_poly1305_key()
1418        else {
1419            panic!("Failed to create XChaCha20Poly1305Key");
1420        };
1421
1422        let plaintext = b"Hello, world!";
1423        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
1424        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
1425        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
1426    }
1427
1428    #[test]
1429    fn test_decrypt_xchacha20_test_vector() {
1430        let key = make_xchacha_key();
1431        let decrypted =
1432            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
1433                .unwrap();
1434        assert_eq!(
1435            decrypted,
1436            (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
1437        );
1438    }
1439
1440    #[test]
1441    fn test_decrypt_xchacha20_fail_wrong_key_id() {
1442        let key = XChaCha20Poly1305Key {
1443            key_id: KeyId::from([1; 16]),
1444            enc_key: Box::pin(Array::from(KEY_DATA)),
1445            supported_operations: vec![
1446                KeyOperation::Decrypt,
1447                KeyOperation::Encrypt,
1448                KeyOperation::WrapKey,
1449                KeyOperation::UnwrapKey,
1450            ],
1451        };
1452        assert!(matches!(
1453            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
1454            Err(CryptoError::WrongCoseKeyId)
1455        ));
1456    }
1457
1458    #[test]
1459    fn test_decrypt_xchacha20_fail_wrong_algorithm() {
1460        use coset::iana;
1461        let protected_header = coset::HeaderBuilder::new()
1462            .algorithm(iana::Algorithm::A256GCM)
1463            .key_id(KEY_ID.to_vec())
1464            .build();
1465        let nonce = [0u8; 16];
1466        let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
1467            .protected(protected_header)
1468            .create_ciphertext(&[], &[], |_, _| Vec::new())
1469            .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
1470            .build();
1471        let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
1472
1473        let key = make_xchacha_key();
1474        assert!(matches!(
1475            decrypt_xchacha20_poly1305(&serialized_message, &key),
1476            Err(CryptoError::WrongKeyType)
1477        ));
1478    }
1479
1480    fn xaes_message(content_format: ContentFormat) -> CoseEncrypt0 {
1481        let encoded =
1482            encrypt_xaes256_gcm(TEST_VECTOR_PLAINTEXT, &make_xaes_key(), content_format).unwrap();
1483        CoseEncrypt0::from_slice(encoded.as_ref()).unwrap()
1484    }
1485
1486    fn rebuild_xaes_message(message: CoseEncrypt0, protected: Header) -> CoseEncrypt0Bytes {
1487        CoseEncrypt0Builder::new()
1488            .protected(protected)
1489            .unprotected(message.unprotected)
1490            .ciphertext(message.ciphertext.unwrap())
1491            .build()
1492            .to_vec()
1493            .unwrap()
1494            .into()
1495    }
1496
1497    #[test]
1498    fn test_xaes256_gcm_roundtrip_content_formats() {
1499        for content_format in [
1500            ContentFormat::OctetStream,
1501            ContentFormat::Utf8,
1502            ContentFormat::Pkcs8PrivateKey,
1503            ContentFormat::CoseKey,
1504        ] {
1505            let encrypted =
1506                encrypt_xaes256_gcm(TEST_VECTOR_PLAINTEXT, &make_xaes_key(), content_format)
1507                    .unwrap();
1508            assert_eq!(
1509                decrypt_xaes256_gcm(&encrypted, &make_xaes_key()).unwrap(),
1510                (TEST_VECTOR_PLAINTEXT.to_vec(), content_format)
1511            );
1512        }
1513    }
1514
1515    #[test]
1516    fn test_xaes256_gcm_key_id_and_authentication_failures() {
1517        let encrypted = encrypt_xaes256_gcm(
1518            TEST_VECTOR_PLAINTEXT,
1519            &make_xaes_key(),
1520            ContentFormat::OctetStream,
1521        )
1522        .unwrap();
1523
1524        let mut wrong_bytes = make_xaes_key();
1525        wrong_bytes.enc_key[0] ^= 1;
1526        assert!(matches!(
1527            decrypt_xaes256_gcm(&encrypted, &wrong_bytes),
1528            Err(CryptoError::KeyDecrypt)
1529        ));
1530
1531        let mut wrong_id = make_xaes_key();
1532        wrong_id.key_id = KeyId::from([1; 16]);
1533        assert!(matches!(
1534            decrypt_xaes256_gcm(&encrypted, &wrong_id),
1535            Err(CryptoError::WrongCoseKeyId)
1536        ));
1537    }
1538
1539    #[test]
1540    fn test_xaes256_gcm_rejects_invalid_protected_headers() {
1541        let key = make_xaes_key();
1542
1543        let mut message = xaes_message(ContentFormat::OctetStream);
1544        message.protected.header.alg = Some(Algorithm::Assigned(iana::Algorithm::A256GCM));
1545        let encrypted = rebuild_xaes_message(message.clone(), message.protected.header.clone());
1546        assert!(matches!(
1547            decrypt_xaes256_gcm(&encrypted, &key),
1548            Err(CryptoError::WrongKeyType)
1549        ));
1550
1551        message.protected.header.alg = None;
1552        let encrypted = rebuild_xaes_message(message.clone(), message.protected.header.clone());
1553        assert!(matches!(
1554            decrypt_xaes256_gcm(&encrypted, &key),
1555            Err(CryptoError::EncString(
1556                EncStringParseError::CoseMissingAlgorithm
1557            ))
1558        ));
1559
1560        for content_type in [
1561            None,
1562            Some(coset::ContentType::Text("application/unsupported".into())),
1563        ] {
1564            message.protected.header.alg = Some(Algorithm::PrivateUse(XAES_256_GCM));
1565            message.protected.header.content_type = content_type;
1566            let encrypted = rebuild_xaes_message(message.clone(), message.protected.header.clone());
1567            assert!(matches!(
1568                decrypt_xaes256_gcm(&encrypted, &key),
1569                Err(CryptoError::EncString(
1570                    EncStringParseError::CoseMissingContentType
1571                ))
1572            ));
1573        }
1574    }
1575
1576    #[test]
1577    fn test_xaes256_gcm_rejects_missing_or_malformed_fields() {
1578        let key = make_xaes_key();
1579        let mut message = xaes_message(ContentFormat::OctetStream);
1580        message.ciphertext = None;
1581        let encrypted = message.to_vec().unwrap().into();
1582        assert!(matches!(
1583            decrypt_xaes256_gcm(&encrypted, &key),
1584            Err(CryptoError::MissingField("ciphertext"))
1585        ));
1586
1587        let mut message = xaes_message(ContentFormat::OctetStream);
1588        message.unprotected.iv.pop();
1589        let encrypted = message.to_vec().unwrap().into();
1590        assert!(matches!(
1591            decrypt_xaes256_gcm(&encrypted, &key),
1592            Err(CryptoError::InvalidNonceLength)
1593        ));
1594
1595        assert!(matches!(
1596            decrypt_xaes256_gcm(&CoseEncrypt0Bytes::from([0xff].as_slice()), &key),
1597            Err(CryptoError::EncString(
1598                EncStringParseError::InvalidCoseEncoding(_)
1599            ))
1600        ));
1601    }
1602
1603    #[test]
1604    fn test_xaes256_gcm_rejects_invalid_padding() {
1605        let key = make_xaes_key();
1606        let nonce = XAes256GcmNonce::make();
1607        let mut protected = HeaderBuilder::from(ContentFormat::Utf8)
1608            .key_id(KEY_ID.to_vec())
1609            .build();
1610        protected.alg = Some(Algorithm::PrivateUse(XAES_256_GCM));
1611        let message = CoseEncrypt0Builder::new()
1612            .protected(protected)
1613            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
1614            .create_ciphertext(&[0], &[], |data, aad| {
1615                XAes256Gcm::encrypt(&KEY_DATA, &nonce, data, aad)
1616                    .encrypted_bytes()
1617                    .to_vec()
1618            })
1619            .build()
1620            .to_vec()
1621            .unwrap()
1622            .into();
1623
1624        assert!(matches!(
1625            decrypt_xaes256_gcm(&message, &key),
1626            Err(CryptoError::InvalidPadding)
1627        ));
1628    }
1629}