1use coset::{
7 iana::{self, CoapContentFormat},
8 CborSerializable, ContentType, Label,
9};
10use generic_array::GenericArray;
11use typenum::U32;
12
13use crate::{
14 content_format::{Bytes, ConstContentFormat, CoseContentFormat},
15 error::{EncStringParseError, EncodingError},
16 xchacha20, ContentFormat, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
17};
18
19pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
23const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;
24
25const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded";
28const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key";
29const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-key";
30
31pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
35
36pub(crate) fn encrypt_xchacha20_poly1305(
38 plaintext: &[u8],
39 key: &crate::XChaCha20Poly1305Key,
40 content_format: ContentFormat,
41) -> Result<Vec<u8>, CryptoError> {
42 let mut plaintext = plaintext.to_vec();
43
44 let header_builder: coset::HeaderBuilder = content_format.into();
45 let mut protected_header = header_builder.key_id(key.key_id.to_vec()).build();
46 protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
50
51 if should_pad_content(&content_format) {
52 crate::keys::utils::pad_bytes(&mut plaintext, XCHACHA20_TEXT_PAD_BLOCK_SIZE);
54 }
55
56 let mut nonce = [0u8; xchacha20::NONCE_SIZE];
57 let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
58 .protected(protected_header)
59 .create_ciphertext(&plaintext, &[], |data, aad| {
60 let ciphertext =
61 crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
62 nonce = ciphertext.nonce();
63 ciphertext.encrypted_bytes().to_vec()
64 })
65 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
66 .build();
67
68 cose_encrypt0
69 .to_vec()
70 .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
71}
72
73pub(crate) fn decrypt_xchacha20_poly1305(
75 cose_encrypt0_message: &[u8],
76 key: &crate::XChaCha20Poly1305Key,
77) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
78 let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message)
79 .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
80
81 let Some(ref alg) = msg.protected.header.alg else {
82 return Err(CryptoError::EncString(
83 EncStringParseError::CoseMissingAlgorithm,
84 ));
85 };
86
87 if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
88 return Err(CryptoError::WrongKeyType);
89 }
90
91 let content_format = ContentFormat::try_from(&msg.protected.header)
92 .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
93
94 if key.key_id != *msg.protected.header.key_id {
95 return Err(CryptoError::WrongCoseKeyId);
96 }
97
98 let decrypted_message = msg.decrypt(&[], |data, aad| {
99 let nonce = msg.unprotected.iv.as_slice();
100 crate::xchacha20::decrypt_xchacha20_poly1305(
101 nonce
102 .try_into()
103 .map_err(|_| CryptoError::InvalidNonceLength)?,
104 &(*key.enc_key).into(),
105 data,
106 aad,
107 )
108 })?;
109
110 if should_pad_content(&content_format) {
111 let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
113 return Ok((data.to_vec(), content_format));
114 }
115
116 Ok((decrypted_message, content_format))
117}
118
119const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
120
121impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
122 type Error = CryptoError;
123
124 fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
125 let key_bytes = cose_key
126 .params
127 .iter()
128 .find_map(|(label, value)| match (label, value) {
129 (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
130 _ => None,
131 })
132 .ok_or(CryptoError::InvalidKey)?;
133 let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
134
135 match alg {
136 coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
137 if key_bytes.len() != xchacha20::KEY_SIZE {
140 return Err(CryptoError::InvalidKey);
141 }
142 let enc_key = Box::pin(GenericArray::<u8, U32>::clone_from_slice(key_bytes));
143 let key_id = cose_key
144 .key_id
145 .as_slice()
146 .try_into()
147 .map_err(|_| CryptoError::InvalidKey)?;
148 Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
149 XChaCha20Poly1305Key { enc_key, key_id },
150 ))
151 }
152 _ => Err(CryptoError::InvalidKey),
153 }
154 }
155}
156
157impl From<ContentFormat> for coset::HeaderBuilder {
158 fn from(format: ContentFormat) -> Self {
159 let header_builder = coset::HeaderBuilder::new();
160
161 match format {
162 ContentFormat::Utf8 => {
163 header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string())
164 }
165 ContentFormat::Pkcs8PrivateKey => {
166 header_builder.content_format(CoapContentFormat::Pkcs8)
167 }
168 ContentFormat::SPKIPublicKeyDer => {
169 header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string())
170 }
171 ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
172 ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
173 ContentFormat::BitwardenLegacyKey => {
174 header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
175 }
176 ContentFormat::OctetStream => {
177 header_builder.content_format(CoapContentFormat::OctetStream)
178 }
179 }
180 }
181}
182
183impl TryFrom<&coset::Header> for ContentFormat {
184 type Error = CryptoError;
185
186 fn try_from(header: &coset::Header) -> Result<Self, Self::Error> {
187 match header.content_type.as_ref() {
188 Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => {
189 Ok(ContentFormat::Utf8)
190 }
191 Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => {
192 Ok(ContentFormat::BitwardenLegacyKey)
193 }
194 Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => {
195 Ok(ContentFormat::SPKIPublicKeyDer)
196 }
197 Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => {
198 Ok(ContentFormat::Pkcs8PrivateKey)
199 }
200 Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey),
201 Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
202 Ok(ContentFormat::OctetStream)
203 }
204 _ => Err(CryptoError::EncString(
205 EncStringParseError::CoseMissingContentType,
206 )),
207 }
208 }
209}
210
211fn should_pad_content(format: &ContentFormat) -> bool {
212 matches!(format, ContentFormat::Utf8)
213}
214
215pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
217 fn to_cose(&self) -> Bytes<T>;
219 fn from_cose(bytes: &Bytes<T>) -> Result<Self, EncodingError>
221 where
222 Self: Sized;
223}
224#[cfg(test)]
225mod test {
226 use super::*;
227
228 const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
229 const KEY_DATA: [u8; 32] = [
230 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
231 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
232 0x1e, 0x1f,
233 ];
234 const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
235 const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
236 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,
237 11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
238 75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
239 172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
240 13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
241 ];
242
243 #[test]
244 fn test_encrypt_decrypt_roundtrip_octetstream() {
245 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
246 SymmetricCryptoKey::make_xchacha20_poly1305_key()
247 else {
248 panic!("Failed to create XChaCha20Poly1305Key");
249 };
250
251 let plaintext = b"Hello, world!";
252 let encrypted =
253 encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
254 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
255 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
256 }
257
258 #[test]
259 fn test_encrypt_decrypt_roundtrip_utf8() {
260 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
261 SymmetricCryptoKey::make_xchacha20_poly1305_key()
262 else {
263 panic!("Failed to create XChaCha20Poly1305Key");
264 };
265
266 let plaintext = b"Hello, world!";
267 let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
268 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
269 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
270 }
271
272 #[test]
273 fn test_encrypt_decrypt_roundtrip_pkcs8() {
274 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
275 SymmetricCryptoKey::make_xchacha20_poly1305_key()
276 else {
277 panic!("Failed to create XChaCha20Poly1305Key");
278 };
279
280 let plaintext = b"Hello, world!";
281 let encrypted =
282 encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
283 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
284 assert_eq!(
285 decrypted,
286 (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
287 );
288 }
289
290 #[test]
291 fn test_encrypt_decrypt_roundtrip_cosekey() {
292 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
293 SymmetricCryptoKey::make_xchacha20_poly1305_key()
294 else {
295 panic!("Failed to create XChaCha20Poly1305Key");
296 };
297
298 let plaintext = b"Hello, world!";
299 let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
300 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
301 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
302 }
303
304 #[test]
305 fn test_decrypt_test_vector() {
306 let key = XChaCha20Poly1305Key {
307 key_id: KEY_ID,
308 enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
309 };
310 let decrypted = decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key).unwrap();
311 assert_eq!(
312 decrypted,
313 (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
314 );
315 }
316
317 #[test]
318 fn test_fail_wrong_key_id() {
319 let key = XChaCha20Poly1305Key {
320 key_id: [1; 16], enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
322 };
323 assert!(matches!(
324 decrypt_xchacha20_poly1305(TEST_VECTOR_COSE_ENCRYPT0, &key),
325 Err(CryptoError::WrongCoseKeyId)
326 ));
327 }
328
329 #[test]
330 fn test_fail_wrong_algorithm() {
331 let protected_header = coset::HeaderBuilder::new()
332 .algorithm(iana::Algorithm::A256GCM)
333 .key_id(KEY_ID.to_vec())
334 .build();
335 let nonce = [0u8; 16];
336 let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
337 .protected(protected_header)
338 .create_ciphertext(&[], &[], |_, _| Vec::new())
339 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
340 .build();
341 let serialized_message = cose_encrypt0.to_vec().unwrap();
342
343 let key = XChaCha20Poly1305Key {
344 key_id: KEY_ID,
345 enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
346 };
347 assert!(matches!(
348 decrypt_xchacha20_poly1305(&serialized_message, &key),
349 Err(CryptoError::WrongKeyType)
350 ));
351 }
352}