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