1use std::str::FromStr;
2
3use bitwarden_encoding::{B64, FromStrVisitor, NotB64EncodedError};
4#[allow(unused_imports)]
5use coset::{CborSerializable, ProtectedHeader, RegisteredLabel, iana::CoapContentFormat};
6use serde::{Deserialize, Serialize, de::DeserializeOwned};
7use thiserror::Error;
8#[cfg(feature = "wasm")]
9use wasm_bindgen::convert::FromWasmAbi;
10
11use crate::{
12 CONTENT_TYPE_PADDED_CBOR, CoseEncrypt0Bytes, CryptoError, EncString, EncodingError, KeyIds,
13 SerializedMessage, SymmetricCryptoKey, XChaCha20Poly1305Key,
14 cose::{ContentNamespace, SafeObjectNamespace, XCHACHA20_POLY1305},
15 safe::helpers::{debug_fmt, set_safe_namespaces, validate_safe_namespaces},
16 utils::pad_bytes,
17 xchacha20,
18};
19
20pub(crate) const DATA_ENVELOPE_PADDING_SIZE: usize = 64;
21
22pub trait SealableVersionedData: Serialize + DeserializeOwned {
26 const NAMESPACE: DataEnvelopeNamespace;
28}
29
30pub trait SealableData: Serialize + DeserializeOwned {}
44
45#[derive(Clone)]
56pub struct DataEnvelope {
57 envelope_data: CoseEncrypt0Bytes,
58}
59
60impl DataEnvelope {
61 pub fn seal<Ids: KeyIds, T>(
64 data: T,
65 ctx: &mut crate::store::KeyStoreContext<Ids>,
66 ) -> Result<(Self, Ids::Symmetric), DataEnvelopeError>
67 where
68 T: Serialize + SealableVersionedData,
69 {
70 let (envelope, cek) = Self::seal_ref(&data, T::NAMESPACE)?;
71 let cek_id = ctx.generate_symmetric_key();
72 ctx.set_symmetric_key_internal(cek_id, SymmetricCryptoKey::XChaCha20Poly1305Key(cek))
73 .map_err(|_| DataEnvelopeError::KeyStoreError)?;
74 Ok((envelope, cek_id))
75 }
76
77 pub fn seal_with_wrapping_key<Ids: KeyIds, T>(
80 data: T,
81 wrapping_key: &Ids::Symmetric,
82 ctx: &mut crate::store::KeyStoreContext<Ids>,
83 ) -> Result<(Self, EncString), DataEnvelopeError>
84 where
85 T: Serialize + SealableVersionedData,
86 {
87 let (envelope, cek) = Self::seal(data, ctx)?;
88
89 let wrapped_cek = ctx
90 .wrap_symmetric_key(*wrapping_key, cek)
91 .map_err(|_| DataEnvelopeError::EncryptionError)?;
92
93 Ok((envelope, wrapped_cek))
94 }
95
96 fn seal_ref<T>(
99 data: &T,
100 namespace: DataEnvelopeNamespace,
101 ) -> Result<(DataEnvelope, XChaCha20Poly1305Key), DataEnvelopeError>
102 where
103 T: Serialize + SealableVersionedData,
104 {
105 let mut cek = XChaCha20Poly1305Key::make();
106
107 let serialized_message =
109 SerializedMessage::encode(&data).map_err(|_| DataEnvelopeError::EncodingError)?;
110 if serialized_message.content_type() != coset::iana::CoapContentFormat::Cbor {
111 return Err(DataEnvelopeError::UnsupportedContentFormat);
112 }
113
114 let serialized_and_padded_message = pad_cbor(serialized_message.as_bytes())
115 .map_err(|_| DataEnvelopeError::EncodingError)?;
116
117 let mut protected_header = coset::HeaderBuilder::new()
119 .key_id(cek.key_id.as_slice().to_vec())
120 .content_type(CONTENT_TYPE_PADDED_CBOR.to_string())
121 .build();
122 set_safe_namespaces(
123 &mut protected_header,
124 SafeObjectNamespace::DataEnvelope,
125 namespace,
126 );
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 content_format =
205 content_format(&msg.protected).map_err(|_| DataEnvelopeError::DecodingError)?;
206
207 if !matches!(
209 msg.protected.header.alg,
210 Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305)),
211 ) {
212 return Err(DataEnvelopeError::DecryptionError);
213 }
214 if msg.protected.header.key_id != cek.key_id.as_slice() {
215 return Err(DataEnvelopeError::WrongKey);
216 }
217
218 validate_safe_namespaces(
219 &msg.protected.header,
220 SafeObjectNamespace::DataEnvelope,
221 namespace,
222 )
223 .map_err(|_| DataEnvelopeError::InvalidNamespace)?;
224
225 if content_format != CONTENT_TYPE_PADDED_CBOR {
226 return Err(DataEnvelopeError::UnsupportedContentFormat);
227 }
228
229 let decrypted_message = msg
231 .decrypt_ciphertext(
232 &[],
233 || CryptoError::MissingField("ciphertext"),
234 |data, aad| {
235 let nonce = msg.unprotected.iv.as_slice();
236 crate::xchacha20::decrypt_xchacha20_poly1305(
237 nonce
238 .try_into()
239 .map_err(|_| CryptoError::InvalidNonceLength)?,
240 &(*cek.enc_key).into(),
241 data,
242 aad,
243 )
244 },
245 )
246 .map_err(|_| DataEnvelopeError::DecryptionError)?;
247
248 let unpadded_message =
249 unpad_cbor(&decrypted_message).map_err(|_| DataEnvelopeError::DecryptionError)?;
250
251 let serialized_message =
253 SerializedMessage::from_bytes(unpadded_message, CoapContentFormat::Cbor);
254 serialized_message
255 .decode()
256 .map_err(|_| DataEnvelopeError::DecodingError)
257 }
258}
259
260pub(super) fn content_format(protected_header: &ProtectedHeader) -> Result<String, EncodingError> {
264 protected_header
265 .header
266 .content_type
267 .as_ref()
268 .and_then(|ct| match ct {
269 RegisteredLabel::Text(content_format) => Some(content_format.clone()),
270 _ => None,
271 })
272 .ok_or(EncodingError::InvalidCoseEncoding)
273}
274
275impl From<&DataEnvelope> for Vec<u8> {
276 fn from(val: &DataEnvelope) -> Self {
277 val.envelope_data.to_vec()
278 }
279}
280
281impl From<Vec<u8>> for DataEnvelope {
282 fn from(data: Vec<u8>) -> Self {
283 DataEnvelope {
284 envelope_data: CoseEncrypt0Bytes::from(data),
285 }
286 }
287}
288
289impl std::fmt::Debug for DataEnvelope {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 let mut s = f.debug_struct("DataEnvelope");
292 if let Ok(msg) = coset::CoseEncrypt0::from_slice(self.envelope_data.as_ref()) {
293 debug_fmt::<DataEnvelopeNamespace>(&mut s, &msg.protected.header);
294 }
295 s.finish()
296 }
297}
298
299impl FromStr for DataEnvelope {
300 type Err = NotB64EncodedError;
301
302 fn from_str(s: &str) -> Result<Self, Self::Err> {
303 let data = B64::try_from(s)?;
304 Ok(Self::from(data.into_bytes()))
305 }
306}
307
308impl From<DataEnvelope> for String {
309 fn from(val: DataEnvelope) -> Self {
310 let serialized: Vec<u8> = (&val).into();
311 B64::from(serialized).to_string()
312 }
313}
314
315impl<'de> Deserialize<'de> for DataEnvelope {
316 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
317 where
318 D: serde::Deserializer<'de>,
319 {
320 deserializer.deserialize_str(FromStrVisitor::new())
321 }
322}
323
324impl Serialize for DataEnvelope {
325 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
326 where
327 S: serde::Serializer,
328 {
329 let serialized: Vec<u8> = self.into();
330 serializer.serialize_str(&B64::from(serialized).to_string())
331 }
332}
333
334impl std::fmt::Display for DataEnvelope {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 let serialized: Vec<u8> = self.into();
337 write!(f, "{}", B64::from(serialized))
338 }
339}
340
341#[derive(Debug, Error)]
343pub enum DataEnvelopeError {
344 #[error("Unsupported content format")]
346 UnsupportedContentFormat,
347 #[error("Failed to decode COSE message")]
349 CoseDecodingError,
350 #[error("Failed to decode the content of the envelope")]
352 DecodingError,
353 #[error("Encoding error")]
355 EncodingError,
356 #[error("KeyStore error")]
358 KeyStoreError,
359 #[error("Decryption error")]
361 DecryptionError,
362 #[error("Encryption error")]
364 EncryptionError,
365 #[error("Parsing error: {0}")]
367 ParsingError(String),
368 #[error("Invalid namespace")]
370 InvalidNamespace,
371 #[error("Wrong key used for decryption")]
373 WrongKey,
374}
375
376#[cfg(feature = "wasm")]
377#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
378const TS_CUSTOM_TYPES: &'static str = r#"
379export type DataEnvelope = Tagged<string, "DataEnvelope">;
380"#;
381
382#[cfg(feature = "wasm")]
383impl wasm_bindgen::describe::WasmDescribe for DataEnvelope {
384 fn describe() {
385 <String as wasm_bindgen::describe::WasmDescribe>::describe();
386 }
387}
388
389#[cfg(feature = "wasm")]
390impl FromWasmAbi for DataEnvelope {
391 type Abi = <String as FromWasmAbi>::Abi;
392
393 unsafe fn from_abi(abi: Self::Abi) -> Self {
394 use wasm_bindgen::UnwrapThrowExt;
395
396 let s = unsafe { String::from_abi(abi) };
397 Self::from_str(&s).unwrap_throw()
398 }
399}
400
401fn pad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
402 let mut data = data.to_vec();
403 pad_bytes(&mut data, DATA_ENVELOPE_PADDING_SIZE).map_err(|_| CryptoError::InvalidPadding)?;
404 Ok(data)
405}
406
407fn unpad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
408 let unpadded = crate::utils::unpad_bytes(data).map_err(|_| CryptoError::InvalidPadding)?;
409 Ok(unpadded.to_vec())
410}
411
412#[macro_export]
447macro_rules! generate_versioned_sealable {
448 (
449 $enum_name:ident,
451 $namespace:path,
453 [ $( $variant_ty:ident => $rename:literal ),+ $(,)? ]
455 ) => {
456 #[derive(Serialize, Deserialize, Debug, PartialEq)]
458 #[serde(tag = "version", content = "content")]
459 enum $enum_name {
460 $(
461 #[serde(rename = $rename)]
462 $variant_ty($variant_ty),
464 )+
465 }
466
467 impl SealableVersionedData for $enum_name
469 where
470 $( $variant_ty: SealableData ),+
471 {
472 const NAMESPACE: DataEnvelopeNamespace = $namespace;
474 }
475
476 $(
478 impl From<$variant_ty> for $enum_name {
479 fn from(value: $variant_ty) -> Self {
480 Self::$variant_ty(value)
481 }
482 }
483 )+
484 };
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
494pub enum DataEnvelopeNamespace {
495 VaultItem = 1,
497 #[cfg(test)]
499 ExampleNamespace = -1,
500 #[cfg(test)]
502 ExampleNamespace2 = -2,
503}
504
505impl DataEnvelopeNamespace {
506 fn as_i64(&self) -> i64 {
508 *self as i64
509 }
510}
511
512impl TryFrom<i128> for DataEnvelopeNamespace {
513 type Error = DataEnvelopeError;
514
515 fn try_from(value: i128) -> Result<Self, Self::Error> {
516 match value {
517 1 => Ok(DataEnvelopeNamespace::VaultItem),
518 #[cfg(test)]
519 -1 => Ok(DataEnvelopeNamespace::ExampleNamespace),
520 #[cfg(test)]
521 -2 => Ok(DataEnvelopeNamespace::ExampleNamespace2),
522 _ => Err(DataEnvelopeError::InvalidNamespace),
523 }
524 }
525}
526
527impl TryFrom<i64> for DataEnvelopeNamespace {
528 type Error = DataEnvelopeError;
529
530 fn try_from(value: i64) -> Result<Self, Self::Error> {
531 Self::try_from(i128::from(value))
532 }
533}
534
535impl From<DataEnvelopeNamespace> for i128 {
536 fn from(val: DataEnvelopeNamespace) -> Self {
537 val.as_i64().into()
538 }
539}
540
541impl ContentNamespace for DataEnvelopeNamespace {}
542
543#[cfg(test)]
544mod tests {
545 use serde::Deserialize;
546
547 use super::*;
548 use crate::traits::tests::TestIds;
549
550 #[derive(Serialize, Deserialize, Debug, PartialEq)]
551 struct TestDataV1 {
552 field: u32,
553 }
554 impl SealableData for TestDataV1 {}
555
556 generate_versioned_sealable!(
557 TestData,
558 DataEnvelopeNamespace::ExampleNamespace,
559 [
560 TestDataV1 => "1",
561 ]
562 );
563
564 const TEST_VECTOR_CEK: &str =
565 "pQEEAlB5RTKA0xXdA7C4iQE4QfVUAzoAARFvBIEEIFggQYqnsrAfeFFTaXGXB54YrksB6eQcctMpnaZ8rG6rMJ0B";
566 const TEST_VECTOR_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFB5RTKA0xXdA7C4iQE4QfVUOgABOIECOgABOIAgoQVYGLfQrYHVWxRxO6A8m/yp5DPbBIn3h8nijlhQj4jFwDLWfFz7le1Oy8dTls5vdEFg/FjjsPvXicI2bdb5KDdJCz/YkEu0kqjpQwdCcALpJLVJwgQQeKIeU2klBHEPZjnlLpRRXeCUp5c5BYQ=";
567
568 #[test]
569 #[ignore = "Manual test to verify debug format"]
570 fn test_debug() {
571 let data: TestData = TestDataV1 { field: 42 }.into();
572 let (envelope, _cek) =
573 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
574 println!("{:?}", envelope);
575 }
576
577 #[test]
578 #[ignore]
579 fn generate_test_vectors() {
580 let data: TestData = TestDataV1 { field: 123 }.into();
581 let (envelope, cek) =
582 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
583 let unsealed_data: TestData = envelope
584 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace, &cek)
585 .unwrap();
586 assert_eq!(unsealed_data, data);
587 println!(
588 "const TEST_VECTOR_CEK: &str = \"{}\";",
589 B64::from(SymmetricCryptoKey::XChaCha20Poly1305Key(cek).to_encoded())
590 );
591 println!(
592 "const TEST_VECTOR_ENVELOPE: &str = \"{}\";",
593 String::from(envelope)
594 );
595 }
596
597 #[test]
598 fn test_data_envelope_test_vector() {
599 let cek = SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_CEK).unwrap()).unwrap();
600 let cek = match cek {
601 SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) => key.clone(),
602 _ => panic!("Invalid CEK type"),
603 };
604
605 let envelope: DataEnvelope = TEST_VECTOR_ENVELOPE.parse().unwrap();
606 let unsealed_data: TestData = envelope
607 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace, &cek)
608 .unwrap();
609 assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
610 }
611
612 #[test]
613 fn test_data_envelope() {
614 let data: TestData = TestDataV1 { field: 42 }.into();
616
617 let (envelope, cek) =
619 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
620 let unsealed_data: TestData = envelope
621 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace, &cek)
622 .unwrap();
623
624 assert_eq!(unsealed_data, data);
626 }
627
628 #[test]
629 fn test_namespace_validation_success() {
630 let data: TestData = TestDataV1 { field: 123 }.into();
631
632 let (envelope1, cek1) =
634 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
635 let unsealed_data1: TestData = envelope1
636 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace, &cek1)
637 .unwrap();
638 assert_eq!(unsealed_data1, data);
639
640 let (envelope2, cek2) =
642 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace2).unwrap();
643 let unsealed_data2: TestData = envelope2
644 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace2, &cek2)
645 .unwrap();
646 assert_eq!(unsealed_data2, data);
647 }
648
649 #[test]
650 fn test_namespace_validation_failure() {
651 let data: TestData = TestDataV1 { field: 456 }.into();
652
653 let (envelope, cek) =
655 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
656
657 let result: Result<TestData, DataEnvelopeError> =
659 envelope.unseal_ref(DataEnvelopeNamespace::ExampleNamespace2, &cek);
660 assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
661
662 let unsealed_data: TestData = envelope
664 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace, &cek)
665 .unwrap();
666 assert_eq!(unsealed_data, data);
667 }
668
669 #[test]
670 fn test_namespace_validation_with_keystore() {
671 let data: TestData = TestDataV1 { field: 789 }.into();
672 let key_store = crate::store::KeyStore::<TestIds>::default();
673 let mut ctx = key_store.context_mut();
674
675 let (envelope, cek) =
677 DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace2).unwrap();
678 ctx.set_symmetric_key_internal(
679 crate::traits::tests::TestSymmKey::A(0),
680 SymmetricCryptoKey::XChaCha20Poly1305Key(cek),
681 )
682 .unwrap();
683
684 let result: Result<TestData, DataEnvelopeError> =
686 envelope.unseal(crate::traits::tests::TestSymmKey::A(0), &mut ctx);
687 assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
688 }
689
690 #[test]
691 fn test_namespace_cross_contamination_protection() {
692 let data1: TestData = TestDataV1 { field: 111 }.into();
693 let data2: TestData = TestDataV1 { field: 222 }.into();
694
695 let (envelope1, cek1) =
697 DataEnvelope::seal_ref(&data1, DataEnvelopeNamespace::ExampleNamespace).unwrap();
698 let (envelope2, cek2) =
699 DataEnvelope::seal_ref(&data2, DataEnvelopeNamespace::ExampleNamespace2).unwrap();
700
701 let unsealed1: TestData = envelope1
703 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace, &cek1)
704 .unwrap();
705 assert_eq!(unsealed1, data1);
706
707 let unsealed2: TestData = envelope2
708 .unseal_ref(DataEnvelopeNamespace::ExampleNamespace2, &cek2)
709 .unwrap();
710 assert_eq!(unsealed2, data2);
711
712 assert!(matches!(
714 envelope1.unseal_ref::<TestData>(DataEnvelopeNamespace::ExampleNamespace2, &cek1),
715 Err(DataEnvelopeError::InvalidNamespace)
716 ));
717 assert!(matches!(
718 envelope2.unseal_ref::<TestData>(DataEnvelopeNamespace::ExampleNamespace, &cek2),
719 Err(DataEnvelopeError::InvalidNamespace)
720 ));
721 }
722}