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