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(&[], |data, aad| {
228                let nonce = msg.unprotected.iv.as_slice();
229                crate::xchacha20::decrypt_xchacha20_poly1305(
230                    nonce
231                        .try_into()
232                        .map_err(|_| CryptoError::InvalidNonceLength)?,
233                    &(*cek.enc_key).into(),
234                    data,
235                    aad,
236                )
237            })
238            .map_err(|_| DataEnvelopeError::DecryptionError)?;
239
240        let unpadded_message =
241            unpad_cbor(&decrypted_message).map_err(|_| DataEnvelopeError::DecryptionError)?;
242
243        // Deserialize the message
244        let serialized_message =
245            SerializedMessage::from_bytes(unpadded_message, CoapContentFormat::Cbor);
246        serialized_message
247            .decode()
248            .map_err(|_| DataEnvelopeError::DecodingError)
249    }
250}
251
252/// Helper function to extract the namespace from a `ProtectedHeader`. The namespace is stored
253/// as a custom header value using the DATA_ENVELOPE_NAMESPACE label.
254fn extract_namespace(header: &coset::Header) -> Result<DataEnvelopeNamespace, DataEnvelopeError> {
255    let namespace_value = header
256        .rest
257        .iter()
258        .find(|(label, _)| {
259            if let coset::Label::Int(label_int) = label {
260                *label_int == DATA_ENVELOPE_NAMESPACE
261            } else {
262                false
263            }
264        })
265        .map(|(_, value)| value)
266        .ok_or(DataEnvelopeError::InvalidNamespace)?;
267
268    let namespace_int = match namespace_value {
269        ciborium::Value::Integer(int) => {
270            let int_val: i128 = (*int).into();
271            int_val
272        }
273        _ => return Err(DataEnvelopeError::InvalidNamespace),
274    };
275
276    DataEnvelopeNamespace::try_from(namespace_int).map_err(|_| DataEnvelopeError::InvalidNamespace)
277}
278
279/// Helper function to extract the content type from a `ProtectedHeader`. The content type is a
280/// standardized header set on the protected headers of the signature object. Currently we only
281/// support registered values, but PrivateUse values are also allowed in the COSE specification.
282pub(super) fn content_format(protected_header: &ProtectedHeader) -> Result<String, EncodingError> {
283    protected_header
284        .header
285        .content_type
286        .as_ref()
287        .and_then(|ct| match ct {
288            RegisteredLabel::Text(content_format) => Some(content_format.clone()),
289            _ => None,
290        })
291        .ok_or(EncodingError::InvalidCoseEncoding)
292}
293
294impl From<&DataEnvelope> for Vec<u8> {
295    fn from(val: &DataEnvelope) -> Self {
296        val.envelope_data.to_vec()
297    }
298}
299
300impl From<Vec<u8>> for DataEnvelope {
301    fn from(data: Vec<u8>) -> Self {
302        DataEnvelope {
303            envelope_data: CoseEncrypt0Bytes::from(data),
304        }
305    }
306}
307
308impl std::fmt::Debug for DataEnvelope {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        f.debug_struct("DataEnvelope")
311            .field("envelope_data", &self.envelope_data)
312            .finish()
313    }
314}
315
316impl FromStr for DataEnvelope {
317    type Err = NotB64EncodedError;
318
319    fn from_str(s: &str) -> Result<Self, Self::Err> {
320        let data = B64::try_from(s)?;
321        Ok(Self::from(data.into_bytes()))
322    }
323}
324
325impl From<DataEnvelope> for String {
326    fn from(val: DataEnvelope) -> Self {
327        let serialized: Vec<u8> = (&val).into();
328        B64::from(serialized).to_string()
329    }
330}
331
332impl<'de> Deserialize<'de> for DataEnvelope {
333    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
334    where
335        D: serde::Deserializer<'de>,
336    {
337        deserializer.deserialize_str(FromStrVisitor::new())
338    }
339}
340
341impl Serialize for DataEnvelope {
342    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
343    where
344        S: serde::Serializer,
345    {
346        let serialized: Vec<u8> = self.into();
347        serializer.serialize_str(&B64::from(serialized).to_string())
348    }
349}
350
351impl std::fmt::Display for DataEnvelope {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        let serialized: Vec<u8> = self.into();
354        write!(f, "{}", B64::from(serialized))
355    }
356}
357
358/// Error type for `DataEnvelope` operations.
359#[derive(Debug, Error)]
360pub enum DataEnvelopeError {
361    /// Indicates that the content format is not supported.
362    #[error("Unsupported content format")]
363    UnsupportedContentFormat,
364    /// Indicates that there was an error during decoding of the message.
365    #[error("Failed to decode COSE message")]
366    CoseDecodingError,
367    /// Indicates that there was an error during decoding of the message.
368    #[error("Failed to decode the content of the envelope")]
369    DecodingError,
370    /// Indicates that there was an error during encoding of the message.
371    #[error("Encoding error")]
372    EncodingError,
373    /// Indicates that there was an error with the key store.
374    #[error("KeyStore error")]
375    KeyStoreError,
376    /// Indicates that there was an error during decryption.
377    #[error("Decryption error")]
378    DecryptionError,
379    /// Indicates that there was an error during encryption.
380    #[error("Encryption error")]
381    EncryptionError,
382    /// Indicates that there was an error parsing the DataEnvelope.
383    #[error("Parsing error: {0}")]
384    ParsingError(String),
385    /// Indicates that the data envelope namespace is invalid.
386    #[error("Invalid namespace")]
387    InvalidNamespace,
388    /// Indicates that the wrong key was used for decryption.
389    #[error("Wrong key used for decryption")]
390    WrongKey,
391}
392
393#[cfg(feature = "wasm")]
394#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
395const TS_CUSTOM_TYPES: &'static str = r#"
396export type DataEnvelope = Tagged<string, "DataEnvelope">;
397"#;
398
399#[cfg(feature = "wasm")]
400impl wasm_bindgen::describe::WasmDescribe for DataEnvelope {
401    fn describe() {
402        <String as wasm_bindgen::describe::WasmDescribe>::describe();
403    }
404}
405
406#[cfg(feature = "wasm")]
407impl FromWasmAbi for DataEnvelope {
408    type Abi = <String as FromWasmAbi>::Abi;
409
410    unsafe fn from_abi(abi: Self::Abi) -> Self {
411        use wasm_bindgen::UnwrapThrowExt;
412
413        let s = unsafe { String::from_abi(abi) };
414        Self::from_str(&s).unwrap_throw()
415    }
416}
417
418fn pad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
419    let mut data = data.to_vec();
420    pad_bytes(&mut data, DATA_ENVELOPE_PADDING_SIZE).map_err(|_| CryptoError::InvalidPadding)?;
421    Ok(data)
422}
423
424fn unpad_cbor(data: &[u8]) -> Result<Vec<u8>, CryptoError> {
425    let unpadded = crate::utils::unpad_bytes(data).map_err(|_| CryptoError::InvalidPadding)?;
426    Ok(unpadded.to_vec())
427}
428
429/// Generates a versioned enum that implements `SealableData`.
430///
431/// This serializes to an adjacently tagged enum, with the "version" field being set to the provided
432/// version, and the "content" field being the serialized struct.
433///
434///
435/// ```
436/// use bitwarden_crypto::{safe::{DataEnvelopeNamespace, SealableData, SealableVersionedData}, generate_versioned_sealable};
437/// use serde::{Deserialize, Serialize};
438///
439/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
440/// struct MyItemV1 {
441///     a: u32,
442///     b: String,
443/// }
444/// impl SealableData for MyItemV1 {}
445///
446/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
447/// struct MyItemV2 {
448///     a: u32,
449///     b: bool,
450///     c: bool,
451/// }
452/// impl SealableData for MyItemV2 {}
453///
454/// generate_versioned_sealable!(
455///     MyItem,
456///     DataEnvelopeNamespace::VaultItem,
457///     [
458///         MyItemV1 => "1",
459///         MyItemV2 => "2",
460///     ]
461/// );
462/// ```
463#[macro_export]
464macro_rules! generate_versioned_sealable {
465    (
466        // Provide the name
467        $enum_name:ident,
468        // Provide the namespace
469        $namespace:path,
470        // Provide mappings from the variant to version. This must not be changed later.
471        [ $( $variant_ty:ident => $rename:literal ),+ $(,)? ]
472    ) => {
473        // Implement the enum
474        #[derive(Serialize, Deserialize, Debug, PartialEq)]
475        #[serde(tag = "version", content = "content")]
476        enum $enum_name {
477            $(
478                #[serde(rename = $rename)]
479                // Strip the `MyItem` prefix from type name if you want shorter variant names
480                $variant_ty($variant_ty),
481            )+
482        }
483
484        // Implement the SealableVersionedData trait for the enum
485        impl SealableVersionedData for $enum_name
486        where
487            $( $variant_ty: SealableData ),+
488        {
489            // Implement with the specified namespace
490            const NAMESPACE: DataEnvelopeNamespace = $namespace;
491        }
492
493        // Implement Into from each variant to the enum
494        $(
495            impl From<$variant_ty> for $enum_name {
496                fn from(value: $variant_ty) -> Self {
497                    Self::$variant_ty(value)
498                }
499            }
500        )+
501    };
502}
503
504#[cfg(test)]
505mod tests {
506    use serde::Deserialize;
507
508    use super::*;
509    use crate::traits::tests::TestIds;
510
511    #[derive(Serialize, Deserialize, Debug, PartialEq)]
512    struct TestDataV1 {
513        field: u32,
514    }
515    impl SealableData for TestDataV1 {}
516
517    generate_versioned_sealable!(
518        TestData,
519        DataEnvelopeNamespace::ExampleNamespace,
520        [
521            TestDataV1 => "1",
522        ]
523    );
524
525    const TEST_VECTOR_CEK: &str =
526        "pQEEAlAiZII8tW5Lu9YH2bND5qx4AzoAARFvBIEEIFggnlL+dg+plLs+YqbUS00NYjwvir9E7O5pTJgX/O++XuQB";
527    const TEST_VECTOR_ENVELOPE: &str = "g1hFpAE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFAiZII8tW5Lu9YH2bND5qx4OgABOIAgoQVYGDjsL+Q0npomBf7fVsefBkXNJT/OkMncuVhQ8VSz8YWHIRylVilXRDrQp3LRSnDHQKIU4F0A49yi8W2tmRATUcPkU87eI9xbRvxjdUY/X4wL26MoFsqbWxyMJHcj8svQWwL3Jq3OvK9VS6A=";
528
529    #[test]
530    #[ignore]
531    fn generate_test_vectors() {
532        let data: TestData = TestDataV1 { field: 123 }.into();
533        let (envelope, cek) =
534            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
535        let unsealed_data: TestData = envelope
536            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
537            .unwrap();
538        assert_eq!(unsealed_data, data);
539        println!(
540            "CEK: {}",
541            B64::from(SymmetricCryptoKey::XChaCha20Poly1305Key(cek).to_encoded())
542        );
543        println!("Envelope: {}", String::from(envelope));
544    }
545
546    #[test]
547    fn test_data_envelope_test_vector() {
548        let cek = SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_CEK).unwrap()).unwrap();
549        let cek = match cek {
550            SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) => key.clone(),
551            _ => panic!("Invalid CEK type"),
552        };
553
554        let envelope: DataEnvelope = TEST_VECTOR_ENVELOPE.parse().unwrap();
555        let unsealed_data: TestData = envelope
556            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
557            .unwrap();
558        assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
559    }
560
561    #[test]
562    fn test_data_envelope() {
563        // Create an instance of TestData
564        let data: TestData = TestDataV1 { field: 42 }.into();
565
566        // Seal the data
567        let (envelope, cek) =
568            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
569        let unsealed_data: TestData = envelope
570            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
571            .unwrap();
572
573        // Verify that the unsealed data matches the original data
574        assert_eq!(unsealed_data, data);
575    }
576
577    #[test]
578    fn test_namespace_validation_success() {
579        let data: TestData = TestDataV1 { field: 123 }.into();
580
581        // Test with ExampleNamespace
582        let (envelope1, cek1) =
583            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
584        let unsealed_data1: TestData = envelope1
585            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek1)
586            .unwrap();
587        assert_eq!(unsealed_data1, data);
588
589        // Test with ExampleNamespace2
590        let (envelope2, cek2) =
591            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
592        let unsealed_data2: TestData = envelope2
593            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek2)
594            .unwrap();
595        assert_eq!(unsealed_data2, data);
596    }
597
598    #[test]
599    fn test_namespace_validation_failure() {
600        let data: TestData = TestDataV1 { field: 456 }.into();
601
602        // Seal with ExampleNamespace
603        let (envelope, cek) =
604            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
605
606        // Try to unseal with wrong namespace - should fail
607        let result: Result<TestData, DataEnvelopeError> =
608            envelope.unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek);
609        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
610
611        // Verify correct namespace still works
612        let unsealed_data: TestData = envelope
613            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek)
614            .unwrap();
615        assert_eq!(unsealed_data, data);
616    }
617
618    #[test]
619    fn test_namespace_validation_with_keystore() {
620        let data: TestData = TestDataV1 { field: 789 }.into();
621        let key_store = crate::store::KeyStore::<TestIds>::default();
622        let mut ctx = key_store.context_mut();
623
624        // Seal with keystore using ExampleNamespace2
625        let (envelope, cek) =
626            DataEnvelope::seal_ref(&data, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
627        ctx.set_symmetric_key_internal(
628            crate::traits::tests::TestSymmKey::A(0),
629            SymmetricCryptoKey::XChaCha20Poly1305Key(cek),
630        )
631        .unwrap();
632
633        // Try to unseal with wrong namespace - should fail
634        let result: Result<TestData, DataEnvelopeError> =
635            envelope.unseal(crate::traits::tests::TestSymmKey::A(0), &mut ctx);
636        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
637    }
638
639    #[test]
640    fn test_namespace_cross_contamination_protection() {
641        let data1: TestData = TestDataV1 { field: 111 }.into();
642        let data2: TestData = TestDataV1 { field: 222 }.into();
643
644        // Seal two different pieces of data with different namespaces
645        let (envelope1, cek1) =
646            DataEnvelope::seal_ref(&data1, &DataEnvelopeNamespace::ExampleNamespace).unwrap();
647        let (envelope2, cek2) =
648            DataEnvelope::seal_ref(&data2, &DataEnvelopeNamespace::ExampleNamespace2).unwrap();
649
650        // Verify each envelope only opens with its correct namespace
651        let unsealed1: TestData = envelope1
652            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace, &cek1)
653            .unwrap();
654        assert_eq!(unsealed1, data1);
655
656        let unsealed2: TestData = envelope2
657            .unseal_ref(&DataEnvelopeNamespace::ExampleNamespace2, &cek2)
658            .unwrap();
659        assert_eq!(unsealed2, data2);
660
661        // Cross-unsealing should fail
662        assert!(matches!(
663            envelope1.unseal_ref::<TestData>(&DataEnvelopeNamespace::ExampleNamespace2, &cek1),
664            Err(DataEnvelopeError::InvalidNamespace)
665        ));
666        assert!(matches!(
667            envelope2.unseal_ref::<TestData>(&DataEnvelopeNamespace::ExampleNamespace, &cek2),
668            Err(DataEnvelopeError::InvalidNamespace)
669        ));
670    }
671}