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