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 coset::{iana, CborSerializable, Label};
7use generic_array::GenericArray;
8use typenum::U32;
9
10use crate::{
11    error::{EncStringParseError, EncodingError},
12    xchacha20, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
13};
14
15/// XChaCha20 <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03> is used over ChaCha20
16/// to be able to randomly generate nonces, and to not have to worry about key wearout. Since
17/// the draft was never published as an RFC, we use a private-use value for the algorithm.
18pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
19
20// Labels
21//
22/// The label used for the namespace ensuring strong domain separation when using signatures.
23pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
24
25/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message
26pub(crate) fn encrypt_xchacha20_poly1305(
27    plaintext: &[u8],
28    key: &crate::XChaCha20Poly1305Key,
29) -> Result<Vec<u8>, CryptoError> {
30    let mut protected_header = coset::HeaderBuilder::new()
31        .key_id(key.key_id.to_vec())
32        .build();
33    // This should be adjusted to use the builder pattern once implemented in coset.
34    // The related coset upstream issue is:
35    // https://github.com/google/coset/issues/105
36    protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
37
38    let mut nonce = [0u8; xchacha20::NONCE_SIZE];
39    let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
40        .protected(protected_header)
41        .create_ciphertext(plaintext, &[], |data, aad| {
42            let ciphertext =
43                crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
44            nonce = ciphertext.nonce();
45            ciphertext.encrypted_bytes().to_vec()
46        })
47        .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
48        .build();
49
50    cose_encrypt0
51        .to_vec()
52        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
53}
54
55/// Decrypts a COSE Encrypt0 message, using a XChaCha20Poly1305 key
56pub(crate) fn decrypt_xchacha20_poly1305(
57    cose_encrypt0_message: &[u8],
58    key: &crate::XChaCha20Poly1305Key,
59) -> Result<Vec<u8>, CryptoError> {
60    let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message)
61        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
62    let Some(ref alg) = msg.protected.header.alg else {
63        return Err(CryptoError::EncString(
64            EncStringParseError::CoseMissingAlgorithm,
65        ));
66    };
67    if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
68        return Err(CryptoError::WrongKeyType);
69    }
70    if key.key_id != *msg.protected.header.key_id {
71        return Err(CryptoError::WrongCoseKeyId);
72    }
73
74    let decrypted_message = msg.decrypt(&[], |data, aad| {
75        let nonce = msg.unprotected.iv.as_slice();
76        crate::xchacha20::decrypt_xchacha20_poly1305(
77            nonce
78                .try_into()
79                .map_err(|_| CryptoError::InvalidNonceLength)?,
80            &(*key.enc_key).into(),
81            data,
82            aad,
83        )
84    })?;
85    Ok(decrypted_message)
86}
87
88const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
89
90impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
91    type Error = CryptoError;
92
93    fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
94        let key_bytes = cose_key
95            .params
96            .iter()
97            .find_map(|(label, value)| match (label, value) {
98                (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
99                _ => None,
100            })
101            .ok_or(CryptoError::InvalidKey)?;
102        let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
103
104        match alg {
105            coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
106                // Ensure the length is correct since `GenericArray::clone_from_slice` panics if it
107                // receives the wrong length.
108                if key_bytes.len() != xchacha20::KEY_SIZE {
109                    return Err(CryptoError::InvalidKey);
110                }
111                let enc_key = Box::pin(GenericArray::<u8, U32>::clone_from_slice(key_bytes));
112                let key_id = cose_key
113                    .key_id
114                    .as_slice()
115                    .try_into()
116                    .map_err(|_| CryptoError::InvalidKey)?;
117                Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
118                    XChaCha20Poly1305Key { enc_key, key_id },
119                ))
120            }
121            _ => Err(CryptoError::InvalidKey),
122        }
123    }
124}
125
126/// Trait for structs that are serializable to COSE objects.
127pub trait CoseSerializable {
128    /// Serializes the struct to COSE serialization
129    fn to_cose(&self) -> Vec<u8>;
130    /// Deserializes a serialized COSE object to a struct
131    fn from_cose(bytes: &[u8]) -> Result<Self, EncodingError>
132    where
133        Self: Sized;
134}
135#[cfg(test)]
136mod test {
137    use super::*;
138
139    const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
140    const KEY_DATA: [u8; 32] = [
141        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
142        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
143        0x1e, 0x1f,
144    ];
145    const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
146    const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
147        131, 88, 25, 162, 1, 58, 0, 1, 17, 111, 4, 80, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
148        13, 14, 15, 161, 5, 88, 24, 39, 48, 159, 48, 215, 77, 21, 100, 241, 209, 216, 65, 99, 221,
149        83, 63, 118, 204, 200, 175, 126, 202, 53, 33, 88, 35, 218, 136, 132, 223, 131, 246, 169,
150        120, 134, 49, 56, 173, 169, 133, 232, 109, 248, 101, 59, 226, 90, 97, 210, 181, 76, 68,
151        158, 159, 94, 65, 67, 23, 112, 253, 83,
152    ];
153
154    #[test]
155    fn test_encrypt_decrypt_roundtrip() {
156        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
157            SymmetricCryptoKey::make_xchacha20_poly1305_key()
158        else {
159            panic!("Failed to create XChaCha20Poly1305Key");
160        };
161
162        let plaintext = b"Hello, world!";
163        let encrypted = encrypt_xchacha20_poly1305(plaintext, key).unwrap();
164        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
165        assert_eq!(decrypted, plaintext);
166    }
167
168    #[test]
169    fn test_decrypt_test_vector() {
170        let key = XChaCha20Poly1305Key {
171            key_id: KEY_ID,
172            enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
173        };
174        let decrypted = decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key).unwrap();
175        assert_eq!(decrypted, TEST_VECTOR_PLAINTEXT);
176    }
177
178    #[test]
179    fn test_fail_wrong_key_id() {
180        let key = XChaCha20Poly1305Key {
181            key_id: [1; 16], // Different key ID
182            enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
183        };
184        assert!(matches!(
185            decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key),
186            Err(CryptoError::WrongCoseKeyId)
187        ));
188    }
189
190    #[test]
191    fn test_fail_wrong_algorithm() {
192        let protected_header = coset::HeaderBuilder::new()
193            .algorithm(iana::Algorithm::A256GCM)
194            .key_id(KEY_ID.to_vec())
195            .build();
196        let nonce = [0u8; 16];
197        let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
198            .protected(protected_header)
199            .create_ciphertext(&[], &[], |_, _| Vec::new())
200            .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
201            .build();
202        let serialized_message = cose_encrypt0.to_vec().unwrap();
203
204        let key = XChaCha20Poly1305Key {
205            key_id: KEY_ID,
206            enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
207        };
208        assert!(matches!(
209            decrypt_xchacha20_poly1305(&serialized_message, &key),
210            Err(CryptoError::WrongKeyType)
211        ));
212    }
213}