bitwarden_crypto/safe/
data_envelope.rs

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
23/// Marker trait for data that can be sealed in a `DataEnvelope`.
24///
25/// Do not manually implement this! Use the generate_versioned_sealable! macro instead.
26pub trait SealableVersionedData: Serialize + DeserializeOwned {
27    /// The namespace to use when sealing this type of data. This must be unique per struct.
28    const NAMESPACE: DataEnvelopeNamespace;
29}
30
31/// Marker trait for data that can be sealed in a `DataEnvelope`.
32///
33/// Note: If you implement this trait, you agree to the following:
34/// The struct serialization format is stable. Struct modifications must maintain backward
35/// compatibility with existing serialized data. Changes that break deserialization are considered
36/// breaking changes and require a new version and struct.
37///
38/// Ideally, when creating a new struct, create a test vector (a sealed DataEnvelope for a test
39/// value), and create a unit test ensuring that it permanently deserializes correctly.
40///
41/// To make breaking changes, introduce a new version. This should use the
42/// `generate_versioned_sealable!` macro to auto-generate the versioning code. Please see the
43/// examples directory.
44pub trait SealableData: Serialize + DeserializeOwned {}
45
46/// `DataEnvelope` allows sealing structs entire structs to encrypted blobs.
47///
48/// Sealing a struct results in an encrypted blob, and a content-encryption-key. The
49/// content-encryption-key must be provided again when unsealing the data. A content encryption key
50/// allows easy key-rotation of the encrypting-key, as now just the content-encryption-keys need to
51/// be re-uploaded, instead of all data.
52///
53/// The content-encryption-key cannot be re-used for encrypting other data.
54///
55/// Note: This is explicitly meant for structured data, not large binary blobs (files).
56#[derive(Clone)]
57pub struct DataEnvelope {
58    envelope_data: CoseEncrypt0Bytes,
59}
60
61impl DataEnvelope {
62    /// Seals a struct into an encrypted blob, and stores the content-encryption-key in the provided
63    /// context.
64    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    /// Seals a struct into an encrypted blob. The content encryption key is wrapped with the
79    /// provided wrapping key
80    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    /// Seals a struct into an encrypted blob, and returns the encrypted blob and the
98    /// content-encryption-key.
99    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        // Serialize the message
109        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        // Build the COSE headers
119        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        // Encrypt the message
130        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        // Serialize the COSE message
143        let envelope_data = encrypt0
144            .to_vec()
145            .map(CoseEncrypt0Bytes::from)
146            .map_err(|_| DataEnvelopeError::EncodingError)?;
147
148        // Disable key operations other than decrypt on the CEK
149        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    /// Unseals the data from the encrypted blob using a content-encryption-key stored in the
157    /// context.
158    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    /// Unseals the data from the encrypted blob and wrapped content-encryption-key.
177    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    /// Unseals the data from the encrypted blob using the provided content-encryption-key.
193    fn unseal_ref<T>(
194        &self,
195        namespace: &DataEnvelopeNamespace,
196        cek: &XChaCha20Poly1305Key,
197    ) -> Result<T, DataEnvelopeError>
198    where
199        T: DeserializeOwned + SealableVersionedData,
200    {
201        // Parse the COSE message
202        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        // Validate the message
209        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        // Decrypt the message
226        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        // Deserialize the message
248        let serialized_message =
249            SerializedMessage::from_bytes(unpadded_message, CoapContentFormat::Cbor);
250        serialized_message
251            .decode()
252            .map_err(|_| DataEnvelopeError::DecodingError)
253    }
254}
255
256/// Helper function to extract the namespace from a `ProtectedHeader`. The namespace is stored
257/// as a custom header value using the DATA_ENVELOPE_NAMESPACE label.
258fn 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
283/// Helper function to extract the content type from a `ProtectedHeader`. The content type is a
284/// standardized header set on the protected headers of the signature object. Currently we only
285/// support registered values, but PrivateUse values are also allowed in the COSE specification.
286pub(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/// Error type for `DataEnvelope` operations.
363#[derive(Debug, Error)]
364pub enum DataEnvelopeError {
365    /// Indicates that the content format is not supported.
366    #[error("Unsupported content format")]
367    UnsupportedContentFormat,
368    /// Indicates that there was an error during decoding of the message.
369    #[error("Failed to decode COSE message")]
370    CoseDecodingError,
371    /// Indicates that there was an error during decoding of the message.
372    #[error("Failed to decode the content of the envelope")]
373    DecodingError,
374    /// Indicates that there was an error during encoding of the message.
375    #[error("Encoding error")]
376    EncodingError,
377    /// Indicates that there was an error with the key store.
378    #[error("KeyStore error")]
379    KeyStoreError,
380    /// Indicates that there was an error during decryption.
381    #[error("Decryption error")]
382    DecryptionError,
383    /// Indicates that there was an error during encryption.
384    #[error("Encryption error")]
385    EncryptionError,
386    /// Indicates that there was an error parsing the DataEnvelope.
387    #[error("Parsing error: {0}")]
388    ParsingError(String),
389    /// Indicates that the data envelope namespace is invalid.
390    #[error("Invalid namespace")]
391    InvalidNamespace,
392    /// Indicates that the wrong key was used for decryption.
393    #[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/// Generates a versioned enum that implements `SealableData`.
434///
435/// This serializes to an adjacently tagged enum, with the "version" field being set to the provided
436/// version, and the "content" field being the serialized struct.
437///
438///
439/// ```
440/// use bitwarden_crypto::{safe::{DataEnvelopeNamespace, SealableData, SealableVersionedData}, generate_versioned_sealable};
441/// use serde::{Deserialize, Serialize};
442///
443/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
444/// struct MyItemV1 {
445///     a: u32,
446///     b: String,
447/// }
448/// impl SealableData for MyItemV1 {}
449///
450/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
451/// struct MyItemV2 {
452///     a: u32,
453///     b: bool,
454///     c: bool,
455/// }
456/// impl SealableData for MyItemV2 {}
457///
458/// generate_versioned_sealable!(
459///     MyItem,
460///     DataEnvelopeNamespace::VaultItem,
461///     [
462///         MyItemV1 => "1",
463///         MyItemV2 => "2",
464///     ]
465/// );
466/// ```
467#[macro_export]
468macro_rules! generate_versioned_sealable {
469    (
470        // Provide the name
471        $enum_name:ident,
472        // Provide the namespace
473        $namespace:path,
474        // Provide mappings from the variant to version. This must not be changed later.
475        [ $( $variant_ty:ident => $rename:literal ),+ $(,)? ]
476    ) => {
477        // Implement the enum
478        #[derive(Serialize, Deserialize, Debug, PartialEq)]
479        #[serde(tag = "version", content = "content")]
480        enum $enum_name {
481            $(
482                #[serde(rename = $rename)]
483                // Strip the `MyItem` prefix from type name if you want shorter variant names
484                $variant_ty($variant_ty),
485            )+
486        }
487
488        // Implement the SealableVersionedData trait for the enum
489        impl SealableVersionedData for $enum_name
490        where
491            $( $variant_ty: SealableData ),+
492        {
493            // Implement with the specified namespace
494            const NAMESPACE: DataEnvelopeNamespace = $namespace;
495        }
496
497        // Implement Into from each variant to the enum
498        $(
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        // Create an instance of TestData
568        let data: TestData = TestDataV1 { field: 42 }.into();
569
570        // Seal the data
571        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        // Verify that the unsealed data matches the original data
578        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        // Test with ExampleNamespace
586        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        // Test with ExampleNamespace2
594        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        // Seal with ExampleNamespace
607        let (envelope, cek) =
608            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
609
610        // Try to unseal with wrong namespace - should fail
611        let result: Result<TestData, DataEnvelopeError> =
612            envelope.unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek);
613        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
614
615        // Verify correct namespace still works
616        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        // Seal with keystore using ExampleNamespace2
629        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        // Try to unseal with wrong namespace - should fail
638        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        // Seal two different pieces of data with different namespaces
649        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        // Verify each envelope only opens with its correct namespace
655        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        // Cross-unsealing should fail
666        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}