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