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