Skip to main content

bitwarden_crypto/
cose.rs

1//! This file contains private-use constants for COSE encoded key types and algorithms.
2//! Standardized values from <https://www.iana.org/assignments/cose/cose.xhtml> should always be preferred
3//! unless there is a a clear benefit, such as a clear cryptographic benefit, which MUST
4//! be documented publicly.
5
6use std::fmt::Debug;
7
8use coset::{
9    CborSerializable, ContentType, CoseEncrypt0, CoseEncrypt0Builder, Header, Label,
10    iana::{self, CoapContentFormat, KeyOperation},
11};
12use generic_array::GenericArray;
13use thiserror::Error;
14use tracing::instrument;
15use typenum::U32;
16
17use crate::{
18    ContentFormat, CoseEncrypt0Bytes, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
19    content_format::{Bytes, ConstContentFormat, CoseContentFormat},
20    error::{EncStringParseError, EncodingError},
21    xchacha20,
22};
23
24// Custom COSE algorithm values
25// NOTE: Any algorithm value below -65536 is reserved for private use in the IANA allocations and
26// can be used freely.
27/// XChaCha20 <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03> is used over ChaCha20
28/// to be able to randomly generate nonces, and to not have to worry about key wearout. Since
29/// the draft was never published as an RFC, we use a private-use value for the algorithm.
30pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
31pub(crate) const ALG_ARGON2ID13: i64 = -71000;
32
33// Custom labels for COSE headers
34// NOTE: Any label below -65536 is reserved for private use in the IANA allocations and can be used
35// freely.
36pub(crate) const ARGON2_SALT: i64 = -71001;
37pub(crate) const ARGON2_ITERATIONS: i64 = -71002;
38pub(crate) const ARGON2_MEMORY: i64 = -71003;
39pub(crate) const ARGON2_PARALLELISM: i64 = -71004;
40/// Indicates for any object containing a key (wrapped key, password protected key envelope) which
41/// key ID that contained key has
42pub(crate) const CONTAINED_KEY_ID: i64 = -71005;
43
44// Note: These are in the "unregistered" tree: https://datatracker.ietf.org/doc/html/rfc6838#section-3.4
45// These are only used within Bitwarden, and not meant for exchange with other systems.
46const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded";
47pub(crate) const CONTENT_TYPE_PADDED_CBOR: &str = "application/x.bitwarden.cbor-padded";
48const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key";
49const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-key";
50
51/// The label used for the namespace ensuring strong domain separation when using signatures.
52pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
53
54// Domain separation / Namespaces
55//
56// Cryptographic objects are strongly domain separated so that items can only be decrypted
57// in the correct context, making cryptographic analysis significantly easier and preventing
58// misuse of cryptographic objects. For this, there is a partitioning at two layers. First,
59// the object types are partitioned into e.g. EncString, DataEnvelope, Signature, KeyEnvelope, and
60// so on. Second, within each of these types, each of these spans their own namespace for usages.
61// For instance, a DataEnvelope may describe that the contained item is only valid as a vault item,
62// or as account settings.
63
64/// MUST be placed in the protected header of cose objects
65pub(crate) const SAFE_OBJECT_NAMESPACE: i64 = -80002;
66
67#[allow(clippy::enum_variant_names)]
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub(crate) enum SafeObjectNamespace {
70    PasswordProtectedKeyEnvelope = 1,
71    DataEnvelope = 2,
72    SymmetricKeyEnvelope = 3,
73    //Reserved:
74    //PrivateKeyEnvelope = 4,
75    //SigningKeyEnvelope = 5,
76}
77
78impl TryFrom<i128> for SafeObjectNamespace {
79    type Error = ();
80
81    fn try_from(value: i128) -> Result<Self, Self::Error> {
82        match value {
83            1 => Ok(SafeObjectNamespace::PasswordProtectedKeyEnvelope),
84            2 => Ok(SafeObjectNamespace::DataEnvelope),
85            3 => Ok(SafeObjectNamespace::SymmetricKeyEnvelope),
86            _ => Err(()),
87        }
88    }
89}
90
91impl From<SafeObjectNamespace> for i128 {
92    fn from(namespace: SafeObjectNamespace) -> Self {
93        namespace as i128
94    }
95}
96
97pub(crate) trait ContentNamespace: TryFrom<i128> + Into<i128> + PartialEq + Debug {}
98
99/// Each type of object has it's own namespace for strong domain separation to eliminate
100/// attacks which attempt to confuse object types. For signatures, this refers to signature
101/// namespaces, for data envelopes to data envelope namespaces and so on.
102pub(crate) const SAFE_CONTENT_NAMESPACE: i64 = -80001;
103
104const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;
105
106/// Encrypt a plaintext message with a given key
107pub(crate) fn encrypt_cose(
108    cose_encrypt0_builder: CoseEncrypt0Builder,
109    plaintext: &[u8],
110    key: &XChaCha20Poly1305Key,
111) -> CoseEncrypt0 {
112    let mut nonce = [0u8; xchacha20::NONCE_SIZE];
113    cose_encrypt0_builder
114        .create_ciphertext(plaintext, &[], |data, aad| {
115            let ciphertext =
116                crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
117            nonce = ciphertext.nonce();
118            ciphertext.encrypted_bytes().to_vec()
119        })
120        .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
121        .build()
122}
123
124pub struct DecryptFailed;
125/// Decrypt a CoseEncrypt0 message with a CoseKey
126pub(crate) fn decrypt_cose(
127    cose_encrypt0: &CoseEncrypt0,
128    key: &XChaCha20Poly1305Key,
129) -> Result<Vec<u8>, DecryptFailed> {
130    let nonce: [u8; xchacha20::NONCE_SIZE] = cose_encrypt0
131        .unprotected
132        .iv
133        .clone()
134        .try_into()
135        .map_err(|_| DecryptFailed)?;
136    cose_encrypt0
137        .clone()
138        .decrypt_ciphertext(
139            &[],
140            || CryptoError::MissingField("ciphertext"),
141            |data, aad| {
142                xchacha20::decrypt_xchacha20_poly1305(&nonce, &(*key.enc_key).into(), data, aad)
143            },
144        )
145        .map_err(|_| DecryptFailed)
146}
147
148/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message
149pub(crate) fn encrypt_xchacha20_poly1305(
150    plaintext: &[u8],
151    key: &crate::XChaCha20Poly1305Key,
152    content_format: ContentFormat,
153) -> Result<CoseEncrypt0Bytes, CryptoError> {
154    let mut plaintext = plaintext.to_vec();
155
156    let header_builder: coset::HeaderBuilder = content_format.into();
157    let mut protected_header = header_builder
158        .key_id(key.key_id.as_slice().to_vec())
159        .build();
160    // This should be adjusted to use the builder pattern once implemented in coset.
161    // The related coset upstream issue is:
162    // https://github.com/google/coset/issues/105
163    protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
164
165    if should_pad_content(&content_format) {
166        // Pad the data to a block size in order to hide plaintext length
167        let min_length =
168            XCHACHA20_TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / XCHACHA20_TEXT_PAD_BLOCK_SIZE));
169        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
170    }
171
172    let mut nonce = [0u8; xchacha20::NONCE_SIZE];
173    let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
174        .protected(protected_header)
175        .create_ciphertext(&plaintext, &[], |data, aad| {
176            let ciphertext =
177                crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
178            nonce = ciphertext.nonce();
179            ciphertext.encrypted_bytes().to_vec()
180        })
181        .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
182        .build();
183
184    cose_encrypt0
185        .to_vec()
186        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
187        .map(CoseEncrypt0Bytes::from)
188}
189
190/// Decrypts a COSE Encrypt0 message, using a XChaCha20Poly1305 key
191pub(crate) fn decrypt_xchacha20_poly1305(
192    cose_encrypt0_message: &CoseEncrypt0Bytes,
193    key: &crate::XChaCha20Poly1305Key,
194) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
195    let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message.as_ref())
196        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
197
198    let Some(ref alg) = msg.protected.header.alg else {
199        return Err(CryptoError::EncString(
200            EncStringParseError::CoseMissingAlgorithm,
201        ));
202    };
203
204    if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
205        return Err(CryptoError::WrongKeyType);
206    }
207
208    let content_format = ContentFormat::try_from(&msg.protected.header)
209        .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
210
211    if key.key_id.as_slice() != msg.protected.header.key_id {
212        return Err(CryptoError::WrongCoseKeyId);
213    }
214
215    let decrypted_message = msg.decrypt_ciphertext(
216        &[],
217        || CryptoError::MissingField("ciphertext"),
218        |data, aad| {
219            let nonce = msg.unprotected.iv.as_slice();
220            crate::xchacha20::decrypt_xchacha20_poly1305(
221                nonce
222                    .try_into()
223                    .map_err(|_| CryptoError::InvalidNonceLength)?,
224                &(*key.enc_key).into(),
225                data,
226                aad,
227            )
228        },
229    )?;
230
231    if should_pad_content(&content_format) {
232        // Unpad the data to get the original plaintext
233        let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
234        return Ok((data.to_vec(), content_format));
235    }
236
237    Ok((decrypted_message, content_format))
238}
239
240const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
241
242impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
243    type Error = CryptoError;
244
245    #[instrument(err, skip_all)]
246    fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
247        let key_bytes = cose_key
248            .params
249            .iter()
250            .find_map(|(label, value)| match (label, value) {
251                (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
252                _ => None,
253            })
254            .ok_or(CryptoError::InvalidKey)?;
255        let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
256        let key_opts = cose_key
257            .key_ops
258            .iter()
259            .map(|op| match op {
260                coset::RegisteredLabel::Assigned(iana::KeyOperation::Encrypt) => {
261                    Ok(KeyOperation::Encrypt)
262                }
263                coset::RegisteredLabel::Assigned(iana::KeyOperation::Decrypt) => {
264                    Ok(KeyOperation::Decrypt)
265                }
266                coset::RegisteredLabel::Assigned(iana::KeyOperation::WrapKey) => {
267                    Ok(KeyOperation::WrapKey)
268                }
269                coset::RegisteredLabel::Assigned(iana::KeyOperation::UnwrapKey) => {
270                    Ok(KeyOperation::UnwrapKey)
271                }
272                _ => Err(CryptoError::InvalidKey),
273            })
274            .collect::<Result<Vec<KeyOperation>, CryptoError>>()?;
275
276        match alg {
277            coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
278                // Ensure the length is correct since `GenericArray::clone_from_slice` panics if it
279                // receives the wrong length.
280                if key_bytes.len() != xchacha20::KEY_SIZE {
281                    return Err(CryptoError::InvalidKey);
282                }
283                let enc_key = Box::pin(GenericArray::<u8, U32>::clone_from_slice(key_bytes));
284                let key_id = cose_key
285                    .key_id
286                    .as_slice()
287                    .try_into()
288                    .map_err(|_| CryptoError::InvalidKey)?;
289                Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
290                    XChaCha20Poly1305Key {
291                        enc_key,
292                        key_id,
293                        supported_operations: key_opts,
294                    },
295                ))
296            }
297            _ => Err(CryptoError::InvalidKey),
298        }
299    }
300}
301
302impl From<ContentFormat> for coset::HeaderBuilder {
303    fn from(format: ContentFormat) -> Self {
304        let header_builder = coset::HeaderBuilder::new();
305
306        match format {
307            ContentFormat::Utf8 => {
308                header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string())
309            }
310            ContentFormat::Pkcs8PrivateKey => {
311                header_builder.content_format(CoapContentFormat::Pkcs8)
312            }
313            ContentFormat::SPKIPublicKeyDer => {
314                header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string())
315            }
316            ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
317            ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
318            ContentFormat::CoseEncrypt0 => {
319                header_builder.content_format(CoapContentFormat::CoseEncrypt0)
320            }
321            ContentFormat::BitwardenLegacyKey => {
322                header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
323            }
324            ContentFormat::OctetStream => {
325                header_builder.content_format(CoapContentFormat::OctetStream)
326            }
327            ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor),
328        }
329    }
330}
331
332impl TryFrom<&coset::Header> for ContentFormat {
333    type Error = CryptoError;
334
335    fn try_from(header: &coset::Header) -> Result<Self, Self::Error> {
336        match header.content_type.as_ref() {
337            Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => {
338                Ok(ContentFormat::Utf8)
339            }
340            Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => {
341                Ok(ContentFormat::BitwardenLegacyKey)
342            }
343            Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => {
344                Ok(ContentFormat::SPKIPublicKeyDer)
345            }
346            Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => {
347                Ok(ContentFormat::Pkcs8PrivateKey)
348            }
349            Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey),
350            Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
351                Ok(ContentFormat::OctetStream)
352            }
353            Some(ContentType::Assigned(CoapContentFormat::Cbor)) => Ok(ContentFormat::Cbor),
354            _ => Err(CryptoError::EncString(
355                EncStringParseError::CoseMissingContentType,
356            )),
357        }
358    }
359}
360
361fn should_pad_content(format: &ContentFormat) -> bool {
362    matches!(format, ContentFormat::Utf8)
363}
364
365/// Trait for structs that are serializable to COSE objects.
366pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
367    /// Serializes the struct to COSE serialization
368    fn to_cose(&self) -> Bytes<T>;
369    /// Deserializes a serialized COSE object to a struct
370    fn from_cose(bytes: &Bytes<T>) -> Result<Self, EncodingError>
371    where
372        Self: Sized;
373}
374
375pub(crate) fn extract_integer(
376    header: &Header,
377    target_label: i64,
378    value_name: &str,
379) -> Result<i128, CoseExtractError> {
380    header
381        .rest
382        .iter()
383        .find_map(|(label, value)| match (label, value) {
384            (Label::Int(label_value), ciborium::Value::Integer(int_value))
385                if *label_value == target_label =>
386            {
387                Some(*int_value)
388            }
389            _ => None,
390        })
391        .map(Into::into)
392        .ok_or_else(|| CoseExtractError::MissingValue(value_name.to_string()))
393}
394
395pub(crate) fn extract_bytes(
396    header: &Header,
397    target_label: i64,
398    value_name: &str,
399) -> Result<Vec<u8>, CoseExtractError> {
400    header
401        .rest
402        .iter()
403        .find_map(|(label, value)| match (label, value) {
404            (Label::Int(label_value), ciborium::Value::Bytes(byte_value))
405                if *label_value == target_label =>
406            {
407                Some(byte_value.clone())
408            }
409            _ => None,
410        })
411        .ok_or(CoseExtractError::MissingValue(value_name.to_string()))
412}
413
414#[derive(Debug, Error)]
415pub(crate) enum CoseExtractError {
416    #[error("Missing value {0}")]
417    MissingValue(String),
418}
419
420/// Helper function to convert a COSE KeyOperation to a debug string
421pub(crate) fn debug_key_operation(key_operation: KeyOperation) -> &'static str {
422    match key_operation {
423        KeyOperation::Sign => "Sign",
424        KeyOperation::Verify => "Verify",
425        KeyOperation::Encrypt => "Encrypt",
426        KeyOperation::Decrypt => "Decrypt",
427        KeyOperation::WrapKey => "WrapKey",
428        KeyOperation::UnwrapKey => "UnwrapKey",
429        KeyOperation::DeriveKey => "DeriveKey",
430        KeyOperation::DeriveBits => "DeriveBits",
431        _ => "Unknown",
432    }
433}
434
435#[cfg(test)]
436mod test {
437    use super::*;
438    use crate::keys::KeyId;
439
440    const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
441    const KEY_DATA: [u8; 32] = [
442        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
443        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
444        0x1e, 0x1f,
445    ];
446    const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
447    const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
448        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,
449        11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
450        75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
451        172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
452        13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
453    ];
454
455    #[test]
456    fn test_encrypt_decrypt_roundtrip_octetstream() {
457        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
458            SymmetricCryptoKey::make_xchacha20_poly1305_key()
459        else {
460            panic!("Failed to create XChaCha20Poly1305Key");
461        };
462
463        let plaintext = b"Hello, world!";
464        let encrypted =
465            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
466        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
467        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
468    }
469
470    #[test]
471    fn test_encrypt_decrypt_roundtrip_utf8() {
472        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
473            SymmetricCryptoKey::make_xchacha20_poly1305_key()
474        else {
475            panic!("Failed to create XChaCha20Poly1305Key");
476        };
477
478        let plaintext = b"Hello, world!";
479        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
480        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
481        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
482    }
483
484    #[test]
485    fn test_encrypt_decrypt_roundtrip_pkcs8() {
486        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
487            SymmetricCryptoKey::make_xchacha20_poly1305_key()
488        else {
489            panic!("Failed to create XChaCha20Poly1305Key");
490        };
491
492        let plaintext = b"Hello, world!";
493        let encrypted =
494            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
495        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
496        assert_eq!(
497            decrypted,
498            (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
499        );
500    }
501
502    #[test]
503    fn test_encrypt_decrypt_roundtrip_cosekey() {
504        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
505            SymmetricCryptoKey::make_xchacha20_poly1305_key()
506        else {
507            panic!("Failed to create XChaCha20Poly1305Key");
508        };
509
510        let plaintext = b"Hello, world!";
511        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
512        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
513        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
514    }
515
516    #[test]
517    fn test_decrypt_test_vector() {
518        let key = XChaCha20Poly1305Key {
519            key_id: KeyId::from(KEY_ID),
520            enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
521            supported_operations: vec![
522                KeyOperation::Decrypt,
523                KeyOperation::Encrypt,
524                KeyOperation::WrapKey,
525                KeyOperation::UnwrapKey,
526            ],
527        };
528        let decrypted =
529            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
530                .unwrap();
531        assert_eq!(
532            decrypted,
533            (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
534        );
535    }
536
537    #[test]
538    fn test_fail_wrong_key_id() {
539        let key = XChaCha20Poly1305Key {
540            key_id: KeyId::from([1; 16]), // Different key ID
541            enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
542            supported_operations: vec![
543                KeyOperation::Decrypt,
544                KeyOperation::Encrypt,
545                KeyOperation::WrapKey,
546                KeyOperation::UnwrapKey,
547            ],
548        };
549        assert!(matches!(
550            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
551            Err(CryptoError::WrongCoseKeyId)
552        ));
553    }
554
555    #[test]
556    fn test_fail_wrong_algorithm() {
557        let protected_header = coset::HeaderBuilder::new()
558            .algorithm(iana::Algorithm::A256GCM)
559            .key_id(KEY_ID.to_vec())
560            .build();
561        let nonce = [0u8; 16];
562        let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
563            .protected(protected_header)
564            .create_ciphertext(&[], &[], |_, _| Vec::new())
565            .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
566            .build();
567        let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
568
569        let key = XChaCha20Poly1305Key {
570            key_id: KeyId::from(KEY_ID),
571            enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
572            supported_operations: vec![
573                KeyOperation::Decrypt,
574                KeyOperation::Encrypt,
575                KeyOperation::WrapKey,
576                KeyOperation::UnwrapKey,
577            ],
578        };
579        assert!(matches!(
580            decrypt_xchacha20_poly1305(&serialized_message, &key),
581            Err(CryptoError::WrongKeyType)
582        ));
583    }
584}