Skip to main content

bitwarden_crypto/safe/
data_envelope.rs

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    Aes256GcmKey, CONTENT_TYPE_PADDED_CBOR, CoseEncrypt0Bytes, CoseKeyView, CryptoError, EncString,
13    EncodingError, KeyId, KeySlotIds, SerializedMessage, SymmetricCryptoKey,
14    cose::{
15        ContentNamespace, SafeObjectNamespace,
16        symmetric::{
17            CoseAlgorithmPolicy, CoseContentEncryptionAlgorithm, decrypt_cose0, encrypt_cose0,
18        },
19    },
20    safe::helpers::{debug_fmt, set_safe_namespaces, validate_safe_namespaces},
21    utils::pad_bytes,
22};
23
24pub(crate) const DATA_ENVELOPE_PADDING_SIZE: usize = 64;
25
26/// Marker trait for data that can be sealed in a `DataEnvelope`.
27///
28/// Do not manually implement this! Use the generate_versioned_sealable! macro instead.
29pub trait SealableVersionedData: Serialize + DeserializeOwned {
30    /// The namespace to use when sealing this type of data. This must be unique per struct.
31    const NAMESPACE: DataEnvelopeNamespace;
32}
33
34/// Marker trait for data that can be sealed in a `DataEnvelope`.
35///
36/// Note: If you implement this trait, you agree to the following:
37/// The struct serialization format is stable. Struct modifications must maintain backward
38/// compatibility with existing serialized data. Changes that break deserialization are considered
39/// breaking changes and require a new version and struct.
40///
41/// Ideally, when creating a new struct, create a test vector (a sealed DataEnvelope for a test
42/// value), and create a unit test ensuring that it permanently deserializes correctly.
43///
44/// To make breaking changes, introduce a new version. This should use the
45/// `generate_versioned_sealable!` macro to auto-generate the versioning code. Please see the
46/// examples directory.
47pub trait SealableData: Serialize + DeserializeOwned {}
48
49/// `DataEnvelope` allows sealing structs entire structs to encrypted blobs.
50///
51/// Sealing a struct results in an encrypted blob, and a content-encryption-key. The
52/// content-encryption-key must be provided again when unsealing the data. A content encryption key
53/// allows easy key-rotation of the encrypting-key, as now just the content-encryption-keys need to
54/// be re-uploaded, instead of all data.
55///
56/// The content-encryption-key cannot be re-used for encrypting other data.
57///
58/// Note: This is explicitly meant for structured data, not large binary blobs (files).
59#[derive(Clone)]
60pub struct DataEnvelope {
61    envelope_data: CoseEncrypt0Bytes,
62}
63
64impl DataEnvelope {
65    /// Seals a struct into an encrypted blob, and stores the content-encryption-key in the provided
66    /// context.
67    pub fn seal<Ids: KeySlotIds, T>(
68        data: T,
69        ctx: &mut crate::store::KeyStoreContext<Ids>,
70    ) -> Result<(Self, Ids::Symmetric), DataEnvelopeError>
71    where
72        T: Serialize + SealableVersionedData,
73    {
74        let (envelope, cek) = Self::seal_ref(&data, T::NAMESPACE)?;
75        let cek_id = ctx.generate_symmetric_key();
76        ctx.set_symmetric_key_internal(cek_id, SymmetricCryptoKey::Aes256GcmKey(cek))
77            .map_err(|_| DataEnvelopeError::KeyStore)?;
78        Ok((envelope, cek_id))
79    }
80
81    /// Seals a struct into an encrypted blob. The content encryption key is wrapped with the
82    /// provided wrapping key
83    pub fn seal_with_wrapping_key<Ids: KeySlotIds, T>(
84        data: T,
85        wrapping_key: &Ids::Symmetric,
86        ctx: &mut crate::store::KeyStoreContext<Ids>,
87    ) -> Result<(Self, EncString), DataEnvelopeError>
88    where
89        T: Serialize + SealableVersionedData,
90    {
91        let (envelope, cek) = Self::seal(data, ctx)?;
92
93        let wrapped_cek = ctx
94            .wrap_symmetric_key(*wrapping_key, cek)
95            .map_err(|_| DataEnvelopeError::Encryption)?;
96
97        Ok((envelope, wrapped_cek))
98    }
99
100    /// Seals a struct into an encrypted blob, and returns the encrypted blob and the
101    /// content-encryption-key.
102    fn seal_ref<T>(
103        data: &T,
104        namespace: DataEnvelopeNamespace,
105    ) -> Result<(DataEnvelope, Aes256GcmKey), DataEnvelopeError>
106    where
107        T: Serialize + SealableVersionedData,
108    {
109        let mut cek = Aes256GcmKey::make();
110
111        // Serialize the message
112        let serialized_message =
113            SerializedMessage::encode(&data).map_err(|_| DataEnvelopeError::Encoding)?;
114        if serialized_message.content_type() != coset::iana::CoapContentFormat::Cbor {
115            return Err(DataEnvelopeError::UnsupportedContentFormat);
116        }
117
118        let serialized_and_padded_message =
119            pad_cbor(serialized_message.as_bytes()).map_err(|_| DataEnvelopeError::Encoding)?;
120
121        // Build the COSE headers
122        let mut protected_header = coset::HeaderBuilder::new()
123            .key_id(cek.key_id.as_slice().to_vec())
124            .content_type(CONTENT_TYPE_PADDED_CBOR.to_string())
125            .build();
126        set_safe_namespaces(
127            &mut protected_header,
128            SafeObjectNamespace::DataEnvelope,
129            namespace,
130        );
131
132        // Encrypt the message. `encrypt_cose0` declares the content-encryption algorithm
133        // (AES-256-GCM) in the protected header and stores a fresh nonce in the unprotected `iv`
134        // header.
135        let encrypt0 = encrypt_cose0(
136            CoseContentEncryptionAlgorithm::Aes256Gcm,
137            coset::CoseEncrypt0Builder::new(),
138            protected_header,
139            &serialized_and_padded_message,
140            cek.enc_key.as_slice(),
141        )
142        .map_err(|_| DataEnvelopeError::Encoding)?;
143
144        // Serialize the COSE message
145        let envelope_data = encrypt0
146            .to_vec()
147            .map(CoseEncrypt0Bytes::from)
148            .map_err(|_| DataEnvelopeError::Encoding)?;
149
150        // Disable key operations other than decrypt on the CEK
151        cek.disable_key_operation(coset::iana::KeyOperation::Encrypt)
152            .disable_key_operation(coset::iana::KeyOperation::WrapKey)
153            .disable_key_operation(coset::iana::KeyOperation::UnwrapKey);
154
155        Ok((DataEnvelope { envelope_data }, cek))
156    }
157
158    /// Unseals the data from the encrypted blob using a content-encryption-key stored in the
159    /// context.
160    pub fn unseal<Ids: KeySlotIds, T>(
161        &self,
162        cek_keyslot: Ids::Symmetric,
163        ctx: &mut crate::store::KeyStoreContext<Ids>,
164    ) -> Result<T, DataEnvelopeError>
165    where
166        T: DeserializeOwned + SealableVersionedData,
167    {
168        let cek = ctx
169            .get_symmetric_key(cek_keyslot)
170            .map_err(|_| DataEnvelopeError::KeyStore)?;
171
172        // AES-256-GCM (current), XAES-256-GCM, and XChaCha20-Poly1305 (legacy)
173        // content-encryption keys are accepted. The typed key's algorithm must match the algorithm
174        // in the envelope's protected header.
175        let view = cek
176            .as_cose_key_view()
177            .ok_or(DataEnvelopeError::UnsupportedContentFormat)?;
178        self.unseal_ref(T::NAMESPACE, view)
179    }
180
181    /// Unseals the data from the encrypted blob and wrapped content-encryption-key.
182    pub fn unseal_with_wrapping_key<Ids: KeySlotIds, T>(
183        &self,
184        wrapping_key: &Ids::Symmetric,
185        wrapped_cek: &EncString,
186        ctx: &mut crate::store::KeyStoreContext<Ids>,
187    ) -> Result<T, DataEnvelopeError>
188    where
189        T: DeserializeOwned + SealableVersionedData,
190    {
191        let cek = ctx
192            .unwrap_symmetric_key(*wrapping_key, wrapped_cek)
193            .map_err(|_| DataEnvelopeError::Decryption)?;
194        self.unseal(cek, ctx)
195    }
196
197    /// Unseals the data from the encrypted blob using the provided content-encryption-key, which
198    /// may be an AES-256-GCM, XAES-256-GCM, or legacy XChaCha20-Poly1305 key.
199    fn unseal_ref<T>(
200        &self,
201        namespace: DataEnvelopeNamespace,
202        cek: CoseKeyView,
203    ) -> Result<T, DataEnvelopeError>
204    where
205        T: DeserializeOwned + SealableVersionedData,
206    {
207        // Parse the COSE message
208        let msg = coset::CoseEncrypt0::from_slice(self.envelope_data.as_ref())
209            .map_err(|_| DataEnvelopeError::CoseDecoding)?;
210        let content_format =
211            content_format(&msg.protected).map_err(|_| DataEnvelopeError::Decoding)?;
212
213        // Validate the message
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        // Bind the protected content-encryption algorithm to the independently typed CEK before
230        // attempting decryption. DataEnvelope has no legacy format that omits the algorithm.
231        let decrypted_message = decrypt_cose0(
232            &msg,
233            CoseAlgorithmPolicy::Exactly(cek.algorithm()),
234            cek.key_bytes(),
235        )
236        .map_err(|_| DataEnvelopeError::Decryption)?;
237
238        let unpadded_message =
239            unpad_cbor(&decrypted_message).map_err(|_| DataEnvelopeError::Decryption)?;
240
241        // Deserialize the message
242        let serialized_message =
243            SerializedMessage::from_bytes(unpadded_message, CoapContentFormat::Cbor);
244        serialized_message
245            .decode()
246            .map_err(|_| DataEnvelopeError::Decoding)
247    }
248}
249
250/// Helper function to extract the content type from a `ProtectedHeader`. The content type is a
251/// standardized header set on the protected headers of the signature object. Currently we only
252/// support registered values, but PrivateUse values are also allowed in the COSE specification.
253pub(super) fn content_format(protected_header: &ProtectedHeader) -> Result<String, EncodingError> {
254    protected_header
255        .header
256        .content_type
257        .as_ref()
258        .and_then(|ct| match ct {
259            RegisteredLabel::Text(content_format) => Some(content_format.clone()),
260            _ => None,
261        })
262        .ok_or(EncodingError::InvalidCoseEncoding)
263}
264
265impl From<&DataEnvelope> for Vec<u8> {
266    fn from(val: &DataEnvelope) -> Self {
267        val.envelope_data.to_vec()
268    }
269}
270
271impl From<Vec<u8>> for DataEnvelope {
272    fn from(data: Vec<u8>) -> Self {
273        DataEnvelope {
274            envelope_data: CoseEncrypt0Bytes::from(data),
275        }
276    }
277}
278
279impl std::fmt::Debug for DataEnvelope {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        let mut s = f.debug_struct("DataEnvelope");
282        if let Ok(msg) = coset::CoseEncrypt0::from_slice(self.envelope_data.as_ref()) {
283            debug_fmt::<DataEnvelopeNamespace>(&mut s, &msg.protected.header);
284            if let Ok(encrypted_by) = KeyId::try_from(msg.protected.header.key_id.as_slice()) {
285                s.field("encrypted_by", &encrypted_by);
286            }
287        }
288        s.finish()
289    }
290}
291
292impl FromStr for DataEnvelope {
293    type Err = NotB64EncodedError;
294
295    fn from_str(s: &str) -> Result<Self, Self::Err> {
296        let data = B64::try_from(s)?;
297        Ok(Self::from(data.into_bytes()))
298    }
299}
300
301impl From<DataEnvelope> for String {
302    fn from(val: DataEnvelope) -> Self {
303        let serialized: Vec<u8> = (&val).into();
304        B64::from(serialized).to_string()
305    }
306}
307
308impl<'de> Deserialize<'de> for DataEnvelope {
309    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
310    where
311        D: serde::Deserializer<'de>,
312    {
313        deserializer.deserialize_str(FromStrVisitor::new())
314    }
315}
316
317impl Serialize for DataEnvelope {
318    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
319    where
320        S: serde::Serializer,
321    {
322        let serialized: Vec<u8> = self.into();
323        serializer.serialize_str(&B64::from(serialized).to_string())
324    }
325}
326
327impl std::fmt::Display for DataEnvelope {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        let serialized: Vec<u8> = self.into();
330        write!(f, "{}", B64::from(serialized))
331    }
332}
333
334/// Error type for `DataEnvelope` operations.
335#[derive(Debug, Error)]
336pub enum DataEnvelopeError {
337    /// Indicates that the content format is not supported.
338    #[error("Unsupported content format")]
339    UnsupportedContentFormat,
340    /// Indicates that there was an error during decoding of the message.
341    #[error("Failed to decode COSE message")]
342    CoseDecoding,
343    /// Indicates that there was an error during decoding of the message.
344    #[error("Failed to decode the content of the envelope")]
345    Decoding,
346    /// Indicates that there was an error during encoding of the message.
347    #[error("Encoding error")]
348    Encoding,
349    /// Indicates that there was an error with the key store.
350    #[error("KeyStore error")]
351    KeyStore,
352    /// Indicates that there was an error during decryption.
353    #[error("Decryption error")]
354    Decryption,
355    /// Indicates that there was an error during encryption.
356    #[error("Encryption error")]
357    Encryption,
358    /// Indicates that there was an error parsing the DataEnvelope.
359    #[error("Parsing error: {0}")]
360    Parsing(String),
361    /// Indicates that the data envelope namespace is invalid.
362    #[error("Invalid namespace")]
363    InvalidNamespace,
364    /// Indicates that the wrong key was used for decryption.
365    #[error("Wrong key used for decryption")]
366    WrongKey,
367}
368
369#[cfg(feature = "wasm")]
370#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
371const TS_CUSTOM_TYPES: &'static str = r#"
372export type DataEnvelope = Tagged<string, "DataEnvelope">;
373"#;
374
375#[cfg(feature = "wasm")]
376impl wasm_bindgen::describe::WasmDescribe for DataEnvelope {
377    fn describe() {
378        <String as wasm_bindgen::describe::WasmDescribe>::describe();
379    }
380}
381
382#[cfg(feature = "wasm")]
383impl FromWasmAbi for DataEnvelope {
384    type Abi = <String as FromWasmAbi>::Abi;
385
386    unsafe fn from_abi(abi: Self::Abi) -> Self {
387        use wasm_bindgen::UnwrapThrowExt;
388
389        let s = unsafe { String::from_abi(abi) };
390        Self::from_str(&s).unwrap_throw()
391    }
392}
393
394fn pad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
395    let mut data = data.to_vec();
396    pad_bytes(&mut data, DATA_ENVELOPE_PADDING_SIZE).map_err(|_| CryptoError::InvalidPadding)?;
397    Ok(data)
398}
399
400fn unpad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
401    let unpadded = crate::utils::unpad_bytes(data).map_err(|_| CryptoError::InvalidPadding)?;
402    Ok(unpadded.to_vec())
403}
404
405/// Generates a versioned enum that implements `SealableData`.
406///
407/// This serializes to an adjacently tagged enum, with the "version" field being set to the provided
408/// version, and the "content" field being the serialized struct.
409///
410///
411/// ```
412/// use bitwarden_crypto::{safe::{DataEnvelopeNamespace, SealableData, SealableVersionedData}, generate_versioned_sealable};
413/// use serde::{Deserialize, Serialize};
414///
415/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
416/// struct MyItemV1 {
417///     a: u32,
418///     b: String,
419/// }
420/// impl SealableData for MyItemV1 {}
421///
422/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
423/// struct MyItemV2 {
424///     a: u32,
425///     b: bool,
426///     c: bool,
427/// }
428/// impl SealableData for MyItemV2 {}
429///
430/// generate_versioned_sealable!(
431///     MyItem,
432///     DataEnvelopeNamespace::VaultItem,
433///     [
434///         MyItemV1 => "1",
435///         MyItemV2 => "2",
436///     ]
437/// );
438/// ```
439#[macro_export]
440macro_rules! generate_versioned_sealable {
441    (
442        // Provide the name
443        $enum_name:ident,
444        // Provide the namespace
445        $namespace:path,
446        // Provide mappings from the variant to version. This must not be changed later.
447        [ $( $variant_ty:ident => $rename:literal ),+ $(,)? ]
448    ) => {
449        // Implement the enum
450        #[derive(Serialize, Deserialize, Debug, PartialEq)]
451        #[serde(tag = "version", content = "content")]
452        enum $enum_name {
453            $(
454                #[serde(rename = $rename)]
455                // Strip the `MyItem` prefix from type name if you want shorter variant names
456                $variant_ty($variant_ty),
457            )+
458        }
459
460        // Implement the SealableVersionedData trait for the enum
461        impl SealableVersionedData for $enum_name
462        where
463            $( $variant_ty: SealableData ),+
464        {
465            // Implement with the specified namespace
466            const NAMESPACE: DataEnvelopeNamespace = $namespace;
467        }
468
469        // Implement Into from each variant to the enum
470        $(
471            impl From<$variant_ty> for $enum_name {
472                fn from(value: $variant_ty) -> Self {
473                    Self::$variant_ty(value)
474                }
475            }
476        )+
477    };
478}
479
480/// Data envelopes are domain-separated within bitwarden, to prevent cross protocol attacks.
481///
482/// A new struct shall use a new data envelope namespace. Generally, this means
483/// that a data envelope namespace has exactly one associated valid message struct. Internal
484/// versioning within a namespace is permitted and up to the domain owner to ensure is done
485/// correctly.
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487pub enum DataEnvelopeNamespace {
488    /// The namespace for vault items ("ciphers")
489    VaultItem = 1,
490    /// This namespace is only used in tests
491    #[cfg(test)]
492    ExampleNamespace = -1,
493    /// This namespace is only used in tests
494    #[cfg(test)]
495    ExampleNamespace2 = -2,
496}
497
498impl DataEnvelopeNamespace {
499    /// Returns the numeric value of the namespace.
500    fn as_i64(&self) -> i64 {
501        *self as i64
502    }
503}
504
505impl TryFrom<i128> for DataEnvelopeNamespace {
506    type Error = DataEnvelopeError;
507
508    fn try_from(value: i128) -> Result<Self, Self::Error> {
509        match value {
510            1 => Ok(DataEnvelopeNamespace::VaultItem),
511            #[cfg(test)]
512            -1 => Ok(DataEnvelopeNamespace::ExampleNamespace),
513            #[cfg(test)]
514            -2 => Ok(DataEnvelopeNamespace::ExampleNamespace2),
515            _ => Err(DataEnvelopeError::InvalidNamespace),
516        }
517    }
518}
519
520impl TryFrom<i64> for DataEnvelopeNamespace {
521    type Error = DataEnvelopeError;
522
523    fn try_from(value: i64) -> Result<Self, Self::Error> {
524        Self::try_from(i128::from(value))
525    }
526}
527
528impl From<DataEnvelopeNamespace> for i128 {
529    fn from(val: DataEnvelopeNamespace) -> Self {
530        val.as_i64().into()
531    }
532}
533
534impl ContentNamespace for DataEnvelopeNamespace {}
535
536#[cfg(test)]
537mod tests {
538    use serde::Deserialize;
539
540    use super::*;
541    use crate::{SymmetricKeyAlgorithm, traits::tests::TestIds};
542
543    #[derive(Serialize, Deserialize, Debug, PartialEq)]
544    struct TestDataV1 {
545        field: u32,
546    }
547    impl SealableData for TestDataV1 {}
548
549    generate_versioned_sealable!(
550        TestData,
551        DataEnvelopeNamespace::ExampleNamespace,
552        [
553            TestDataV1 => "1",
554        ]
555    );
556
557    /// Legacy XChaCha20-Poly1305 test vector, kept to prove that DataEnvelopes sealed before the
558    /// switch to AES-256-GCM still decrypt (the algorithm is recovered from the protected header).
559    const TEST_VECTOR_CEK: &str =
560        "pQEEAlB5RTKA0xXdA7C4iQE4QfVUAzoAARFvBIEEIFggQYqnsrAfeFFTaXGXB54YrksB6eQcctMpnaZ8rG6rMJ0B";
561    const TEST_VECTOR_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFB5RTKA0xXdA7C4iQE4QfVUOgABOIECOgABOIAgoQVYGLfQrYHVWxRxO6A8m/yp5DPbBIn3h8nijlhQj4jFwDLWfFz7le1Oy8dTls5vdEFg/FjjsPvXicI2bdb5KDdJCz/YkEu0kqjpQwdCcALpJLVJwgQQeKIeU2klBHEPZjnlLpRRXeCUp5c5BYQ=";
562
563    /// AES-256-GCM test vector, generated by `generate_aes_gcm_test_vectors`. Locks the current
564    /// (FIPS-compatible) DataEnvelope wire format for backward compatibility.
565    const TEST_VECTOR_AES_GCM_CEK: &str =
566        "pQEEAlDLIx+izSLk9h9sVjHFzpKoAwMEgQQgWCDWM3iwTX2/LHTIaXS0cPIKCYFZethtKyD6Pucdt4fkGQQEBAQ=";
567    const TEST_VECTOR_AES_GCM_ENVELOPE: &str = "g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUMsjH6LNIuT2H2xWMcXOkqg6AAE4gQI6AAE4gCChBUyoF/oGEm+lJYrjjgdYUGIH5LnQjqFMWo2BJORVPYH2+hEWkxIn3tRgAMHNwIr0nTXMVD1EyVGZOsHSDMPqn2HaYrDeR5s+Rg0ezZ2WLh8n2FbdC44A/ExOms4IHcyT";
568
569    /// Test helper: unseal an envelope with an AES-256-GCM content-encryption key.
570    fn unseal_with_cek<T>(
571        envelope: &DataEnvelope,
572        namespace: DataEnvelopeNamespace,
573        cek: &Aes256GcmKey,
574    ) -> Result<T, DataEnvelopeError>
575    where
576        T: serde::de::DeserializeOwned + SealableVersionedData,
577    {
578        envelope.unseal_ref(namespace, CoseKeyView::Aes256Gcm(cek))
579    }
580
581    #[test]
582    #[ignore = "Manual test to verify debug format"]
583    fn test_debug() {
584        let data: TestData = TestDataV1 { field: 42 }.into();
585        let (envelope, _cek) =
586            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
587        println!("{:?}", envelope);
588    }
589
590    #[test]
591    #[ignore]
592    fn generate_aes_gcm_test_vectors() {
593        let data: TestData = TestDataV1 { field: 123 }.into();
594        let (envelope, cek) =
595            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
596        let unsealed_data: TestData =
597            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
598        assert_eq!(unsealed_data, data);
599        println!(
600            "const TEST_VECTOR_AES_GCM_CEK: &str = \"{}\";",
601            B64::from(SymmetricCryptoKey::Aes256GcmKey(cek).to_encoded())
602        );
603        println!(
604            "const TEST_VECTOR_AES_GCM_ENVELOPE: &str = \"{}\";",
605            String::from(envelope)
606        );
607    }
608
609    #[test]
610    fn test_data_envelope_legacy_xchacha20_test_vector() {
611        let cek = SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_CEK).unwrap()).unwrap();
612        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref cek) = cek else {
613            panic!("Invalid CEK type");
614        };
615
616        let envelope: DataEnvelope = TEST_VECTOR_ENVELOPE.parse().unwrap();
617        let unsealed_data: TestData = envelope
618            .unseal_ref(
619                DataEnvelopeNamespace::ExampleNamespace,
620                CoseKeyView::XChaCha20Poly1305(cek),
621            )
622            .unwrap();
623        assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
624    }
625
626    #[test]
627    fn test_data_envelope_aes_gcm_test_vector() {
628        let cek =
629            SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_AES_GCM_CEK).unwrap()).unwrap();
630        let SymmetricCryptoKey::Aes256GcmKey(ref cek) = cek else {
631            panic!("Invalid CEK type");
632        };
633
634        let envelope: DataEnvelope = TEST_VECTOR_AES_GCM_ENVELOPE.parse().unwrap();
635        let unsealed_data: TestData =
636            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, cek).unwrap();
637        assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
638    }
639
640    #[test]
641    fn test_data_envelope_uses_aes_gcm() {
642        let data: TestData = TestDataV1 { field: 42 }.into();
643        let (envelope, _cek) =
644            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
645
646        // New envelopes declare AES-256-GCM in their protected header.
647        let msg = coset::CoseEncrypt0::from_slice(envelope.envelope_data.as_ref()).unwrap();
648        assert_eq!(
649            msg.protected.header.alg,
650            Some(coset::Algorithm::Assigned(coset::iana::Algorithm::A256GCM))
651        );
652    }
653
654    #[test]
655    fn test_data_envelope() {
656        // Create an instance of TestData
657        let data: TestData = TestDataV1 { field: 42 }.into();
658
659        // Seal the data
660        let (envelope, cek) =
661            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
662        let unsealed_data: TestData =
663            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
664
665        // Verify that the unsealed data matches the original data
666        assert_eq!(unsealed_data, data);
667    }
668
669    #[test]
670    fn test_data_envelope_with_keystore_roundtrip() {
671        let data: TestData = TestDataV1 { field: 7 }.into();
672        let key_store = crate::store::KeyStore::<TestIds>::default();
673        let mut ctx = key_store.context_mut();
674
675        let (envelope, cek_id) = DataEnvelope::seal(data, &mut ctx).unwrap();
676
677        // The CEK stored in the key store is an AES-256-GCM key.
678        assert_eq!(
679            ctx.get_symmetric_key_algorithm(cek_id).unwrap(),
680            SymmetricKeyAlgorithm::Aes256Gcm
681        );
682
683        let unsealed: TestData = envelope.unseal(cek_id, &mut ctx).unwrap();
684        assert_eq!(unsealed, TestDataV1 { field: 7 }.into());
685    }
686
687    #[test]
688    fn test_data_envelope_wrapping_key_roundtrip() {
689        let data: TestData = TestDataV1 { field: 99 }.into();
690        let key_store = crate::store::KeyStore::<TestIds>::default();
691        let mut ctx = key_store.context_mut();
692
693        let wrapping_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
694
695        // The AES-256-GCM CEK is wrapped by the wrapping key (encoded as a COSE key) and unwrapped
696        // back for unsealing.
697        let (envelope, wrapped_cek) =
698            DataEnvelope::seal_with_wrapping_key(data, &wrapping_key, &mut ctx).unwrap();
699        let unsealed: TestData = envelope
700            .unseal_with_wrapping_key(&wrapping_key, &wrapped_cek, &mut ctx)
701            .unwrap();
702        assert_eq!(unsealed, TestDataV1 { field: 99 }.into());
703    }
704
705    #[test]
706    fn test_namespace_validation_success() {
707        let data: TestData = TestDataV1 { field: 123 }.into();
708
709        // Test with ExampleNamespace
710        let (envelope1, cek1) =
711            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
712        let unsealed_data1: TestData =
713            unseal_with_cek(&envelope1, DataEnvelopeNamespace::ExampleNamespace, &cek1).unwrap();
714        assert_eq!(unsealed_data1, data);
715
716        // Test with ExampleNamespace2
717        let (envelope2, cek2) =
718            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace2).unwrap();
719        let unsealed_data2: TestData =
720            unseal_with_cek(&envelope2, DataEnvelopeNamespace::ExampleNamespace2, &cek2).unwrap();
721        assert_eq!(unsealed_data2, data);
722    }
723
724    #[test]
725    fn test_namespace_validation_failure() {
726        let data: TestData = TestDataV1 { field: 456 }.into();
727
728        // Seal with ExampleNamespace
729        let (envelope, cek) =
730            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace).unwrap();
731
732        // Try to unseal with wrong namespace - should fail
733        let result: Result<TestData, DataEnvelopeError> =
734            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace2, &cek);
735        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
736
737        // Verify correct namespace still works
738        let unsealed_data: TestData =
739            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
740        assert_eq!(unsealed_data, data);
741    }
742
743    #[test]
744    fn test_namespace_validation_with_keystore() {
745        let data: TestData = TestDataV1 { field: 789 }.into();
746        let key_store = crate::store::KeyStore::<TestIds>::default();
747        let mut ctx = key_store.context_mut();
748
749        // Seal with keystore using ExampleNamespace2
750        let (envelope, cek) =
751            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace2).unwrap();
752        ctx.set_symmetric_key_internal(
753            crate::traits::tests::TestSymmKey::A(0),
754            SymmetricCryptoKey::Aes256GcmKey(cek),
755        )
756        .unwrap();
757
758        // Try to unseal with wrong namespace - should fail
759        let result: Result<TestData, DataEnvelopeError> =
760            envelope.unseal(crate::traits::tests::TestSymmKey::A(0), &mut ctx);
761        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
762    }
763
764    #[test]
765    fn test_namespace_cross_contamination_protection() {
766        let data1: TestData = TestDataV1 { field: 111 }.into();
767        let data2: TestData = TestDataV1 { field: 222 }.into();
768
769        // Seal two different pieces of data with different namespaces
770        let (envelope1, cek1) =
771            DataEnvelope::seal_ref(&data1, DataEnvelopeNamespace::ExampleNamespace).unwrap();
772        let (envelope2, cek2) =
773            DataEnvelope::seal_ref(&data2, DataEnvelopeNamespace::ExampleNamespace2).unwrap();
774
775        // Verify each envelope only opens with its correct namespace
776        let unsealed1: TestData =
777            unseal_with_cek(&envelope1, DataEnvelopeNamespace::ExampleNamespace, &cek1).unwrap();
778        assert_eq!(unsealed1, data1);
779
780        let unsealed2: TestData =
781            unseal_with_cek(&envelope2, DataEnvelopeNamespace::ExampleNamespace2, &cek2).unwrap();
782        assert_eq!(unsealed2, data2);
783
784        // Cross-unsealing should fail
785        assert!(matches!(
786            unseal_with_cek::<TestData>(
787                &envelope1,
788                DataEnvelopeNamespace::ExampleNamespace2,
789                &cek1
790            ),
791            Err(DataEnvelopeError::InvalidNamespace)
792        ));
793        assert!(matches!(
794            unseal_with_cek::<TestData>(&envelope2, DataEnvelopeNamespace::ExampleNamespace, &cek2),
795            Err(DataEnvelopeError::InvalidNamespace)
796        ));
797    }
798}