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