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