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_ciphertext(
228 &[],
229 || CryptoError::MissingField("ciphertext"),
230 |data, aad| {
231 let nonce = msg.unprotected.iv.as_slice();
232 crate::xchacha20::decrypt_xchacha20_poly1305(
233 nonce
234 .try_into()
235 .map_err(|_| CryptoError::InvalidNonceLength)?,
236 &(*cek.enc_key).into(),
237 data,
238 aad,
239 )
240 },
241 )
242 .map_err(|_| DataEnvelopeError::DecryptionError)?;
243
244 let unpadded_message =
245 unpad_cbor(&decrypted_message).map_err(|_| DataEnvelopeError::DecryptionError)?;
246
247 let serialized_message =
249 SerializedMessage::from_bytes(unpadded_message, CoapContentFormat::Cbor);
250 serialized_message
251 .decode()
252 .map_err(|_| DataEnvelopeError::DecodingError)
253 }
254}
255
256fn extract_namespace(header: &coset::Header) -> Result<DataEnvelopeNamespace, DataEnvelopeError> {
259 let namespace_value = header
260 .rest
261 .iter()
262 .find(|(label, _)| {
263 if let coset::Label::Int(label_int) = label {
264 *label_int == DATA_ENVELOPE_NAMESPACE
265 } else {
266 false
267 }
268 })
269 .map(|(_, value)| value)
270 .ok_or(DataEnvelopeError::InvalidNamespace)?;
271
272 let namespace_int = match namespace_value {
273 ciborium::Value::Integer(int) => {
274 let int_val: i128 = (*int).into();
275 int_val
276 }
277 _ => return Err(DataEnvelopeError::InvalidNamespace),
278 };
279
280 DataEnvelopeNamespace::try_from(namespace_int).map_err(|_| DataEnvelopeError::InvalidNamespace)
281}
282
283pub(super) fn content_format(protected_header: &ProtectedHeader) -> Result<String, EncodingError> {
287 protected_header
288 .header
289 .content_type
290 .as_ref()
291 .and_then(|ct| match ct {
292 RegisteredLabel::Text(content_format) => Some(content_format.clone()),
293 _ => None,
294 })
295 .ok_or(EncodingError::InvalidCoseEncoding)
296}
297
298impl From<&DataEnvelope> for Vec<u8> {
299 fn from(val: &DataEnvelope) -> Self {
300 val.envelope_data.to_vec()
301 }
302}
303
304impl From<Vec<u8>> for DataEnvelope {
305 fn from(data: Vec<u8>) -> Self {
306 DataEnvelope {
307 envelope_data: CoseEncrypt0Bytes::from(data),
308 }
309 }
310}
311
312impl std::fmt::Debug for DataEnvelope {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 f.debug_struct("DataEnvelope")
315 .field("envelope_data", &self.envelope_data)
316 .finish()
317 }
318}
319
320impl FromStr for DataEnvelope {
321 type Err = NotB64EncodedError;
322
323 fn from_str(s: &str) -> Result<Self, Self::Err> {
324 let data = B64::try_from(s)?;
325 Ok(Self::from(data.into_bytes()))
326 }
327}
328
329impl From<DataEnvelope> for String {
330 fn from(val: DataEnvelope) -> Self {
331 let serialized: Vec<u8> = (&val).into();
332 B64::from(serialized).to_string()
333 }
334}
335
336impl<'de> Deserialize<'de> for DataEnvelope {
337 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
338 where
339 D: serde::Deserializer<'de>,
340 {
341 deserializer.deserialize_str(FromStrVisitor::new())
342 }
343}
344
345impl Serialize for DataEnvelope {
346 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
347 where
348 S: serde::Serializer,
349 {
350 let serialized: Vec<u8> = self.into();
351 serializer.serialize_str(&B64::from(serialized).to_string())
352 }
353}
354
355impl std::fmt::Display for DataEnvelope {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 let serialized: Vec<u8> = self.into();
358 write!(f, "{}", B64::from(serialized))
359 }
360}
361
362#[derive(Debug, Error)]
364pub enum DataEnvelopeError {
365 #[error("Unsupported content format")]
367 UnsupportedContentFormat,
368 #[error("Failed to decode COSE message")]
370 CoseDecodingError,
371 #[error("Failed to decode the content of the envelope")]
373 DecodingError,
374 #[error("Encoding error")]
376 EncodingError,
377 #[error("KeyStore error")]
379 KeyStoreError,
380 #[error("Decryption error")]
382 DecryptionError,
383 #[error("Encryption error")]
385 EncryptionError,
386 #[error("Parsing error: {0}")]
388 ParsingError(String),
389 #[error("Invalid namespace")]
391 InvalidNamespace,
392 #[error("Wrong key used for decryption")]
394 WrongKey,
395}
396
397#[cfg(feature = "wasm")]
398#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
399const TS_CUSTOM_TYPES: &'static str = r#"
400export type DataEnvelope = Tagged<string, "DataEnvelope">;
401"#;
402
403#[cfg(feature = "wasm")]
404impl wasm_bindgen::describe::WasmDescribe for DataEnvelope {
405 fn describe() {
406 <String as wasm_bindgen::describe::WasmDescribe>::describe();
407 }
408}
409
410#[cfg(feature = "wasm")]
411impl FromWasmAbi for DataEnvelope {
412 type Abi = <String as FromWasmAbi>::Abi;
413
414 unsafe fn from_abi(abi: Self::Abi) -> Self {
415 use wasm_bindgen::UnwrapThrowExt;
416
417 let s = unsafe { String::from_abi(abi) };
418 Self::from_str(&s).unwrap_throw()
419 }
420}
421
422fn pad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
423 let mut data = data.to_vec();
424 pad_bytes(&mut data, DATA_ENVELOPE_PADDING_SIZE).map_err(|_| CryptoError::InvalidPadding)?;
425 Ok(data)
426}
427
428fn unpad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
429 let unpadded = crate::utils::unpad_bytes(data).map_err(|_| CryptoError::InvalidPadding)?;
430 Ok(unpadded.to_vec())
431}
432
433#[macro_export]
468macro_rules! generate_versioned_sealable {
469 (
470 $enum_name:ident,
472 $namespace:path,
474 [ $( $variant_ty:ident => $rename:literal ),+ $(,)? ]
476 ) => {
477 #[derive(Serialize, Deserialize, Debug, PartialEq)]
479 #[serde(tag = "version", content = "content")]
480 enum $enum_name {
481 $(
482 #[serde(rename = $rename)]
483 $variant_ty($variant_ty),
485 )+
486 }
487
488 impl SealableVersionedData for $enum_name
490 where
491 $( $variant_ty: SealableData ),+
492 {
493 const NAMESPACE: DataEnvelopeNamespace = $namespace;
495 }
496
497 $(
499 impl From<$variant_ty> for $enum_name {
500 fn from(value: $variant_ty) -> Self {
501 Self::$variant_ty(value)
502 }
503 }
504 )+
505 };
506}
507
508#[cfg(test)]
509mod tests {
510 use serde::Deserialize;
511
512 use super::*;
513 use crate::traits::tests::TestIds;
514
515 #[derive(Serialize, Deserialize, Debug, PartialEq)]
516 struct TestDataV1 {
517 field: u32,
518 }
519 impl SealableData for TestDataV1 {}
520
521 generate_versioned_sealable!(
522 TestData,
523 DataEnvelopeNamespace::ExampleNamespace,
524 [
525 TestDataV1 => "1",
526 ]
527 );
528
529 const TEST_VECTOR_CEK: &str =
530 "pQEEAlAiZII8tW5Lu9YH2bND5qx4AzoAARFvBIEEIFggnlL+dg+plLs+YqbUS00NYjwvir9E7O5pTJgX/O++XuQB";
531 const TEST_VECTOR_ENVELOPE: &str = "g1hFpAE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFAiZII8tW5Lu9YH2bND5qx4OgABOIAgoQVYGDjsL+Q0npomBf7fVsefBkXNJT/OkMncuVhQ8VSz8YWHIRylVilXRDrQp3LRSnDHQKIU4F0A49yi8W2tmRATUcPkU87eI9xbRvxjdUY/X4wL26MoFsqbWxyMJHcj8svQWwL3Jq3OvK9VS6A=";
532
533 #[test]
534 #[ignore]
535 fn generate_test_vectors() {
536 let data: TestData = TestDataV1 { field: 123 }.into();
537 let (envelope, cek) =
538 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
539 let unsealed_data: TestData = envelope
540 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
541 .unwrap();
542 assert_eq!(unsealed_data, data);
543 println!(
544 "CEK: {}",
545 B64::from(SymmetricCryptoKey::XChaCha20Poly1305Key(cek).to_encoded())
546 );
547 println!("Envelope: {}", String::from(envelope));
548 }
549
550 #[test]
551 fn test_data_envelope_test_vector() {
552 let cek = SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_CEK).unwrap()).unwrap();
553 let cek = match cek {
554 SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) => key.clone(),
555 _ => panic!("Invalid CEK type"),
556 };
557
558 let envelope: DataEnvelope = TEST_VECTOR_ENVELOPE.parse().unwrap();
559 let unsealed_data: TestData = envelope
560 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
561 .unwrap();
562 assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
563 }
564
565 #[test]
566 fn test_data_envelope() {
567 let data: TestData = TestDataV1 { field: 42 }.into();
569
570 let (envelope, cek) =
572 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
573 let unsealed_data: TestData = envelope
574 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
575 .unwrap();
576
577 assert_eq!(unsealed_data, data);
579 }
580
581 #[test]
582 fn test_namespace_validation_success() {
583 let data: TestData = TestDataV1 { field: 123 }.into();
584
585 let (envelope1, cek1) =
587 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
588 let unsealed_data1: TestData = envelope1
589 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek1)
590 .unwrap();
591 assert_eq!(unsealed_data1, data);
592
593 let (envelope2, cek2) =
595 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
596 let unsealed_data2: TestData = envelope2
597 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek2)
598 .unwrap();
599 assert_eq!(unsealed_data2, data);
600 }
601
602 #[test]
603 fn test_namespace_validation_failure() {
604 let data: TestData = TestDataV1 { field: 456 }.into();
605
606 let (envelope, cek) =
608 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
609
610 let result: Result<TestData, DataEnvelopeError> =
612 envelope.unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek);
613 assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
614
615 let unsealed_data: TestData = envelope
617 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
618 .unwrap();
619 assert_eq!(unsealed_data, data);
620 }
621
622 #[test]
623 fn test_namespace_validation_with_keystore() {
624 let data: TestData = TestDataV1 { field: 789 }.into();
625 let key_store = crate::store::KeyStore::<TestIds>::default();
626 let mut ctx = key_store.context_mut();
627
628 let (envelope, cek) =
630 DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
631 ctx.set_symmetric_key_internal(
632 crate::traits::tests::TestSymmKey::A(0),
633 SymmetricCryptoKey::XChaCha20Poly1305Key(cek),
634 )
635 .unwrap();
636
637 let result: Result<TestData, DataEnvelopeError> =
639 envelope.unseal(crate::traits::tests::TestSymmKey::A(0), &mut ctx);
640 assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
641 }
642
643 #[test]
644 fn test_namespace_cross_contamination_protection() {
645 let data1: TestData = TestDataV1 { field: 111 }.into();
646 let data2: TestData = TestDataV1 { field: 222 }.into();
647
648 let (envelope1, cek1) =
650 DataEnvelope::seal_ref(&data1, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
651 let (envelope2, cek2) =
652 DataEnvelope::seal_ref(&data2, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
653
654 let unsealed1: TestData = envelope1
656 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek1)
657 .unwrap();
658 assert_eq!(unsealed1, data1);
659
660 let unsealed2: TestData = envelope2
661 .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek2)
662 .unwrap();
663 assert_eq!(unsealed2, data2);
664
665 assert!(matches!(
667 envelope1.unseal_ref::<TestData>(&DataEnvelopeNamespace::ExampleNamespace2, &cek1),
668 Err(DataEnvelopeError::InvalidNamespace)
669 ));
670 assert!(matches!(
671 envelope2.unseal_ref::<TestData>(&DataEnvelopeNamespace::ExampleNamespace, &cek2),
672 Err(DataEnvelopeError::InvalidNamespace)
673 ));
674 }
675}