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