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::XCHACHA20_POLY1305;
12use crate::{
13    ContentFormat, CoseEncrypt0Bytes, CryptoError, XChaCha20Poly1305Key,
14    error::EncStringParseError,
15    hazmat::symmetric_encryption::{
16        Aead,
17        aes_gcm::{Aes256Gcm, Aes256GcmCiphertext, Aes256GcmNonce},
18        xchacha20::{XChaCha20Poly1305, XChaCha20Poly1305Ciphertext, XChaCha20Poly1305Nonce},
19    },
20};
21
22const TEXT_PAD_BLOCK_SIZE: usize = 32;
23
24fn should_pad_content(format: &ContentFormat) -> bool {
25    matches!(format, ContentFormat::Utf8)
26}
27
28/// The content-encryption algorithms that can seal the body of a COSE message.
29///
30/// This selects which [`CoseEncryptCipher`] the free `encrypt_cose`/`encrypt_cose0` functions
31/// dispatch to. On decryption the algorithm is instead recovered from the message's protected
32/// header (see [`decrypt_cose`]/[`decrypt_cose0`]), so the caller does not need to know it up
33/// front.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(crate) enum CoseContentEncryptionAlgorithm {
36    /// AES-256-GCM (COSE `A256GCM`).
37    Aes256Gcm,
38    /// XChaCha20-Poly1305 (private-use [`XCHACHA20_POLY1305`]).
39    XChaCha20Poly1305,
40}
41
42impl TryFrom<&Algorithm> for CoseContentEncryptionAlgorithm {
43    type Error = CryptoError;
44
45    fn try_from(algorithm: &Algorithm) -> Result<Self, Self::Error> {
46        match algorithm {
47            Algorithm::Assigned(iana::Algorithm::A256GCM) => Ok(Self::Aes256Gcm),
48            Algorithm::PrivateUse(XCHACHA20_POLY1305) => Ok(Self::XChaCha20Poly1305),
49            _ => Err(CryptoError::WrongKeyType),
50        }
51    }
52}
53
54/// Recovers the content-encryption algorithm declared in a message's protected header, falling back
55/// to `default_algorithm` when the header omits one.
56///
57/// Some legacy envelopes (notably early
58/// [`PasswordProtectedKeyEnvelope`](crate::safe::PasswordProtectedKeyEnvelope)s) were sealed
59/// without declaring the content-encryption algorithm in their protected header. Callers
60/// that must decrypt such messages pass the algorithm they expect as `default_algorithm`; passing
61/// `None` requires the header to declare it.
62fn algorithm_from_header(
63    header: &Header,
64    default_algorithm: Option<CoseContentEncryptionAlgorithm>,
65) -> Result<CoseContentEncryptionAlgorithm, CryptoError> {
66    match header.alg.as_ref() {
67        Some(algorithm) => CoseContentEncryptionAlgorithm::try_from(algorithm),
68        None => default_algorithm.ok_or(CryptoError::EncString(
69            EncStringParseError::CoseMissingAlgorithm,
70        )),
71    }
72}
73
74/// Validates that, if the protected `header` declares a content-encryption algorithm, it matches
75/// `C`'s.
76///
77/// A missing algorithm is tolerated to support legacy envelopes that were sealed without declaring
78/// it; in that case the dispatcher has already selected the cipher via its fallback. A
79/// present-but-wrong algorithm is rejected with [`CryptoError::WrongKeyType`].
80fn ensure_algorithm_matches<C: CoseEncryptCipher>(header: &Header) -> Result<(), CryptoError> {
81    match header.alg.as_ref() {
82        Some(algorithm) if algorithm != &C::COSE_ALGORITHM => Err(CryptoError::WrongKeyType),
83        _ => Ok(()),
84    }
85}
86
87/// Encrypts `plaintext` into a multi-recipient COSE [`CoseEncrypt`] message, dispatching to the
88/// [`CoseEncryptCipher`] selected by `algorithm`.
89///
90/// The chosen cipher declares its algorithm in the (authenticated) protected header, so
91/// [`decrypt_cose`] can recover it from the message without the caller specifying it. The caller is
92/// expected to have configured the recipient(s) on `builder`; `cek` is the content-encryption key
93/// and must match the selected cipher's key length.
94///
95/// If the `protected_header` declares a [`ContentFormat::Utf8`] content type, the plaintext is
96/// padded to a block boundary before encryption to hide its exact length. The corresponding
97/// [`decrypt_cose`] removes the padding transparently.
98pub(crate) fn encrypt_cose(
99    algorithm: CoseContentEncryptionAlgorithm,
100    builder: CoseEncryptBuilder,
101    protected_header: Header,
102    plaintext: &[u8],
103    cek: &[u8],
104) -> Result<CoseEncrypt, CryptoError> {
105    let mut plaintext = plaintext.to_vec();
106    if let Ok(content_format) = ContentFormat::try_from(&protected_header)
107        && should_pad_content(&content_format)
108    {
109        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
110        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
111    }
112    match algorithm {
113        CoseContentEncryptionAlgorithm::Aes256Gcm => {
114            let cek: &<Aes256Gcm as Aead>::Key =
115                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
116            Ok(Aes256Gcm::encrypt_cose(
117                builder,
118                protected_header,
119                &plaintext,
120                cek,
121            ))
122        }
123        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
124            let cek: &<XChaCha20Poly1305 as Aead>::Key =
125                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
126            Ok(XChaCha20Poly1305::encrypt_cose(
127                builder,
128                protected_header,
129                &plaintext,
130                cek,
131            ))
132        }
133    }
134}
135
136/// Authenticates and decrypts a multi-recipient COSE [`CoseEncrypt`] message, dispatching to the
137/// [`CoseEncryptCipher`] indicated by the content-encryption algorithm declared in the message's
138/// protected header.
139///
140/// When the protected header omits the content-encryption algorithm (some legacy envelopes),
141/// `default_algorithm` is used instead; pass `None` to require the header to declare it.
142///
143/// Returns an error if the algorithm cannot be determined or is unsupported, if `cek` has the wrong
144/// length for that cipher, or if authentication fails.
145pub(crate) fn decrypt_cose(
146    cose_encrypt: &CoseEncrypt,
147    default_algorithm: Option<CoseContentEncryptionAlgorithm>,
148    cek: &[u8],
149) -> Result<Vec<u8>, CryptoError> {
150    let decrypted = match algorithm_from_header(&cose_encrypt.protected.header, default_algorithm)?
151    {
152        CoseContentEncryptionAlgorithm::Aes256Gcm => {
153            let cek: &<Aes256Gcm as Aead>::Key =
154                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
155            Aes256Gcm::decrypt_cose(cose_encrypt, cek)?
156        }
157        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
158            let cek: &<XChaCha20Poly1305 as Aead>::Key =
159                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
160            XChaCha20Poly1305::decrypt_cose(cose_encrypt, cek)?
161        }
162    };
163    if let Ok(content_format) = ContentFormat::try_from(&cose_encrypt.protected.header)
164        && should_pad_content(&content_format)
165    {
166        return Ok(crate::keys::utils::unpad_bytes(&decrypted)?.to_vec());
167    }
168    Ok(decrypted)
169}
170
171/// Encrypts `plaintext` into a single-recipient COSE [`CoseEncrypt0`] message, dispatching to the
172/// [`CoseEncryptCipher`] selected by `algorithm`.
173///
174/// As with [`encrypt_cose`], the cipher declares its algorithm in the (authenticated) protected
175/// header so [`decrypt_cose0`] can recover it. `cek` is the content-encryption key and must match
176/// the selected cipher's key length. Padding is applied for [`ContentFormat::Utf8`] content.
177pub(crate) fn encrypt_cose0(
178    algorithm: CoseContentEncryptionAlgorithm,
179    builder: CoseEncrypt0Builder,
180    protected_header: Header,
181    plaintext: &[u8],
182    cek: &[u8],
183) -> Result<CoseEncrypt0, CryptoError> {
184    let mut plaintext = plaintext.to_vec();
185    if let Ok(content_format) = ContentFormat::try_from(&protected_header)
186        && should_pad_content(&content_format)
187    {
188        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
189        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
190    }
191    match algorithm {
192        CoseContentEncryptionAlgorithm::Aes256Gcm => {
193            let cek: &<Aes256Gcm as Aead>::Key =
194                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
195            Ok(Aes256Gcm::encrypt_cose0(
196                builder,
197                protected_header,
198                &plaintext,
199                cek,
200            ))
201        }
202        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
203            let cek: &<XChaCha20Poly1305 as Aead>::Key =
204                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
205            Ok(XChaCha20Poly1305::encrypt_cose0(
206                builder,
207                protected_header,
208                &plaintext,
209                cek,
210            ))
211        }
212    }
213}
214
215/// Authenticates and decrypts a single-recipient COSE [`CoseEncrypt0`] message, dispatching to the
216/// [`CoseEncryptCipher`] indicated by the content-encryption algorithm declared in the message's
217/// protected header.
218///
219/// When the protected header omits the content-encryption algorithm (some legacy envelopes),
220/// `default_algorithm` is used instead; pass `None` to require the header to declare it.
221///
222/// Returns an error if the algorithm cannot be determined or is unsupported, if `cek` has the wrong
223/// length for that cipher, or if authentication fails.
224pub(crate) fn decrypt_cose0(
225    cose_encrypt0: &CoseEncrypt0,
226    default_algorithm: Option<CoseContentEncryptionAlgorithm>,
227    cek: &[u8],
228) -> Result<Vec<u8>, CryptoError> {
229    let decrypted = match algorithm_from_header(&cose_encrypt0.protected.header, default_algorithm)?
230    {
231        CoseContentEncryptionAlgorithm::Aes256Gcm => {
232            let cek: &<Aes256Gcm as Aead>::Key =
233                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
234            Aes256Gcm::decrypt_cose0(cose_encrypt0, cek)?
235        }
236        CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
237            let cek: &<XChaCha20Poly1305 as Aead>::Key =
238                cek.try_into().map_err(|_| CryptoError::InvalidKeyLen)?;
239            XChaCha20Poly1305::decrypt_cose0(cose_encrypt0, cek)?
240        }
241    };
242    if let Ok(content_format) = ContentFormat::try_from(&cose_encrypt0.protected.header)
243        && should_pad_content(&content_format)
244    {
245        return Ok(crate::keys::utils::unpad_bytes(&decrypted)?.to_vec());
246    }
247    Ok(decrypted)
248}
249
250/// Encrypts and decrypts the content of COSE [`CoseEncrypt`]/[`CoseEncrypt0`] messages with an
251/// [`Aead`] cipher, using the cipher's key as the content-encryption key (CEK).
252pub(crate) trait CoseEncryptCipher: Aead {
253    /// The COSE algorithm identifier for this content-encryption cipher. It is written to the
254    /// protected header by the `encrypt_*` methods and validated by the `decrypt_*` methods.
255    const COSE_ALGORITHM: Algorithm;
256
257    /// Encrypts `plaintext` under `cek` into a [`CoseEncrypt`], declaring
258    /// [`COSE_ALGORITHM`](Self::COSE_ALGORITHM) in the (authenticated) protected header and storing
259    /// the freshly generated nonce in the unprotected `iv` header.
260    ///
261    /// The caller is expected to have already configured the recipient(s) on the builder. A fresh
262    /// random nonce is generated on every call; combined with a per-message CEK this avoids nonce
263    /// reuse.
264    fn encrypt_cose(
265        builder: CoseEncryptBuilder,
266        protected_header: Header,
267        plaintext: &[u8],
268        cek: &Self::Key,
269    ) -> CoseEncrypt;
270
271    /// Authenticates and decrypts the ciphertext of `cose_encrypt` under `cek`, reading the nonce
272    /// from the unprotected `iv` header.
273    ///
274    /// Returns an error if the protected header does not declare
275    /// [`COSE_ALGORITHM`](Self::COSE_ALGORITHM), the `iv` header is missing or malformed, the
276    /// ciphertext is missing, or authentication fails (wrong key, tampered ciphertext, or wrong
277    /// associated data).
278    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError>;
279
280    /// Encrypts `plaintext` under `cek` into a [`CoseEncrypt0`]. Behaves like
281    /// [`encrypt_cose`](Self::encrypt_cose), but produces a single-recipient message.
282    fn encrypt_cose0(
283        builder: CoseEncrypt0Builder,
284        protected_header: Header,
285        plaintext: &[u8],
286        cek: &Self::Key,
287    ) -> CoseEncrypt0;
288
289    /// Authenticates and decrypts the ciphertext of `cose_encrypt0` under `cek`. Behaves like
290    /// [`decrypt_cose`](Self::decrypt_cose), but for a single-recipient message.
291    fn decrypt_cose0(cose_encrypt0: &CoseEncrypt0, cek: &Self::Key)
292    -> Result<Vec<u8>, CryptoError>;
293}
294
295impl CoseEncryptCipher for Aes256Gcm {
296    const COSE_ALGORITHM: Algorithm = Algorithm::Assigned(iana::Algorithm::A256GCM);
297
298    fn encrypt_cose(
299        builder: CoseEncryptBuilder,
300        mut protected_header: Header,
301        plaintext: &[u8],
302        cek: &Self::Key,
303    ) -> CoseEncrypt {
304        // Declare the content-encryption algorithm in the protected header so it is authenticated
305        // as part of the AEAD's associated data, and so the COSE object is self-describing.
306        protected_header.alg = Some(Self::COSE_ALGORITHM);
307
308        // AES-256-GCM requires a fresh nonce per message. The CEK is locally derived and unique per
309        // message, so a fresh random nonce is generated regardless. The nonce is stored in the
310        // unprotected `iv` header via the builder.
311        let nonce = Aes256GcmNonce::make();
312        builder
313            .protected(protected_header)
314            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
315            .create_ciphertext(plaintext, &[], |data, aad| {
316                Aes256Gcm::encrypt(cek, &nonce, data, aad)
317                    .encrypted_bytes()
318                    .to_vec()
319            })
320            .build()
321    }
322
323    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError> {
324        // If the protected header declares an algorithm it must be this cipher's; a missing
325        // algorithm is tolerated for legacy envelopes (the dispatcher selected the cipher). The
326        // header is authenticated as part of the AEAD associated data regardless.
327        ensure_algorithm_matches::<Self>(&cose_encrypt.protected.header)?;
328
329        let nonce = Aes256GcmNonce::try_from(cose_encrypt)?;
330        cose_encrypt.decrypt_ciphertext(
331            &[],
332            || CryptoError::MissingField("ciphertext"),
333            |data, aad| {
334                Aes256Gcm::decrypt(cek, &nonce, &Aes256GcmCiphertext::from(data.to_vec()), aad)
335            },
336        )
337    }
338
339    fn encrypt_cose0(
340        builder: CoseEncrypt0Builder,
341        mut protected_header: Header,
342        plaintext: &[u8],
343        cek: &Self::Key,
344    ) -> CoseEncrypt0 {
345        protected_header.alg = Some(Self::COSE_ALGORITHM);
346
347        let nonce = Aes256GcmNonce::make();
348        builder
349            .protected(protected_header)
350            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
351            .create_ciphertext(plaintext, &[], |data, aad| {
352                Aes256Gcm::encrypt(cek, &nonce, data, aad)
353                    .encrypted_bytes()
354                    .to_vec()
355            })
356            .build()
357    }
358
359    fn decrypt_cose0(
360        cose_encrypt0: &CoseEncrypt0,
361        cek: &Self::Key,
362    ) -> Result<Vec<u8>, CryptoError> {
363        ensure_algorithm_matches::<Self>(&cose_encrypt0.protected.header)?;
364
365        let nonce = Aes256GcmNonce::try_from(cose_encrypt0)?;
366        cose_encrypt0.decrypt_ciphertext(
367            &[],
368            || CryptoError::MissingField("ciphertext"),
369            |data, aad| {
370                Aes256Gcm::decrypt(cek, &nonce, &Aes256GcmCiphertext::from(data.to_vec()), aad)
371            },
372        )
373    }
374}
375
376impl CoseEncryptCipher for XChaCha20Poly1305 {
377    const COSE_ALGORITHM: Algorithm = Algorithm::PrivateUse(XCHACHA20_POLY1305);
378
379    fn encrypt_cose(
380        builder: CoseEncryptBuilder,
381        mut protected_header: Header,
382        plaintext: &[u8],
383        cek: &Self::Key,
384    ) -> CoseEncrypt {
385        protected_header.alg = Some(Self::COSE_ALGORITHM);
386
387        let nonce = XChaCha20Poly1305Nonce::make();
388        builder
389            .protected(protected_header)
390            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
391            .create_ciphertext(plaintext, &[], |data, aad| {
392                XChaCha20Poly1305::encrypt(cek, &nonce, data, aad)
393                    .encrypted_bytes()
394                    .to_vec()
395            })
396            .build()
397    }
398
399    fn decrypt_cose(cose_encrypt: &CoseEncrypt, cek: &Self::Key) -> Result<Vec<u8>, CryptoError> {
400        ensure_algorithm_matches::<Self>(&cose_encrypt.protected.header)?;
401
402        let nonce = XChaCha20Poly1305Nonce::try_from(cose_encrypt)?;
403        cose_encrypt.decrypt_ciphertext(
404            &[],
405            || CryptoError::MissingField("ciphertext"),
406            |data, aad| {
407                XChaCha20Poly1305::decrypt(
408                    cek,
409                    &nonce,
410                    &XChaCha20Poly1305Ciphertext::from(data.to_vec()),
411                    aad,
412                )
413            },
414        )
415    }
416
417    fn encrypt_cose0(
418        builder: CoseEncrypt0Builder,
419        mut protected_header: Header,
420        plaintext: &[u8],
421        cek: &Self::Key,
422    ) -> CoseEncrypt0 {
423        protected_header.alg = Some(Self::COSE_ALGORITHM);
424
425        let nonce = XChaCha20Poly1305Nonce::make();
426        builder
427            .protected(protected_header)
428            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
429            .create_ciphertext(plaintext, &[], |data, aad| {
430                XChaCha20Poly1305::encrypt(cek, &nonce, data, aad)
431                    .encrypted_bytes()
432                    .to_vec()
433            })
434            .build()
435    }
436
437    fn decrypt_cose0(
438        cose_encrypt0: &CoseEncrypt0,
439        cek: &Self::Key,
440    ) -> Result<Vec<u8>, CryptoError> {
441        ensure_algorithm_matches::<Self>(&cose_encrypt0.protected.header)?;
442
443        let nonce = XChaCha20Poly1305Nonce::try_from(cose_encrypt0)?;
444        cose_encrypt0.decrypt_ciphertext(
445            &[],
446            || CryptoError::MissingField("ciphertext"),
447            |data, aad| {
448                XChaCha20Poly1305::decrypt(
449                    cek,
450                    &nonce,
451                    &XChaCha20Poly1305Ciphertext::from(data.to_vec()),
452                    aad,
453                )
454            },
455        )
456    }
457}
458
459/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message.
460pub(crate) fn encrypt_xchacha20_poly1305(
461    plaintext: &[u8],
462    key: &XChaCha20Poly1305Key,
463    content_format: ContentFormat,
464) -> Result<CoseEncrypt0Bytes, CryptoError> {
465    let mut plaintext = plaintext.to_vec();
466
467    let header_builder: coset::HeaderBuilder = content_format.into();
468    let mut protected_header = header_builder
469        .key_id(key.key_id.as_slice().to_vec())
470        .build();
471    // This should be adjusted to use the builder pattern once implemented in coset.
472    // The related coset upstream issue is:
473    // https://github.com/google/coset/issues/105
474    protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
475
476    if should_pad_content(&content_format) {
477        let min_length = TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / TEXT_PAD_BLOCK_SIZE));
478        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
479    }
480
481    let nonce = XChaCha20Poly1305Nonce::make();
482    let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
483        .protected(protected_header)
484        .create_ciphertext(&plaintext, &[], |data, aad| {
485            XChaCha20Poly1305::encrypt(&(*key.enc_key).into(), &nonce, data, aad)
486                .encrypted_bytes()
487                .to_vec()
488        })
489        .unprotected(
490            coset::HeaderBuilder::new()
491                .iv(nonce.as_bytes().to_vec())
492                .build(),
493        )
494        .build();
495
496    cose_encrypt0
497        .to_vec()
498        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
499        .map(CoseEncrypt0Bytes::from)
500}
501
502/// Decrypts a COSE Encrypt0 message using a XChaCha20Poly1305 key.
503pub(crate) fn decrypt_xchacha20_poly1305(
504    cose_encrypt0_message: &CoseEncrypt0Bytes,
505    key: &XChaCha20Poly1305Key,
506) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
507    let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message.as_ref())
508        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
509
510    let Some(ref alg) = msg.protected.header.alg else {
511        return Err(CryptoError::EncString(
512            EncStringParseError::CoseMissingAlgorithm,
513        ));
514    };
515
516    if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
517        return Err(CryptoError::WrongKeyType);
518    }
519
520    let content_format = ContentFormat::try_from(&msg.protected.header)
521        .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
522
523    if key.key_id.as_slice() != msg.protected.header.key_id {
524        return Err(CryptoError::WrongCoseKeyId);
525    }
526
527    let nonce = XChaCha20Poly1305Nonce::try_from(&msg)?;
528    let decrypted_message = msg.decrypt_ciphertext(
529        &[],
530        || CryptoError::MissingField("ciphertext"),
531        |data, aad| {
532            XChaCha20Poly1305::decrypt(
533                &(*key.enc_key).into(),
534                &nonce,
535                &XChaCha20Poly1305Ciphertext::from(data.to_vec()),
536                aad,
537            )
538        },
539    )?;
540
541    if should_pad_content(&content_format) {
542        let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
543        return Ok((data.to_vec(), content_format));
544    }
545
546    Ok((decrypted_message, content_format))
547}
548
549#[cfg(test)]
550mod tests {
551    use coset::{CoseEncrypt0Builder, CoseEncryptBuilder, CoseRecipientBuilder, HeaderBuilder};
552    use hybrid_array::Array;
553    use iana::KeyOperation;
554
555    use super::*;
556    use crate::keys::KeyId;
557
558    const CEK: [u8; 32] = [7u8; 32];
559    const PLAINTEXT: &[u8] = b"content-encryption test vector";
560
561    const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
562    const KEY_DATA: [u8; 32] = [
563        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
564        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
565        0x1e, 0x1f,
566    ];
567    const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
568    const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
569        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,
570        11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
571        75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
572        172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
573        13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
574    ];
575
576    fn algorithms() -> [CoseContentEncryptionAlgorithm; 2] {
577        [
578            CoseContentEncryptionAlgorithm::Aes256Gcm,
579            CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
580        ]
581    }
582
583    fn make_xchacha_key() -> XChaCha20Poly1305Key {
584        XChaCha20Poly1305Key {
585            key_id: KeyId::from(KEY_ID),
586            enc_key: Box::pin(Array::from(KEY_DATA)),
587            supported_operations: vec![
588                KeyOperation::Decrypt,
589                KeyOperation::Encrypt,
590                KeyOperation::WrapKey,
591                KeyOperation::UnwrapKey,
592            ],
593        }
594    }
595
596    #[test]
597    fn test_encrypt_decrypt_cose_roundtrip() {
598        for algorithm in algorithms() {
599            let builder =
600                CoseEncryptBuilder::new().add_recipient(CoseRecipientBuilder::new().build());
601            let cose_encrypt = encrypt_cose(
602                algorithm,
603                builder,
604                HeaderBuilder::new().build(),
605                PLAINTEXT,
606                &CEK,
607            )
608            .unwrap();
609            let decrypted = decrypt_cose(&cose_encrypt, None, &CEK).unwrap();
610            assert_eq!(decrypted, PLAINTEXT);
611        }
612    }
613
614    #[test]
615    fn test_encrypt_decrypt_cose0_roundtrip() {
616        for algorithm in algorithms() {
617            let cose_encrypt0 = encrypt_cose0(
618                algorithm,
619                CoseEncrypt0Builder::new(),
620                HeaderBuilder::new().build(),
621                PLAINTEXT,
622                &CEK,
623            )
624            .unwrap();
625            let decrypted = decrypt_cose0(&cose_encrypt0, None, &CEK).unwrap();
626            assert_eq!(decrypted, PLAINTEXT);
627        }
628    }
629
630    #[test]
631    fn test_decrypt_cose0_wrong_key_fails() {
632        let cose_encrypt0 = encrypt_cose0(
633            CoseContentEncryptionAlgorithm::XChaCha20Poly1305,
634            CoseEncrypt0Builder::new(),
635            HeaderBuilder::new().build(),
636            PLAINTEXT,
637            &CEK,
638        )
639        .unwrap();
640        let wrong_cek = [0u8; 32];
641        assert!(decrypt_cose0(&cose_encrypt0, None, &wrong_cek).is_err());
642    }
643
644    #[test]
645    fn test_decrypt_cose0_missing_algorithm_fails_without_default() {
646        // A message with no declared algorithm and no fallback cannot be dispatched.
647        let cose_encrypt0 = CoseEncrypt0Builder::new()
648            .protected(HeaderBuilder::new().build())
649            .create_ciphertext(PLAINTEXT, &[], |data, _| data.to_vec())
650            .build();
651        assert!(matches!(
652            decrypt_cose0(&cose_encrypt0, None, &CEK),
653            Err(CryptoError::EncString(
654                EncStringParseError::CoseMissingAlgorithm
655            ))
656        ));
657    }
658
659    #[test]
660    fn test_decrypt_cose0_missing_algorithm_uses_default() {
661        // A legacy message with no declared algorithm decrypts when a fallback algorithm is
662        // provided. This is built by hand to omit the algorithm from the protected header, which
663        // the `encrypt_cose0` helper would otherwise always set.
664        let nonce = XChaCha20Poly1305Nonce::make();
665        let cose_encrypt0 = CoseEncrypt0Builder::new()
666            .protected(HeaderBuilder::new().build())
667            .unprotected(HeaderBuilder::new().iv(nonce.as_bytes().to_vec()).build())
668            .create_ciphertext(PLAINTEXT, &[], |data, aad| {
669                XChaCha20Poly1305::encrypt(&CEK, &nonce, data, aad)
670                    .encrypted_bytes()
671                    .to_vec()
672            })
673            .build();
674
675        let decrypted = decrypt_cose0(
676            &cose_encrypt0,
677            Some(CoseContentEncryptionAlgorithm::XChaCha20Poly1305),
678            &CEK,
679        )
680        .unwrap();
681        assert_eq!(decrypted, PLAINTEXT);
682    }
683
684    #[test]
685    fn test_encrypt_decrypt_xchacha20_roundtrip_octetstream() {
686        use crate::SymmetricCryptoKey;
687        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
688            SymmetricCryptoKey::make_xchacha20_poly1305_key()
689        else {
690            panic!("Failed to create XChaCha20Poly1305Key");
691        };
692
693        let plaintext = b"Hello, world!";
694        let encrypted =
695            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
696        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
697        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
698    }
699
700    #[test]
701    fn test_encrypt_decrypt_xchacha20_roundtrip_utf8() {
702        use crate::SymmetricCryptoKey;
703        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
704            SymmetricCryptoKey::make_xchacha20_poly1305_key()
705        else {
706            panic!("Failed to create XChaCha20Poly1305Key");
707        };
708
709        let plaintext = b"Hello, world!";
710        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
711        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
712        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
713    }
714
715    #[test]
716    fn test_encrypt_decrypt_xchacha20_roundtrip_pkcs8() {
717        use crate::SymmetricCryptoKey;
718        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
719            SymmetricCryptoKey::make_xchacha20_poly1305_key()
720        else {
721            panic!("Failed to create XChaCha20Poly1305Key");
722        };
723
724        let plaintext = b"Hello, world!";
725        let encrypted =
726            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
727        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
728        assert_eq!(
729            decrypted,
730            (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
731        );
732    }
733
734    #[test]
735    fn test_encrypt_decrypt_xchacha20_roundtrip_cosekey() {
736        use crate::SymmetricCryptoKey;
737        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
738            SymmetricCryptoKey::make_xchacha20_poly1305_key()
739        else {
740            panic!("Failed to create XChaCha20Poly1305Key");
741        };
742
743        let plaintext = b"Hello, world!";
744        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
745        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
746        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
747    }
748
749    #[test]
750    fn test_decrypt_xchacha20_test_vector() {
751        let key = make_xchacha_key();
752        let decrypted =
753            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
754                .unwrap();
755        assert_eq!(
756            decrypted,
757            (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
758        );
759    }
760
761    #[test]
762    fn test_decrypt_xchacha20_fail_wrong_key_id() {
763        let key = XChaCha20Poly1305Key {
764            key_id: KeyId::from([1; 16]),
765            enc_key: Box::pin(Array::from(KEY_DATA)),
766            supported_operations: vec![
767                KeyOperation::Decrypt,
768                KeyOperation::Encrypt,
769                KeyOperation::WrapKey,
770                KeyOperation::UnwrapKey,
771            ],
772        };
773        assert!(matches!(
774            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
775            Err(CryptoError::WrongCoseKeyId)
776        ));
777    }
778
779    #[test]
780    fn test_decrypt_xchacha20_fail_wrong_algorithm() {
781        use coset::iana;
782        let protected_header = coset::HeaderBuilder::new()
783            .algorithm(iana::Algorithm::A256GCM)
784            .key_id(KEY_ID.to_vec())
785            .build();
786        let nonce = [0u8; 16];
787        let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
788            .protected(protected_header)
789            .create_ciphertext(&[], &[], |_, _| Vec::new())
790            .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
791            .build();
792        let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
793
794        let key = make_xchacha_key();
795        assert!(matches!(
796            decrypt_xchacha20_poly1305(&serialized_message, &key),
797            Err(CryptoError::WrongKeyType)
798        ));
799    }
800}