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