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    /// Namespace for PAM target systems (`bitwarden-pam-sealed::TargetSystemSealed`), sealed by
512    /// the admin client so a hostile server cannot change what the access connector rotates or
513    /// how.
514    PamTargetSystem = 4,
515    /// Namespace for PAM rotation configs (`bitwarden-pam-sealed::RotationConfigSealed`), sealed
516    /// for the same reason as [`DataEnvelopeNamespace::PamTargetSystem`].
517    PamRotationConfig = 5,
518    /// This namespace is only used in tests
519    #[cfg(test)]
520    ExampleNamespace = -1,
521    /// This namespace is only used in tests
522    #[cfg(test)]
523    ExampleNamespace2 = -2,
524}
525
526impl DataEnvelopeNamespace {
527    /// Returns the numeric value of the namespace.
528    fn as_i64(&self) -> i64 {
529        *self as i64
530    }
531}
532
533impl TryFrom<i128> for DataEnvelopeNamespace {
534    type Error = DataEnvelopeError;
535
536    fn try_from(value: i128) -> Result<Self, Self::Error> {
537        match value {
538            1 => Ok(DataEnvelopeNamespace::VaultItem),
539            2 => Ok(DataEnvelopeNamespace::OrganizationInvite),
540            3 => Ok(DataEnvelopeNamespace::RegistrationOpenOrgInviteData),
541            4 => Ok(DataEnvelopeNamespace::PamTargetSystem),
542            5 => Ok(DataEnvelopeNamespace::PamRotationConfig),
543            #[cfg(test)]
544            -1 => Ok(DataEnvelopeNamespace::ExampleNamespace),
545            #[cfg(test)]
546            -2 => Ok(DataEnvelopeNamespace::ExampleNamespace2),
547            _ => Err(DataEnvelopeError::InvalidNamespace),
548        }
549    }
550}
551
552impl TryFrom<i64> for DataEnvelopeNamespace {
553    type Error = DataEnvelopeError;
554
555    fn try_from(value: i64) -> Result<Self, Self::Error> {
556        Self::try_from(i128::from(value))
557    }
558}
559
560impl From<DataEnvelopeNamespace> for i128 {
561    fn from(val: DataEnvelopeNamespace) -> Self {
562        val.as_i64().into()
563    }
564}
565
566impl ContentNamespace for DataEnvelopeNamespace {}
567
568#[cfg(test)]
569mod tests {
570    use serde::Deserialize;
571
572    use super::*;
573    use crate::{SymmetricKeyAlgorithm, safe::KeyEncryptionKey, traits::tests::TestIds};
574
575    #[derive(Serialize, Deserialize, Debug, PartialEq)]
576    struct TestDataV1 {
577        field: u32,
578    }
579    impl SealableData for TestDataV1 {}
580
581    generate_versioned_sealable!(
582        TestData,
583        DataEnvelopeNamespace::ExampleNamespace,
584        [
585            TestDataV1 => "1",
586        ]
587    );
588
589    /// Legacy XChaCha20-Poly1305 test vector, kept to prove that DataEnvelopes sealed before the
590    /// switch to AES-256-GCM still decrypt (the algorithm is recovered from the protected header).
591    const TEST_VECTOR_CEK: &str =
592        "pQEEAlB5RTKA0xXdA7C4iQE4QfVUAzoAARFvBIEEIFggQYqnsrAfeFFTaXGXB54YrksB6eQcctMpnaZ8rG6rMJ0B";
593    const TEST_VECTOR_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFB5RTKA0xXdA7C4iQE4QfVUOgABOIECOgABOIAgoQVYGLfQrYHVWxRxO6A8m/yp5DPbBIn3h8nijlhQj4jFwDLWfFz7le1Oy8dTls5vdEFg/FjjsPvXicI2bdb5KDdJCz/YkEu0kqjpQwdCcALpJLVJwgQQeKIeU2klBHEPZjnlLpRRXeCUp5c5BYQ=";
594
595    /// AES-256-GCM test vector, generated by `generate_aes_gcm_test_vectors`. Locks the current
596    /// (FIPS-compatible) DataEnvelope wire format for backward compatibility.
597    const TEST_VECTOR_AES_GCM_CEK: &str =
598        "pQEEAlDLIx+izSLk9h9sVjHFzpKoAwMEgQQgWCDWM3iwTX2/LHTIaXS0cPIKCYFZethtKyD6Pucdt4fkGQQEBAQ=";
599    const TEST_VECTOR_AES_GCM_ENVELOPE: &str = "g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUMsjH6LNIuT2H2xWMcXOkqg6AAE4gQI6AAE4gCChBUyoF/oGEm+lJYrjjgdYUGIH5LnQjqFMWo2BJORVPYH2+hEWkxIn3tRgAMHNwIr0nTXMVD1EyVGZOsHSDMPqn2HaYrDeR5s+Rg0ezZ2WLh8n2FbdC44A/ExOms4IHcyT";
600
601    /// Test helper: unseal an envelope with an AES-256-GCM content-encryption key.
602    fn unseal_with_cek<T>(
603        envelope: &DataEnvelope,
604        namespace: DataEnvelopeNamespace,
605        cek: &Aes256GcmKey,
606    ) -> Result<T, DataEnvelopeError>
607    where
608        T: serde::de::DeserializeOwned + SealableVersionedData,
609    {
610        envelope.unseal_ref(namespace, CoseKeyView::Aes256Gcm(cek))
611    }
612
613    #[test]
614    #[ignore = "Manual test to verify debug format"]
615    fn test_debug() {
616        let data: TestData = TestDataV1 { field: 42 }.into();
617        let envelope = DataEnvelope::seal_ref(
618            &data,
619            DataEnvelopeNamespace::ExampleNamespace,
620            &Aes256GcmKey::make(),
621        )
622        .unwrap();
623        println!("{:?}", envelope);
624    }
625
626    #[test]
627    #[ignore]
628    fn generate_aes_gcm_test_vectors() {
629        let data: TestData = TestDataV1 { field: 123 }.into();
630        let cek = Aes256GcmKey::make();
631        let envelope =
632            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
633        let unsealed_data: TestData =
634            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
635        assert_eq!(unsealed_data, data);
636        println!(
637            "const TEST_VECTOR_AES_GCM_CEK: &str = \"{}\";",
638            B64::from(SymmetricCryptoKey::Aes256GcmKey(cek).to_encoded())
639        );
640        println!(
641            "const TEST_VECTOR_AES_GCM_ENVELOPE: &str = \"{}\";",
642            String::from(envelope)
643        );
644    }
645
646    #[test]
647    fn test_data_envelope_legacy_xchacha20_test_vector() {
648        let cek = SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_CEK).unwrap()).unwrap();
649        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref cek) = cek else {
650            panic!("Invalid CEK type");
651        };
652
653        let envelope: DataEnvelope = TEST_VECTOR_ENVELOPE.parse().unwrap();
654        let unsealed_data: TestData = envelope
655            .unseal_ref(
656                DataEnvelopeNamespace::ExampleNamespace,
657                CoseKeyView::XChaCha20Poly1305(cek),
658            )
659            .unwrap();
660        assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
661    }
662
663    #[test]
664    fn test_data_envelope_aes_gcm_test_vector() {
665        let cek =
666            SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_AES_GCM_CEK).unwrap()).unwrap();
667        let SymmetricCryptoKey::Aes256GcmKey(ref cek) = cek else {
668            panic!("Invalid CEK type");
669        };
670
671        let envelope: DataEnvelope = TEST_VECTOR_AES_GCM_ENVELOPE.parse().unwrap();
672        let unsealed_data: TestData =
673            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, cek).unwrap();
674        assert_eq!(unsealed_data, TestDataV1 { field: 123 }.into());
675    }
676
677    #[test]
678    fn test_data_envelope_uses_aes_gcm() {
679        let data: TestData = TestDataV1 { field: 42 }.into();
680        let envelope = DataEnvelope::seal_ref(
681            &data,
682            DataEnvelopeNamespace::ExampleNamespace,
683            &Aes256GcmKey::make(),
684        )
685        .unwrap();
686
687        // New envelopes declare AES-256-GCM in their protected header.
688        let msg = coset::CoseEncrypt0::from_slice(envelope.envelope_data.as_ref()).unwrap();
689        assert_eq!(
690            msg.protected.header.alg,
691            Some(coset::Algorithm::Assigned(coset::iana::Algorithm::A256GCM))
692        );
693    }
694
695    #[test]
696    fn test_data_envelope() {
697        // Create an instance of TestData
698        let data: TestData = TestDataV1 { field: 42 }.into();
699
700        // Seal the data
701        let cek = Aes256GcmKey::make();
702        let envelope =
703            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
704        let unsealed_data: TestData =
705            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
706
707        // Verify that the unsealed data matches the original data
708        assert_eq!(unsealed_data, data);
709    }
710
711    #[test]
712    fn test_data_envelope_with_keystore_roundtrip() {
713        let data: TestData = TestDataV1 { field: 7 }.into();
714        let key_store = crate::store::KeyStore::<TestIds>::default();
715        let mut ctx = key_store.context_mut();
716
717        let (envelope, cek_id) = DataEnvelope::seal(data, &mut ctx).unwrap();
718
719        // The CEK stored in the key store is an AES-256-GCM key.
720        assert_eq!(
721            ctx.get_symmetric_key_algorithm(cek_id).unwrap(),
722            SymmetricKeyAlgorithm::Aes256Gcm
723        );
724
725        let unsealed: TestData = envelope.unseal(cek_id, &mut ctx).unwrap();
726        assert_eq!(unsealed, TestDataV1 { field: 7 }.into());
727    }
728
729    #[test]
730    fn test_data_envelope_wrapping_key_roundtrip() {
731        let data: TestData = TestDataV1 { field: 99 }.into();
732        let key_store = crate::store::KeyStore::<TestIds>::default();
733        let mut ctx = key_store.context_mut();
734
735        let wrapping_key = KeyEncryptionKey::make(&mut ctx);
736
737        let (envelope, wrapped_cek) =
738            DataEnvelope::seal_with_wrapping_key(data, &wrapping_key, &mut ctx).unwrap();
739        let unsealed: TestData = envelope
740            .unseal_with_wrapping_key(&wrapping_key, &wrapped_cek, &mut ctx)
741            .unwrap();
742        assert_eq!(unsealed, TestDataV1 { field: 99 }.into());
743    }
744
745    #[test]
746    fn test_namespace_validation_success() {
747        let data: TestData = TestDataV1 { field: 123 }.into();
748
749        // Test with ExampleNamespace
750        let cek1 = Aes256GcmKey::make();
751        let envelope1 =
752            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace, &cek1).unwrap();
753        let unsealed_data1: TestData =
754            unseal_with_cek(&envelope1, DataEnvelopeNamespace::ExampleNamespace, &cek1).unwrap();
755        assert_eq!(unsealed_data1, data);
756
757        // Test with ExampleNamespace2
758        let cek2 = Aes256GcmKey::make();
759        let envelope2 =
760            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace2, &cek2).unwrap();
761        let unsealed_data2: TestData =
762            unseal_with_cek(&envelope2, DataEnvelopeNamespace::ExampleNamespace2, &cek2).unwrap();
763        assert_eq!(unsealed_data2, data);
764    }
765
766    #[test]
767    fn test_namespace_validation_failure() {
768        let data: TestData = TestDataV1 { field: 456 }.into();
769
770        // Seal with ExampleNamespace
771        let cek = Aes256GcmKey::make();
772        let envelope =
773            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
774
775        // Try to unseal with wrong namespace - should fail
776        let result: Result<TestData, DataEnvelopeError> =
777            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace2, &cek);
778        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
779
780        // Verify correct namespace still works
781        let unsealed_data: TestData =
782            unseal_with_cek(&envelope, DataEnvelopeNamespace::ExampleNamespace, &cek).unwrap();
783        assert_eq!(unsealed_data, data);
784    }
785
786    #[test]
787    fn test_namespace_validation_with_keystore() {
788        let data: TestData = TestDataV1 { field: 789 }.into();
789        let key_store = crate::store::KeyStore::<TestIds>::default();
790        let mut ctx = key_store.context_mut();
791
792        // Seal with keystore using ExampleNamespace2
793        let cek = Aes256GcmKey::make();
794        let envelope =
795            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::ExampleNamespace2, &cek).unwrap();
796        ctx.set_symmetric_key_internal(
797            crate::traits::tests::TestSymmKey::A(0),
798            SymmetricCryptoKey::Aes256GcmKey(cek),
799        )
800        .unwrap();
801
802        // Try to unseal with wrong namespace - should fail
803        let result: Result<TestData, DataEnvelopeError> =
804            envelope.unseal(crate::traits::tests::TestSymmKey::A(0), &mut ctx);
805        assert!(matches!(result, Err(DataEnvelopeError::InvalidNamespace)));
806    }
807
808    #[test]
809    fn test_registration_open_org_invite_namespace_maps_to_expected_discriminant() {
810        // The wire discriminant is load-bearing once shipped; a regression here would silently
811        // invalidate every envelope sealed by production callers.
812        assert_eq!(
813            DataEnvelopeNamespace::try_from(3i128).unwrap(),
814            DataEnvelopeNamespace::RegistrationOpenOrgInviteData
815        );
816        assert_eq!(
817            i128::from(DataEnvelopeNamespace::RegistrationOpenOrgInviteData),
818            3
819        );
820    }
821
822    #[test]
823    fn test_registration_open_org_invite_data_rejects_cross_namespace_unseal() {
824        // AC 1.iii analogue: an envelope sealed under the new production namespace must not
825        // unseal under any other `DataEnvelopeNamespace` (here, the sibling production
826        // `VaultItem`). Mirrors `test_namespace_cross_contamination_protection` for the new
827        // variant.
828        let data: TestData = TestDataV1 { field: 42 }.into();
829
830        let cek = Aes256GcmKey::make();
831        let envelope = DataEnvelope::seal_ref(
832            &data,
833            DataEnvelopeNamespace::RegistrationOpenOrgInviteData,
834            &cek,
835        )
836        .unwrap();
837
838        assert!(matches!(
839            unseal_with_cek::<TestData>(&envelope, DataEnvelopeNamespace::VaultItem, &cek),
840            Err(DataEnvelopeError::InvalidNamespace)
841        ));
842
843        // Correct namespace still succeeds.
844        let round_tripped: TestData = unseal_with_cek(
845            &envelope,
846            DataEnvelopeNamespace::RegistrationOpenOrgInviteData,
847            &cek,
848        )
849        .unwrap();
850        assert_eq!(round_tripped, data);
851    }
852
853    #[test]
854    fn test_pam_namespaces_map_to_expected_discriminants() {
855        // The wire discriminants are load-bearing once shipped; a regression here would silently
856        // invalidate every envelope sealed by production callers (bitwarden-pam-sealed).
857        assert_eq!(
858            DataEnvelopeNamespace::try_from(4i128).unwrap(),
859            DataEnvelopeNamespace::PamTargetSystem
860        );
861        assert_eq!(i128::from(DataEnvelopeNamespace::PamTargetSystem), 4);
862
863        assert_eq!(
864            DataEnvelopeNamespace::try_from(5i128).unwrap(),
865            DataEnvelopeNamespace::PamRotationConfig
866        );
867        assert_eq!(i128::from(DataEnvelopeNamespace::PamRotationConfig), 5);
868    }
869
870    #[test]
871    fn test_pam_target_system_rejects_cross_namespace_unseal() {
872        // A target-system blob must not unseal under the sibling PamRotationConfig namespace (or
873        // vice versa).
874        let data: TestData = TestDataV1 { field: 42 }.into();
875
876        let cek = Aes256GcmKey::make();
877        let envelope =
878            DataEnvelope::seal_ref(&data, DataEnvelopeNamespace::PamTargetSystem, &cek).unwrap();
879
880        assert!(matches!(
881            unseal_with_cek::<TestData>(&envelope, DataEnvelopeNamespace::PamRotationConfig, &cek),
882            Err(DataEnvelopeError::InvalidNamespace)
883        ));
884
885        let round_tripped: TestData =
886            unseal_with_cek(&envelope, DataEnvelopeNamespace::PamTargetSystem, &cek).unwrap();
887        assert_eq!(round_tripped, data);
888    }
889
890    #[test]
891    fn test_namespace_cross_contamination_protection() {
892        let data1: TestData = TestDataV1 { field: 111 }.into();
893        let data2: TestData = TestDataV1 { field: 222 }.into();
894
895        // Seal two different pieces of data with different namespaces
896        let cek1 = Aes256GcmKey::make();
897        let envelope1 =
898            DataEnvelope::seal_ref(&data1, DataEnvelopeNamespace::ExampleNamespace, &cek1).unwrap();
899        let cek2 = Aes256GcmKey::make();
900        let envelope2 =
901            DataEnvelope::seal_ref(&data2, DataEnvelopeNamespace::ExampleNamespace2, &cek2)
902                .unwrap();
903
904        // Verify each envelope only opens with its correct namespace
905        let unsealed1: TestData =
906            unseal_with_cek(&envelope1, DataEnvelopeNamespace::ExampleNamespace, &cek1).unwrap();
907        assert_eq!(unsealed1, data1);
908
909        let unsealed2: TestData =
910            unseal_with_cek(&envelope2, DataEnvelopeNamespace::ExampleNamespace2, &cek2).unwrap();
911        assert_eq!(unsealed2, data2);
912
913        // Cross-unsealing should fail
914        assert!(matches!(
915            unseal_with_cek::<TestData>(
916                &envelope1,
917                DataEnvelopeNamespace::ExampleNamespace2,
918                &cek1
919            ),
920            Err(DataEnvelopeError::InvalidNamespace)
921        ));
922        assert!(matches!(
923            unseal_with_cek::<TestData>(&envelope2, DataEnvelopeNamespace::ExampleNamespace, &cek2),
924            Err(DataEnvelopeError::InvalidNamespace)
925        ));
926    }
927}