1use std::fmt::Debug;
7
8use coset::{
9 CborSerializable, ContentType, CoseEncrypt0, CoseEncrypt0Builder, Header, Label,
10 iana::{self, CoapContentFormat, KeyOperation},
11};
12use generic_array::GenericArray;
13use thiserror::Error;
14use tracing::instrument;
15use typenum::U32;
16
17use crate::{
18 ContentFormat, CoseEncrypt0Bytes, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
19 content_format::{Bytes, ConstContentFormat, CoseContentFormat},
20 error::{EncStringParseError, EncodingError},
21 xchacha20,
22};
23
24pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
31pub(crate) const ALG_ARGON2ID13: i64 = -71000;
32
33pub(crate) const ARGON2_SALT: i64 = -71001;
37pub(crate) const ARGON2_ITERATIONS: i64 = -71002;
38pub(crate) const ARGON2_MEMORY: i64 = -71003;
39pub(crate) const ARGON2_PARALLELISM: i64 = -71004;
40pub(crate) const CONTAINED_KEY_ID: i64 = -71005;
43
44const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded";
47pub(crate) const CONTENT_TYPE_PADDED_CBOR: &str = "application/x.bitwarden.cbor-padded";
48const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key";
49const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-key";
50
51pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
53
54pub(crate) const SAFE_OBJECT_NAMESPACE: i64 = -80002;
66
67#[allow(clippy::enum_variant_names)]
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub(crate) enum SafeObjectNamespace {
70 PasswordProtectedKeyEnvelope = 1,
71 DataEnvelope = 2,
72 SymmetricKeyEnvelope = 3,
73 }
77
78impl TryFrom<i128> for SafeObjectNamespace {
79 type Error = ();
80
81 fn try_from(value: i128) -> Result<Self, Self::Error> {
82 match value {
83 1 => Ok(SafeObjectNamespace::PasswordProtectedKeyEnvelope),
84 2 => Ok(SafeObjectNamespace::DataEnvelope),
85 3 => Ok(SafeObjectNamespace::SymmetricKeyEnvelope),
86 _ => Err(()),
87 }
88 }
89}
90
91impl From<SafeObjectNamespace> for i128 {
92 fn from(namespace: SafeObjectNamespace) -> Self {
93 namespace as i128
94 }
95}
96
97pub(crate) trait ContentNamespace: TryFrom<i128> + Into<i128> + PartialEq + Debug {}
98
99pub(crate) const SAFE_CONTENT_NAMESPACE: i64 = -80001;
103
104const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;
105
106pub(crate) fn encrypt_cose(
108 cose_encrypt0_builder: CoseEncrypt0Builder,
109 plaintext: &[u8],
110 key: &XChaCha20Poly1305Key,
111) -> CoseEncrypt0 {
112 let mut nonce = [0u8; xchacha20::NONCE_SIZE];
113 cose_encrypt0_builder
114 .create_ciphertext(plaintext, &[], |data, aad| {
115 let ciphertext =
116 crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
117 nonce = ciphertext.nonce();
118 ciphertext.encrypted_bytes().to_vec()
119 })
120 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
121 .build()
122}
123
124pub struct DecryptFailed;
125pub(crate) fn decrypt_cose(
127 cose_encrypt0: &CoseEncrypt0,
128 key: &XChaCha20Poly1305Key,
129) -> Result<Vec<u8>, DecryptFailed> {
130 let nonce: [u8; xchacha20::NONCE_SIZE] = cose_encrypt0
131 .unprotected
132 .iv
133 .clone()
134 .try_into()
135 .map_err(|_| DecryptFailed)?;
136 cose_encrypt0
137 .clone()
138 .decrypt_ciphertext(
139 &[],
140 || CryptoError::MissingField("ciphertext"),
141 |data, aad| {
142 xchacha20::decrypt_xchacha20_poly1305(&nonce, &(*key.enc_key).into(), data, aad)
143 },
144 )
145 .map_err(|_| DecryptFailed)
146}
147
148pub(crate) fn encrypt_xchacha20_poly1305(
150 plaintext: &[u8],
151 key: &crate::XChaCha20Poly1305Key,
152 content_format: ContentFormat,
153) -> Result<CoseEncrypt0Bytes, CryptoError> {
154 let mut plaintext = plaintext.to_vec();
155
156 let header_builder: coset::HeaderBuilder = content_format.into();
157 let mut protected_header = header_builder
158 .key_id(key.key_id.as_slice().to_vec())
159 .build();
160 protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
164
165 if should_pad_content(&content_format) {
166 let min_length =
168 XCHACHA20_TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / XCHACHA20_TEXT_PAD_BLOCK_SIZE));
169 crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
170 }
171
172 let mut nonce = [0u8; xchacha20::NONCE_SIZE];
173 let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
174 .protected(protected_header)
175 .create_ciphertext(&plaintext, &[], |data, aad| {
176 let ciphertext =
177 crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
178 nonce = ciphertext.nonce();
179 ciphertext.encrypted_bytes().to_vec()
180 })
181 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
182 .build();
183
184 cose_encrypt0
185 .to_vec()
186 .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
187 .map(CoseEncrypt0Bytes::from)
188}
189
190pub(crate) fn decrypt_xchacha20_poly1305(
192 cose_encrypt0_message: &CoseEncrypt0Bytes,
193 key: &crate::XChaCha20Poly1305Key,
194) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
195 let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message.as_ref())
196 .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
197
198 let Some(ref alg) = msg.protected.header.alg else {
199 return Err(CryptoError::EncString(
200 EncStringParseError::CoseMissingAlgorithm,
201 ));
202 };
203
204 if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
205 return Err(CryptoError::WrongKeyType);
206 }
207
208 let content_format = ContentFormat::try_from(&msg.protected.header)
209 .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
210
211 if key.key_id.as_slice() != msg.protected.header.key_id {
212 return Err(CryptoError::WrongCoseKeyId);
213 }
214
215 let decrypted_message = msg.decrypt_ciphertext(
216 &[],
217 || CryptoError::MissingField("ciphertext"),
218 |data, aad| {
219 let nonce = msg.unprotected.iv.as_slice();
220 crate::xchacha20::decrypt_xchacha20_poly1305(
221 nonce
222 .try_into()
223 .map_err(|_| CryptoError::InvalidNonceLength)?,
224 &(*key.enc_key).into(),
225 data,
226 aad,
227 )
228 },
229 )?;
230
231 if should_pad_content(&content_format) {
232 let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
234 return Ok((data.to_vec(), content_format));
235 }
236
237 Ok((decrypted_message, content_format))
238}
239
240const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
241
242impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
243 type Error = CryptoError;
244
245 #[instrument(err, skip_all)]
246 fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
247 let key_bytes = cose_key
248 .params
249 .iter()
250 .find_map(|(label, value)| match (label, value) {
251 (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
252 _ => None,
253 })
254 .ok_or(CryptoError::InvalidKey)?;
255 let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
256 let key_opts = cose_key
257 .key_ops
258 .iter()
259 .map(|op| match op {
260 coset::RegisteredLabel::Assigned(iana::KeyOperation::Encrypt) => {
261 Ok(KeyOperation::Encrypt)
262 }
263 coset::RegisteredLabel::Assigned(iana::KeyOperation::Decrypt) => {
264 Ok(KeyOperation::Decrypt)
265 }
266 coset::RegisteredLabel::Assigned(iana::KeyOperation::WrapKey) => {
267 Ok(KeyOperation::WrapKey)
268 }
269 coset::RegisteredLabel::Assigned(iana::KeyOperation::UnwrapKey) => {
270 Ok(KeyOperation::UnwrapKey)
271 }
272 _ => Err(CryptoError::InvalidKey),
273 })
274 .collect::<Result<Vec<KeyOperation>, CryptoError>>()?;
275
276 match alg {
277 coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
278 if key_bytes.len() != xchacha20::KEY_SIZE {
281 return Err(CryptoError::InvalidKey);
282 }
283 let enc_key = Box::pin(GenericArray::<u8, U32>::clone_from_slice(key_bytes));
284 let key_id = cose_key
285 .key_id
286 .as_slice()
287 .try_into()
288 .map_err(|_| CryptoError::InvalidKey)?;
289 Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
290 XChaCha20Poly1305Key {
291 enc_key,
292 key_id,
293 supported_operations: key_opts,
294 },
295 ))
296 }
297 _ => Err(CryptoError::InvalidKey),
298 }
299 }
300}
301
302impl From<ContentFormat> for coset::HeaderBuilder {
303 fn from(format: ContentFormat) -> Self {
304 let header_builder = coset::HeaderBuilder::new();
305
306 match format {
307 ContentFormat::Utf8 => {
308 header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string())
309 }
310 ContentFormat::Pkcs8PrivateKey => {
311 header_builder.content_format(CoapContentFormat::Pkcs8)
312 }
313 ContentFormat::SPKIPublicKeyDer => {
314 header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string())
315 }
316 ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
317 ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
318 ContentFormat::CoseEncrypt0 => {
319 header_builder.content_format(CoapContentFormat::CoseEncrypt0)
320 }
321 ContentFormat::BitwardenLegacyKey => {
322 header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
323 }
324 ContentFormat::OctetStream => {
325 header_builder.content_format(CoapContentFormat::OctetStream)
326 }
327 ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor),
328 }
329 }
330}
331
332impl TryFrom<&coset::Header> for ContentFormat {
333 type Error = CryptoError;
334
335 fn try_from(header: &coset::Header) -> Result<Self, Self::Error> {
336 match header.content_type.as_ref() {
337 Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => {
338 Ok(ContentFormat::Utf8)
339 }
340 Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => {
341 Ok(ContentFormat::BitwardenLegacyKey)
342 }
343 Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => {
344 Ok(ContentFormat::SPKIPublicKeyDer)
345 }
346 Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => {
347 Ok(ContentFormat::Pkcs8PrivateKey)
348 }
349 Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey),
350 Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
351 Ok(ContentFormat::OctetStream)
352 }
353 Some(ContentType::Assigned(CoapContentFormat::Cbor)) => Ok(ContentFormat::Cbor),
354 _ => Err(CryptoError::EncString(
355 EncStringParseError::CoseMissingContentType,
356 )),
357 }
358 }
359}
360
361fn should_pad_content(format: &ContentFormat) -> bool {
362 matches!(format, ContentFormat::Utf8)
363}
364
365pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
367 fn to_cose(&self) -> Bytes<T>;
369 fn from_cose(bytes: &Bytes<T>) -> Result<Self, EncodingError>
371 where
372 Self: Sized;
373}
374
375pub(crate) fn extract_integer(
376 header: &Header,
377 target_label: i64,
378 value_name: &str,
379) -> Result<i128, CoseExtractError> {
380 header
381 .rest
382 .iter()
383 .find_map(|(label, value)| match (label, value) {
384 (Label::Int(label_value), ciborium::Value::Integer(int_value))
385 if *label_value == target_label =>
386 {
387 Some(*int_value)
388 }
389 _ => None,
390 })
391 .map(Into::into)
392 .ok_or_else(|| CoseExtractError::MissingValue(value_name.to_string()))
393}
394
395pub(crate) fn extract_bytes(
396 header: &Header,
397 target_label: i64,
398 value_name: &str,
399) -> Result<Vec<u8>, CoseExtractError> {
400 header
401 .rest
402 .iter()
403 .find_map(|(label, value)| match (label, value) {
404 (Label::Int(label_value), ciborium::Value::Bytes(byte_value))
405 if *label_value == target_label =>
406 {
407 Some(byte_value.clone())
408 }
409 _ => None,
410 })
411 .ok_or(CoseExtractError::MissingValue(value_name.to_string()))
412}
413
414#[derive(Debug, Error)]
415pub(crate) enum CoseExtractError {
416 #[error("Missing value {0}")]
417 MissingValue(String),
418}
419
420pub(crate) fn debug_key_operation(key_operation: KeyOperation) -> &'static str {
422 match key_operation {
423 KeyOperation::Sign => "Sign",
424 KeyOperation::Verify => "Verify",
425 KeyOperation::Encrypt => "Encrypt",
426 KeyOperation::Decrypt => "Decrypt",
427 KeyOperation::WrapKey => "WrapKey",
428 KeyOperation::UnwrapKey => "UnwrapKey",
429 KeyOperation::DeriveKey => "DeriveKey",
430 KeyOperation::DeriveBits => "DeriveBits",
431 _ => "Unknown",
432 }
433}
434
435#[cfg(test)]
436mod test {
437 use super::*;
438 use crate::keys::KeyId;
439
440 const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
441 const KEY_DATA: [u8; 32] = [
442 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
443 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
444 0x1e, 0x1f,
445 ];
446 const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
447 const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
448 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,
449 11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
450 75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
451 172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
452 13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
453 ];
454
455 #[test]
456 fn test_encrypt_decrypt_roundtrip_octetstream() {
457 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
458 SymmetricCryptoKey::make_xchacha20_poly1305_key()
459 else {
460 panic!("Failed to create XChaCha20Poly1305Key");
461 };
462
463 let plaintext = b"Hello, world!";
464 let encrypted =
465 encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
466 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
467 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
468 }
469
470 #[test]
471 fn test_encrypt_decrypt_roundtrip_utf8() {
472 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
473 SymmetricCryptoKey::make_xchacha20_poly1305_key()
474 else {
475 panic!("Failed to create XChaCha20Poly1305Key");
476 };
477
478 let plaintext = b"Hello, world!";
479 let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
480 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
481 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
482 }
483
484 #[test]
485 fn test_encrypt_decrypt_roundtrip_pkcs8() {
486 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
487 SymmetricCryptoKey::make_xchacha20_poly1305_key()
488 else {
489 panic!("Failed to create XChaCha20Poly1305Key");
490 };
491
492 let plaintext = b"Hello, world!";
493 let encrypted =
494 encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
495 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
496 assert_eq!(
497 decrypted,
498 (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
499 );
500 }
501
502 #[test]
503 fn test_encrypt_decrypt_roundtrip_cosekey() {
504 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
505 SymmetricCryptoKey::make_xchacha20_poly1305_key()
506 else {
507 panic!("Failed to create XChaCha20Poly1305Key");
508 };
509
510 let plaintext = b"Hello, world!";
511 let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
512 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
513 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
514 }
515
516 #[test]
517 fn test_decrypt_test_vector() {
518 let key = XChaCha20Poly1305Key {
519 key_id: KeyId::from(KEY_ID),
520 enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
521 supported_operations: vec![
522 KeyOperation::Decrypt,
523 KeyOperation::Encrypt,
524 KeyOperation::WrapKey,
525 KeyOperation::UnwrapKey,
526 ],
527 };
528 let decrypted =
529 decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
530 .unwrap();
531 assert_eq!(
532 decrypted,
533 (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
534 );
535 }
536
537 #[test]
538 fn test_fail_wrong_key_id() {
539 let key = XChaCha20Poly1305Key {
540 key_id: KeyId::from([1; 16]), enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
542 supported_operations: vec![
543 KeyOperation::Decrypt,
544 KeyOperation::Encrypt,
545 KeyOperation::WrapKey,
546 KeyOperation::UnwrapKey,
547 ],
548 };
549 assert!(matches!(
550 decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
551 Err(CryptoError::WrongCoseKeyId)
552 ));
553 }
554
555 #[test]
556 fn test_fail_wrong_algorithm() {
557 let protected_header = coset::HeaderBuilder::new()
558 .algorithm(iana::Algorithm::A256GCM)
559 .key_id(KEY_ID.to_vec())
560 .build();
561 let nonce = [0u8; 16];
562 let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
563 .protected(protected_header)
564 .create_ciphertext(&[], &[], |_, _| Vec::new())
565 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
566 .build();
567 let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
568
569 let key = XChaCha20Poly1305Key {
570 key_id: KeyId::from(KEY_ID),
571 enc_key: Box::pin(*GenericArray::from_slice(&KEY_DATA)),
572 supported_operations: vec![
573 KeyOperation::Decrypt,
574 KeyOperation::Encrypt,
575 KeyOperation::WrapKey,
576 KeyOperation::UnwrapKey,
577 ],
578 };
579 assert!(matches!(
580 decrypt_xchacha20_poly1305(&serialized_message, &key),
581 Err(CryptoError::WrongKeyType)
582 ));
583 }
584}