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, xchacha20, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
12};
13
14/// XChaCha20 <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03> is used over ChaCha20
15/// to be able to randomly generate nonces, and to not have to worry about key wearout. Since
16/// the draft was never published as an RFC, we use a private-use value for the algorithm.
17pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
18
19/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message
20pub(crate) fn encrypt_xchacha20_poly1305(
21    plaintext: &[u8],
22    key: &crate::XChaCha20Poly1305Key,
23) -> Result<Vec<u8>, CryptoError> {
24    let mut protected_header = coset::HeaderBuilder::new().build();
25    // This should be adjusted to use the builder pattern once implemented in coset.
26    // The related coset upstream issue is:
27    // https://github.com/google/coset/issues/105
28    protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
29
30    let mut nonce = [0u8; xchacha20::NONCE_SIZE];
31    let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
32        .protected(protected_header)
33        .create_ciphertext(plaintext, &[], |data, aad| {
34            let ciphertext =
35                crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
36            nonce = ciphertext.nonce();
37            ciphertext.encrypted_bytes().to_vec()
38        })
39        .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
40        .build();
41
42    cose_encrypt0
43        .to_vec()
44        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
45}
46
47/// Decrypts a COSE Encrypt0 message, using a XChaCha20Poly1305 key
48pub(crate) fn decrypt_xchacha20_poly1305(
49    cose_encrypt0_message: &[u8],
50    key: &crate::XChaCha20Poly1305Key,
51) -> Result<Vec<u8>, CryptoError> {
52    let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message)
53        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
54    let Some(ref alg) = msg.protected.header.alg else {
55        return Err(CryptoError::EncString(
56            EncStringParseError::CoseMissingAlgorithm,
57        ));
58    };
59    if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
60        return Err(CryptoError::WrongKeyType);
61    }
62
63    let decrypted_message = msg.decrypt(&[], |data, aad| {
64        let nonce = msg.unprotected.iv.as_slice();
65        crate::xchacha20::decrypt_xchacha20_poly1305(
66            nonce
67                .try_into()
68                .map_err(|_| CryptoError::InvalidNonceLength)?,
69            &(*key.enc_key).into(),
70            data,
71            aad,
72        )
73    })?;
74    Ok(decrypted_message)
75}
76
77const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
78
79impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
80    type Error = CryptoError;
81
82    fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
83        let key_bytes = cose_key
84            .params
85            .iter()
86            .find_map(|(label, value)| match (label, value) {
87                (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
88                _ => None,
89            })
90            .ok_or(CryptoError::InvalidKey)?;
91        let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
92
93        match alg {
94            coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
95                // Ensure the length is correct since `GenericArray::clone_from_slice` panics if it
96                // receives the wrong length.
97                if key_bytes.len() != xchacha20::KEY_SIZE {
98                    return Err(CryptoError::InvalidKey);
99                }
100                let enc_key = Box::pin(GenericArray::<u8, U32>::clone_from_slice(key_bytes));
101                let key_id = cose_key
102                    .key_id
103                    .as_slice()
104                    .try_into()
105                    .map_err(|_| CryptoError::InvalidKey)?;
106                Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
107                    XChaCha20Poly1305Key { enc_key, key_id },
108                ))
109            }
110            _ => Err(CryptoError::InvalidKey),
111        }
112    }
113}
114
115#[cfg(test)]
116mod test {
117    use super::*;
118
119    #[test]
120    fn test_encrypt_decrypt_roundtrip() {
121        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
122            SymmetricCryptoKey::make_xchacha20_poly1305_key()
123        else {
124            panic!("Failed to create XChaCha20Poly1305Key");
125        };
126
127        let plaintext = b"Hello, world!";
128        let encrypted = encrypt_xchacha20_poly1305(plaintext, key).unwrap();
129        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
130        assert_eq!(decrypted, plaintext);
131    }
132}