1use std::str::FromStr;
2
3use bitwarden_encoding::{B64, FromStrVisitor, NotB64EncodedError};
4use ciborium::value::Integer;
5#[allow(unused_imports)]
6use coset::{CborSerializable, ProtectedHeader, RegisteredLabel, iana::CoapContentFormat};
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8use thiserror::Error;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::convert::FromWasmAbi;
11
12use crate::{
13 CONTENT_TYPE_PADDED_CBOR, CoseEncrypt0Bytes, CryptoError, EncString, EncodingError, KeyIds,
14 SerializedMessage, SymmetricCryptoKey, XChaCha20Poly1305Key,
15 cose::{DATA_ENVELOPE_NAMESPACE, XCHACHA20_POLY1305},
16 safe::DataEnvelopeNamespace,
17 utils::pad_bytes,
18 xchacha20,
19};
20
21pub(crate) const DATA_ENVELOPE_PADDING_SIZE: usize = 64;
22
23pub trait SealableVersionedData: Serialize + DeserializeOwned {
27 const NAMESPACE: DataEnvelopeNamespace;
29}
30
31pub trait SealableData: Serialize + DeserializeOwned {}
45
46#[derive(Clone)]
57pub struct DataEnvelope {
58 envelope_data: CoseEncrypt0Bytes,
59}
60
61impl DataEnvelope {
62 pub fn seal<Ids: KeyIds, T>(
65 data: T,
66 ctx: &mut crate::store::KeyStoreContext<Ids>,
67 ) -> Result<(Self, Ids::Symmetric), DataEnvelopeError>
68 where
69 T: Serialize + SealableVersionedData,
70 {
71 let (envelope, cek) = Self::seal_ref(&data, &T::NAMESPACE)?;
72 let cek_id = ctx.generate_symmetric_key();
73 ctx.set_symmetric_key_internal(cek_id, SymmetricCryptoKey::XChaCha20Poly1305Key(cek))
74 .map_err(|_| DataEnvelopeError::KeyStoreError)?;
75 Ok((envelope, cek_id))
76 }
77
78 pub fn seal_with_wrapping_key<Ids: KeyIds, T>(
81 data: T,
82 wrapping_key: &Ids::Symmetric,
83 ctx: &mut crate::store::KeyStoreContext<Ids>,
84 ) -> Result<(Self, EncString), DataEnvelopeError>
85 where
86 T: Serialize + SealableVersionedData,
87 {
88 let (envelope, cek) = Self::seal(data, ctx)?;
89
90 let wrapped_cek = ctx
91 .wrap_symmetric_key(*wrapping_key, cek)
92 .map_err(|_| DataEnvelopeError::EncryptionError)?;
93
94 Ok((envelope, wrapped_cek))
95 }
96
97 fn seal_ref<T>(
100 data: &T,
101 namespace: &DataEnvelopeNamespace,
102 ) -> Result<(DataEnvelope, XChaCha20Poly1305Key), DataEnvelopeError>
103 where
104 T: Serialize + SealableVersionedData,
105 {
106 let mut cek = XChaCha20Poly1305Key::make();
107
108 let serialized_message =
110 SerializedMessage::encode(&data).map_err(|_| DataEnvelopeError::EncodingError)?;
111 if serialized_message.content_type() != coset::iana::CoapContentFormat::Cbor {
112 return Err(DataEnvelopeError::UnsupportedContentFormat);
113 }
114
115 let serialized_and_padded_message = pad_cbor(serialized_message.as_bytes())
116 .map_err(|_| DataEnvelopeError::EncodingError)?;
117
118 let mut protected_header = coset::HeaderBuilder::new()
120 .key_id(cek.key_id.to_vec())
121 .content_type(CONTENT_TYPE_PADDED_CBOR.to_string())
122 .value(
123 DATA_ENVELOPE_NAMESPACE,
124 ciborium::Value::Integer(Integer::from(namespace.as_i64())),
125 )
126 .build();
127 protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
128
129 let mut nonce = [0u8; xchacha20::NONCE_SIZE];
131 let encrypt0 = coset::CoseEncrypt0Builder::new()
132 .protected(protected_header)
133 .create_ciphertext(&serialized_and_padded_message, &[], |data, aad| {
134 let ciphertext =
135 crate::xchacha20::encrypt_xchacha20_poly1305(&(*cek.enc_key).into(), data, aad);
136 nonce = ciphertext.nonce();
137 ciphertext.encrypted_bytes().to_vec()
138 })
139 .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
140 .build();
141
142 let envelope_data = encrypt0
144 .to_vec()
145 .map(CoseEncrypt0Bytes::from)
146 .map_err(|_| DataEnvelopeError::EncodingError)?;
147
148 cek.disable_key_operation(coset::iana::KeyOperation::Encrypt)
150 .disable_key_operation(coset::iana::KeyOperation::WrapKey)
151 .disable_key_operation(coset::iana::KeyOperation::UnwrapKey);
152
153 Ok((DataEnvelope { envelope_data }, cek))
154 }
155
156 pub fn unseal<Ids: KeyIds, T>(
159 &self,
160 cek_keyslot: Ids::Symmetric,
161 ctx: &mut crate::store::KeyStoreContext<Ids>,
162 ) -> Result<T, DataEnvelopeError>
163 where
164 T: DeserializeOwned + SealableVersionedData,
165 {
166 let cek = ctx
167 .get_symmetric_key(cek_keyslot)
168 .map_err(|_| DataEnvelopeError::KeyStoreError)?;
169
170 match cek {
171 SymmetricCryptoKey::XChaCha20Poly1305Key(key) => self.unseal_ref(&T::NAMESPACE, key),
172 _ => Err(DataEnvelopeError::UnsupportedContentFormat),
173 }
174 }
175
176 pub fn unseal_with_wrapping_key<Ids: KeyIds, T>(
178 &self,
179 wrapping_key: &Ids::Symmetric,
180 wrapped_cek: &EncString,
181 ctx: &mut crate::store::KeyStoreContext<Ids>,
182 ) -> Result<T, DataEnvelopeError>
183 where
184 T: DeserializeOwned + SealableVersionedData,
185 {
186 let cek = ctx
187 .unwrap_symmetric_key(*wrapping_key, wrapped_cek)
188 .map_err(|_| DataEnvelopeError::DecryptionError)?;
189 self.unseal(cek, ctx)
190 }
191
192 fn unseal_ref<T>(
194 &self,
195 namespace: &DataEnvelopeNamespace,
196 cek: &XChaCha20Poly1305Key,
197 ) -> Result<T, DataEnvelopeError>
198 where
199 T: DeserializeOwned + SealableVersionedData,
200 {
201 let msg = coset::CoseEncrypt0::from_slice(self.envelope_data.as_ref())
203 .map_err(|_| DataEnvelopeError::CoseDecodingError)?;
204 let envelope_namespace = extract_namespace(&msg.protected.header)?;
205 let content_format =
206 content_format(&msg.protected).map_err(|_| DataEnvelopeError::DecodingError)?;
207
208 if !matches!(
210 msg.protected.header.alg,
211 Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305)),
212 ) {
213 return Err(DataEnvelopeError::DecryptionError);
214 }
215 if msg.protected.header.key_id != cek.key_id {
216 return Err(DataEnvelopeError::WrongKey);
217 }
218 if envelope_namespace != *namespace {
219 return Err(DataEnvelopeError::InvalidNamespace);
220 }
221 if content_format != CONTENT_TYPE_PADDED_CBOR {
222 return Err(DataEnvelopeError::UnsupportedContentFormat);
223 }
224
225 let decrypted_message = msg
227 .decrypt(&[], |data, aad| {
228 let nonce = msg.unprotected.iv.as_slice();
229 crate::xchacha20::decrypt_xchacha20_poly1305(
230 nonce
231 .try_into()
232 .map_err(|_| CryptoError::InvalidNonceLength)?,
233 &(*cek.enc_key).into(),
234 data,
235 aad,
236 )
237 })
238 .map_err(|_| DataEnvelopeError::DecryptionError)?;
239
240 let unpadded_message =
241 unpad_cbor(&decrypted_message).map_err(|_| DataEnvelopeError::DecryptionError)?;
242
243 let serialized_message =
245 SerializedMessage::from_bytes(unpadded_message, CoapContentFormat::Cbor);
246 serialized_message
247 .decode()
248 .map_err(|_| DataEnvelopeError::DecodingError)
249 }
250}
251
252fn extract_namespace(header: &coset::Header) -> Result<DataEnvelopeNamespace, DataEnvelopeError> {
255 let namespace_value = header
256 .rest
257 .iter()
258 .find(|(label, _)| {
259 if let coset::Label::Int(label_int) = label {
260 *label_int == DATA_ENVELOPE_NAMESPACE
261 } else {
262 false
263 }
264 })
265 .map(|(_, value)| value)
266 .ok_or(DataEnvelopeError::InvalidNamespace)?;
267
268 let namespace_int = match namespace_value {
269 ciborium::Value::Integer(int) => {
270 let int_val: i128 = (*int).into();
271 int_val
272 }
273 _ => return Err(DataEnvelopeError::InvalidNamespace),
274 };
275
276 DataEnvelopeNamespace::try_from(namespace_int).map_err(|_| DataEnvelopeError::InvalidNamespace)
277}
278
279pub(super) fn content_format(protected_header: &ProtectedHeader) -> Result<String, EncodingError> {
283 protected_header
284 .header
285 .content_type
286 .as_ref()
287 .and_then(|ct| match ct {
288 RegisteredLabel::Text(content_format) => Some(content_format.clone()),
289 _ => None,
290 })
291 .ok_or(EncodingError::InvalidCoseEncoding)
292}
293
294impl From<&DataEnvelope> for Vec<u8> {
295 fn from(val: &DataEnvelope) -> Self {
296 val.envelope_data.to_vec()
297 }
298}
299
300impl From<Vec<u8>> for DataEnvelope {
301 fn from(data: Vec<u8>) -> Self {
302 DataEnvelope {
303 envelope_data: CoseEncrypt0Bytes::from(data),
304 }
305 }
306}
307
308impl std::fmt::Debug for DataEnvelope {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 f.debug_struct("DataEnvelope")
311 .field("envelope_data", &self.envelope_data)
312 .finish()
313 }
314}
315
316impl FromStr for DataEnvelope {
317 type Err = NotB64EncodedError;
318
319 fn from_str(s: &str) -> Result<Self, Self::Err> {
320 let data = B64::try_from(s)?;
321 Ok(Self::from(data.into_bytes()))
322 }
323}
324
325impl From<DataEnvelope> for String {
326 fn from(val: DataEnvelope) -> Self {
327 let serialized: Vec<u8> = (&val).into();
328 B64::from(serialized).to_string()
329 }
330}
331
332impl<'de> Deserialize<'de> for DataEnvelope {
333 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
334 where
335 D: serde::Deserializer<'de>,
336 {
337 deserializer.deserialize_str(FromStrVisitor::new())
338 }
339}
340
341impl Serialize for DataEnvelope {
342 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
343 where
344 S: serde::Serializer,
345 {
346 let serialized: Vec<u8> = self.into();
347 serializer.serialize_str(&B64::from(serialized).to_string())
348 }
349}
350
351impl std::fmt::Display for DataEnvelope {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 let serialized: Vec<u8> = self.into();
354 write!(f, "{}", B64::from(serialized))
355 }
356}
357
358#[derive(Debug, Error)]
360pub enum DataEnvelopeError {
361 #[error("Unsupported content format")]
363 UnsupportedContentFormat,
364 #[error("Failed to decode COSE message")]
366 CoseDecodingError,
367 #[error("Failed to decode the content of the envelope")]
369 DecodingError,
370 #[error("Encoding error")]
372 EncodingError,
373 #[error("KeyStore error")]
375 KeyStoreError,
376 #[error("Decryption error")]
378 DecryptionError,
379 #[error("Encryption error")]
381 EncryptionError,
382 #[error("Parsing error: {0}")]
384 ParsingError(String),
385 #[error("Invalid namespace")]
387 InvalidNamespace,
388 #[error("Wrong key used for decryption")]
390 WrongKey,
391}
392
393#[cfg(feature = "wasm")]
394#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
395const TS_CUSTOM_TYPES: &'static str = r#"
396export type DataEnvelope = Tagged<string, "DataEnvelope">;
397"#;
398
399#[cfg(feature = "wasm")]
400impl wasm_bindgen::describe::WasmDescribe for DataEnvelope {
401 fn describe() {
402 <String as wasm_bindgen::describe::WasmDescribe>::describe();
403 }
404}
405
406#[cfg(feature = "wasm")]
407impl FromWasmAbi for DataEnvelope {
408 type Abi = <String as FromWasmAbi>::Abi;
409
410 unsafe fn from_abi(abi: Self::Abi) -> Self {
411 use wasm_bindgen::UnwrapThrowExt;
412
413 let s = unsafe { String::from_abi(abi) };
414 Self::from_str(&s).unwrap_throw()
415 }
416}
417
418fn pad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
419 let mut data = data.to_vec();
420 pad_bytes(&mut data, DATA_ENVELOPE_PADDING_SIZE).map_err(|_| CryptoError::InvalidPadding)?;
421 Ok(data)
422}
423
424fn unpad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
425 let unpadded = crate::utils::unpad_bytes(data).map_err(|_| CryptoError::InvalidPadding)?;
426 Ok(unpadded.to_vec())
427}
428
429#[macro_export]
464macro_rules! generate_versioned_sealable {
465 (
466 $enum_name:ident,
468 $namespace:path,
470 [ $( $variant_ty:ident => $rename:literal ),+ $(,)? ]
472 ) => {
473 #[derive(Serialize, Deserialize, Debug, PartialEq)]
475 #[serde(tag = "version", content = "content")]
476 enum $enum_name {
477 $(
478 #[serde(rename = $rename)]
479 $variant_ty($variant_ty),
481 )+
482 }
483
484 impl SealableVersionedData for $enum_name
486 where
487 $( $variant_ty: SealableData ),+
488 {
489 const NAMESPACE: DataEnvelopeNamespace = $namespace;
491 }
492
493 $(
495 impl From<$variant_ty> for $enum_name {
496 fn from(value: $variant_ty) -> Self {
497 Self::$variant_ty(value)
498 }
499 }
500 )+
501 };
502}
503
504#[cfg(test)]
505mod tests {
506 use serde::Deserialize;
507
508 use super::*;
509 use crate::traits::tests::TestIds;
510
511 #[derive(Serialize, Deserialize, Debug, PartialEq)]
512 struct TestDataV1 {
513 field: u32,
514 }
515 impl SealableData for TestDataV1 {}
516
517 generate_versioned_sealable!(
518 TestData,
519 DataEnvelopeNamespace::ExampleNamespace,
520 [
521 TestDataV1 => "1",
522 ]
523 );
524
525 const TEST_VECTOR_CEK: &str =
526 "pQEEAlAiZII8tW5Lu9YH2bND5qx4AzoAARFvBIEEIFggnlL+dg+plLs+YqbUS00NYjwvir9E7O5pTJgX/O++XuQB";
527 const TEST_VECTOR_ENVELOPE: &str = "g1hFpAE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFAiZII8tW5Lu9YH2bND5qx4OgABOIAgoQVYGDjsL+Q0npomBf7fVsefBkXNJT/OkMncuVhQ8VSz8YWHIRylVilXRDrQp3LRSnDHQKIU4F0A49yi8W2tmRATUcPkU87eI9xbRvxjdUY/X4wL26MoFsqbWxyMJHcj8svQWwL3Jq3OvK9VS6A=";
528
529 #[test]
530 #[ignore]
531 fn generate_test_vectors() {
532 let data: TestData = TestDataV1 { field: 123 }.into();
533 let (envelope, cek) =
534 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
535 let unsealed_data: TestData = envelope
536 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
537 .unwrap();
538 assert_eq!(unsealed_data, data);
539 println!(
540 "CEK: {}",
541 B64::from(SymmetricCryptoKey::XChaCha20Poly1305Key(cek).to_encoded())
542 );
543 println!("Envelope: {}", String::from(envelope));
544 }
545
546 #[test]
547 fn test_data_envelope_test_vector() {
548 let cek = SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_CEK).unwrap()).unwrap();
549 let cek = match cek {
550 SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) => key.clone(),
551 _ => panic!("Invalid CEK type"),
552 };
553
554 let envelope: DataEnvelope = TEST_VECTOR_ENVELOPE.parse().unwrap();
555 let unsealed_data: TestData = envelope
556 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
557 .unwrap();
558 assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
559 }
560
561 #[test]
562 fn test_data_envelope() {
563 let data: TestData = TestDataV1 { field: 42 }.into();
565
566 let (envelope, cek) =
568 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
569 let unsealed_data: TestData = envelope
570 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
571 .unwrap();
572
573 assert_eq!(unsealed_data, data);
575 }
576
577 #[test]
578 fn test_namespace_validation_success() {
579 let data: TestData = TestDataV1 { field: 123 }.into();
580
581 let (envelope1, cek1) =
583 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
584 let unsealed_data1: TestData = envelope1
585 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek1)
586 .unwrap();
587 assert_eq!(unsealed_data1, data);
588
589 let (envelope2, cek2) =
591 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
592 let unsealed_data2: TestData = envelope2
593 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek2)
594 .unwrap();
595 assert_eq!(unsealed_data2, data);
596 }
597
598 #[test]
599 fn test_namespace_validation_failure() {
600 let data: TestData = TestDataV1 { field: 456 }.into();
601
602 let (envelope, cek) =
604 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
605
606 let result: Result<TestData, DataEnvelopeError> =
608 envelope.unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek);
609 assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
610
611 let unsealed_data: TestData = envelope
613 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
614 .unwrap();
615 assert_eq!(unsealed_data, data);
616 }
617
618 #[test]
619 fn test_namespace_validation_with_keystore() {
620 let data: TestData = TestDataV1 { field: 789 }.into();
621 let key_store = crate::store::KeyStore::<TestIds>::default();
622 let mut ctx = key_store.context_mut();
623
624 let (envelope, cek) =
626 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
627 ctx.set_symmetric_key_internal(
628 crate::traits::tests::TestSymmKey::A(0),
629 SymmetricCryptoKey::XChaCha20Poly1305Key(cek),
630 )
631 .unwrap();
632
633 let result: Result<TestData, DataEnvelopeError> =
635 envelope.unseal(crate::traits::tests::TestSymmKey::A(0), &mut ctx);
636 assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
637 }
638
639 #[test]
640 fn test_namespace_cross_contamination_protection() {
641 let data1: TestData = TestDataV1 { field: 111 }.into();
642 let data2: TestData = TestDataV1 { field: 222 }.into();
643
644 let (envelope1, cek1) =
646 DataEnvelope::seal_ref(&data1, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
647 let (envelope2, cek2) =
648 DataEnvelope::seal_ref(&data2, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
649
650 let unsealed1: TestData = envelope1
652 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek1)
653 .unwrap();
654 assert_eq!(unsealed1, data1);
655
656 let unsealed2: TestData = envelope2
657 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek2)
658 .unwrap();
659 assert_eq!(unsealed2, data2);
660
661 assert!(matches!(
663 envelope1.unseal_ref::<TestData>(&DataEnvelopeNamespace::ExampleNamespace2, &cek1),
664 Err(DataEnvelopeError::InvalidNamespace)
665 ));
666 assert!(matches!(
667 envelope2.unseal_ref::<TestData>(&DataEnvelopeNamespace::ExampleNamespace, &cek2),
668 Err(DataEnvelopeError::InvalidNamespace)
669 ));
670 }
671}