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 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 let enc_key = Box::pin(
279 Array::<u8, U32>::try_from(key_bytes).map_err(|_| CryptoError::InvalidKey)?,
280 );
281 let key_id = cose_key
282 .key_id
283 .as_slice()
284 .try_into()
285 .map_err(|_| CryptoError::InvalidKey)?;
286 Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
287 XChaCha20Poly1305Key {
288 enc_key,
289 key_id,
290 supported_operations: key_opts,
291 },
292 ))
293 }
294 _ => Err(CryptoError::InvalidKey),
295 }
296 }
297}
298
299impl From<ContentFormat> for coset::HeaderBuilder {
300 fn from(format: ContentFormat) -> Self {
301 let header_builder = coset::HeaderBuilder::new();
302
303 match format {
304 ContentFormat::Utf8 => {
305 header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string())
306 }
307 ContentFormat::Pkcs8PrivateKey => {
308 header_builder.content_format(CoapContentFormat::Pkcs8)
309 }
310 ContentFormat::SPKIPublicKeyDer => {
311 header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string())
312 }
313 ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
314 ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
315 ContentFormat::CoseEncrypt0 => {
316 header_builder.content_format(CoapContentFormat::CoseEncrypt0)
317 }
318 ContentFormat::BitwardenLegacyKey => {
319 header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
320 }
321 ContentFormat::OctetStream => {
322 header_builder.content_format(CoapContentFormat::OctetStream)
323 }
324 ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor),
325 }
326 }
327}
328
329impl TryFrom<&coset::Header> for ContentFormat {
330 type Error = CryptoError;
331
332 fn try_from(header: &coset::Header) -> Result<Self, Self::Error> {
333 match header.content_type.as_ref() {
334 Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => {
335 Ok(ContentFormat::Utf8)
336 }
337 Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => {
338 Ok(ContentFormat::BitwardenLegacyKey)
339 }
340 Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => {
341 Ok(ContentFormat::SPKIPublicKeyDer)
342 }
343 Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => {
344 Ok(ContentFormat::Pkcs8PrivateKey)
345 }
346 Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey),
347 Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
348 Ok(ContentFormat::OctetStream)
349 }
350 Some(ContentType::Assigned(CoapContentFormat::Cbor)) => Ok(ContentFormat::Cbor),
351 _ => Err(CryptoError::EncString(
352 EncStringParseError::CoseMissingContentType,
353 )),
354 }
355 }
356}
357
358fn should_pad_content(format: &ContentFormat) -> bool {
359 matches!(format, ContentFormat::Utf8)
360}
361
362pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
364 fn to_cose(&self) -> Bytes<T>;
366 fn from_cose(bytes: &Bytes<T>) -> Result<Self, EncodingError>
368 where
369 Self: Sized;
370}
371
372pub(crate) fn extract_integer(
373 header: &Header,
374 target_label: i64,
375 value_name: &str,
376) -> Result<i128, CoseExtractError> {
377 header
378 .rest
379 .iter()
380 .find_map(|(label, value)| match (label, value) {
381 (Label::Int(label_value), ciborium::Value::Integer(int_value))
382 if *label_value == target_label =>
383 {
384 Some(*int_value)
385 }
386 _ => None,
387 })
388 .map(Into::into)
389 .ok_or_else(|| CoseExtractError::MissingValue(value_name.to_string()))
390}
391
392pub(crate) fn extract_bytes(
393 header: &Header,
394 target_label: i64,
395 value_name: &str,
396) -> Result<Vec<u8>, CoseExtractError> {
397 header
398 .rest
399 .iter()
400 .find_map(|(label, value)| match (label, value) {
401 (Label::Int(label_value), ciborium::Value::Bytes(byte_value))
402 if *label_value == target_label =>
403 {
404 Some(byte_value.clone())
405 }
406 _ => None,
407 })
408 .ok_or(CoseExtractError::MissingValue(value_name.to_string()))
409}
410
411#[derive(Debug, Error)]
412pub(crate) enum CoseExtractError {
413 #[error("Missing value {0}")]
414 MissingValue(String),
415}
416
417pub(crate) fn debug_key_operation(key_operation: KeyOperation) -> &'static str {
419 match key_operation {
420 KeyOperation::Sign => "Sign",
421 KeyOperation::Verify => "Verify",
422 KeyOperation::Encrypt => "Encrypt",
423 KeyOperation::Decrypt => "Decrypt",
424 KeyOperation::WrapKey => "WrapKey",
425 KeyOperation::UnwrapKey => "UnwrapKey",
426 KeyOperation::DeriveKey => "DeriveKey",
427 KeyOperation::DeriveBits => "DeriveBits",
428 _ => "Unknown",
429 }
430}
431
432#[cfg(test)]
433mod test {
434 use super::*;
435 use crate::keys::KeyId;
436
437 const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
438 const KEY_DATA: [u8; 32] = [
439 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
440 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
441 0x1e, 0x1f,
442 ];
443 const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
444 const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
445 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,
446 11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
447 75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
448 172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
449 13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
450 ];
451
452 #[test]
453 fn test_encrypt_decrypt_roundtrip_octetstream() {
454 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
455 SymmetricCryptoKey::make_xchacha20_poly1305_key()
456 else {
457 panic!("Failed to create XChaCha20Poly1305Key");
458 };
459
460 let plaintext = b"Hello, world!";
461 let encrypted =
462 encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
463 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
464 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
465 }
466
467 #[test]
468 fn test_encrypt_decrypt_roundtrip_utf8() {
469 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
470 SymmetricCryptoKey::make_xchacha20_poly1305_key()
471 else {
472 panic!("Failed to create XChaCha20Poly1305Key");
473 };
474
475 let plaintext = b"Hello, world!";
476 let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
477 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
478 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
479 }
480
481 #[test]
482 fn test_encrypt_decrypt_roundtrip_pkcs8() {
483 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
484 SymmetricCryptoKey::make_xchacha20_poly1305_key()
485 else {
486 panic!("Failed to create XChaCha20Poly1305Key");
487 };
488
489 let plaintext = b"Hello, world!";
490 let encrypted =
491 encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
492 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
493 assert_eq!(
494 decrypted,
495 (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
496 );
497 }
498
499 #[test]
500 fn test_encrypt_decrypt_roundtrip_cosekey() {
501 let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
502 SymmetricCryptoKey::make_xchacha20_poly1305_key()
503 else {
504 panic!("Failed to create XChaCha20Poly1305Key");
505 };
506
507 let plaintext = b"Hello, world!";
508 let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
509 let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
510 assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
511 }
512
513 #[test]
514 fn test_decrypt_test_vector() {
515 let key = XChaCha20Poly1305Key {
516 key_id: KeyId::from(KEY_ID),
517 enc_key: Box::pin(Array::from(KEY_DATA)),
518 supported_operations: vec![
519 KeyOperation::Decrypt,
520 KeyOperation::Encrypt,
521 KeyOperation::WrapKey,
522 KeyOperation::UnwrapKey,
523 ],
524 };
525 let decrypted =
526 decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
527 .unwrap();
528 assert_eq!(
529 decrypted,
530 (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
531 );
532 }
533
534 #[test]
535 fn test_fail_wrong_key_id() {
536 let key = XChaCha20Poly1305Key {
537 key_id: KeyId::from([1; 16]), enc_key: Box::pin(Array::from(KEY_DATA)),
539 supported_operations: vec![
540 KeyOperation::Decrypt,
541 KeyOperation::Encrypt,
542 KeyOperation::WrapKey,
543 KeyOperation::UnwrapKey,
544 ],
545 };
546 assert!(matches!(
547 decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
548 Err(CryptoError::WrongCoseKeyId)
549 ));
550 }
551
552 #[test]
553 fn test_fail_wrong_algorithm() {
554 let protected_header = coset::HeaderBuilder::new()
555 .algorithm(iana::Algorithm::A256GCM)
556 .key_id(KEY_ID.to_vec())
557 .build();
558 let nonce = [0u8; 16];
559 let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
560 .protected(protected_header)
561 .create_ciphertext(&[], &[], |_, _| Vec::new())
562 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
563 .build();
564 let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
565
566 let key = XChaCha20Poly1305Key {
567 key_id: KeyId::from(KEY_ID),
568 enc_key: Box::pin(Array::from(KEY_DATA)),
569 supported_operations: vec![
570 KeyOperation::Decrypt,
571 KeyOperation::Encrypt,
572 KeyOperation::WrapKey,
573 KeyOperation::UnwrapKey,
574 ],
575 };
576 assert!(matches!(
577 decrypt_xchacha20_poly1305(&serialized_message, &key),
578 Err(CryptoError::WrongKeyType)
579 ));
580 }
581}