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