Skip to main content

bitwarden_vault/cipher/blob/
sealed.rs

1use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
2use bitwarden_crypto::{
3    EncString, KeyStoreContext,
4    safe::{DataEnvelope, DataEnvelopeError},
5};
6use bitwarden_encoding::B64;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use super::CipherBlob;
11
12const FORMAT_VERSION: u8 = 1;
13
14/// Error type for `SealedCipherBlob` operations.
15#[allow(dead_code)]
16#[derive(Debug, Error)]
17pub(super) enum SealedCipherBlobError {
18    #[error("Unsupported format version: {0}")]
19    UnsupportedFormatVersion(u8),
20    #[error("CBOR encoding error")]
21    CborEncodingError,
22    #[error("CBOR decoding error")]
23    CborDecodingError,
24    #[error("Base64 decoding error")]
25    Base64DecodingError,
26    #[error(transparent)]
27    DataEnvelope(#[from] DataEnvelopeError),
28}
29
30/// Sealed container that packages a wrapped CEK and encrypted `DataEnvelope` together.
31///
32/// Serializable into the `Cipher.data: Option<String>` field.
33#[allow(dead_code)]
34#[derive(Serialize, Deserialize, Debug, Clone)]
35pub(super) struct SealedCipherBlob {
36    format_version: u8,
37    wrapped_cek: EncString,
38    envelope: DataEnvelope,
39}
40
41#[allow(dead_code)]
42impl SealedCipherBlob {
43    /// Seals a `CipherBlob` into a `SealedCipherBlob` by encrypting it with a new CEK
44    /// wrapped by the provided wrapping key.
45    pub(super) fn seal(
46        data: CipherBlob,
47        wrapping_key: &SymmetricKeySlotId,
48        ctx: &mut KeyStoreContext<KeySlotIds>,
49    ) -> Result<Self, SealedCipherBlobError> {
50        let (envelope, wrapped_cek) =
51            DataEnvelope::seal_with_wrapping_key(data, wrapping_key, ctx)?;
52        Ok(Self {
53            format_version: FORMAT_VERSION,
54            wrapped_cek,
55            envelope,
56        })
57    }
58
59    /// Unseals the `CipherBlob` from this container using the provided wrapping key.
60    pub(super) fn unseal(
61        &self,
62        wrapping_key: &SymmetricKeySlotId,
63        ctx: &mut KeyStoreContext<KeySlotIds>,
64    ) -> Result<CipherBlob, SealedCipherBlobError> {
65        if self.format_version != FORMAT_VERSION {
66            return Err(SealedCipherBlobError::UnsupportedFormatVersion(
67                self.format_version,
68            ));
69        }
70        Ok(self
71            .envelope
72            .unseal_with_wrapping_key(wrapping_key, &self.wrapped_cek, ctx)?)
73    }
74
75    /// Serializes this container into an opaque base64-encoded CBOR string.
76    pub(super) fn to_opaque_string(&self) -> Result<String, SealedCipherBlobError> {
77        let mut buf = Vec::new();
78        ciborium::ser::into_writer(self, &mut buf)
79            .map_err(|_| SealedCipherBlobError::CborEncodingError)?;
80        Ok(B64::from(buf).to_string())
81    }
82
83    /// Deserializes a `SealedCipherBlob` from an opaque base64-encoded CBOR string.
84    pub(super) fn from_opaque_string(s: &str) -> Result<Self, SealedCipherBlobError> {
85        let bytes = B64::try_from(s)
86            .map_err(|_| SealedCipherBlobError::Base64DecodingError)?
87            .into_bytes();
88        ciborium::de::from_reader(bytes.as_slice())
89            .map_err(|_| SealedCipherBlobError::CborDecodingError)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use bitwarden_core::key_management::KeySlotIds;
96    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
97    use bitwarden_encoding::B64;
98
99    use super::*;
100    use crate::cipher::{blob::v1::*, secure_note::SecureNoteType};
101
102    fn test_cipher_blob() -> CipherBlob {
103        CipherBlobV1 {
104            name: "Test Cipher".to_string(),
105            notes: Some("Some notes".to_string()),
106            type_data: CipherTypeDataV1::SecureNote(SecureNoteDataV1 {
107                r#type: SecureNoteType::Generic,
108            }),
109            fields: Vec::new(),
110            password_history: Vec::new(),
111        }
112        .into()
113    }
114
115    #[test]
116    fn test_seal_unseal_round_trip() {
117        let store: KeyStore<KeySlotIds> = KeyStore::default();
118        let mut ctx = store.context_mut();
119        let wrapping_key = ctx.generate_symmetric_key();
120
121        let sealed = SealedCipherBlob::seal(test_cipher_blob(), &wrapping_key, &mut ctx).unwrap();
122        let unsealed = sealed.unseal(&wrapping_key, &mut ctx).unwrap();
123
124        assert_eq!(test_cipher_blob(), unsealed);
125    }
126
127    #[test]
128    fn test_opaque_string_round_trip() {
129        let store: KeyStore<KeySlotIds> = KeyStore::default();
130        let mut ctx = store.context_mut();
131        let wrapping_key = ctx.generate_symmetric_key();
132
133        let sealed = SealedCipherBlob::seal(test_cipher_blob(), &wrapping_key, &mut ctx).unwrap();
134
135        let opaque = sealed.to_opaque_string().unwrap();
136        let restored = SealedCipherBlob::from_opaque_string(&opaque).unwrap();
137        let unsealed = restored.unseal(&wrapping_key, &mut ctx).unwrap();
138
139        assert_eq!(test_cipher_blob(), unsealed);
140    }
141
142    #[test]
143    fn test_unsupported_format_version() {
144        let store: KeyStore<KeySlotIds> = KeyStore::default();
145        let mut ctx = store.context_mut();
146        let wrapping_key = ctx.generate_symmetric_key();
147
148        let blob = test_cipher_blob();
149        let mut sealed = SealedCipherBlob::seal(blob, &wrapping_key, &mut ctx).unwrap();
150        sealed.format_version = 99;
151
152        let result = sealed.unseal(&wrapping_key, &mut ctx);
153        assert!(matches!(
154            result,
155            Err(SealedCipherBlobError::UnsupportedFormatVersion(99))
156        ));
157    }
158
159    #[test]
160    fn test_invalid_base64() {
161        let result = SealedCipherBlob::from_opaque_string("not valid base64!@#$");
162        assert!(matches!(
163            result,
164            Err(SealedCipherBlobError::Base64DecodingError)
165        ));
166    }
167
168    #[test]
169    fn test_invalid_cbor() {
170        let not_cbor = B64::from(b"this is not valid cbor data".as_slice()).to_string();
171        let result = SealedCipherBlob::from_opaque_string(&not_cbor);
172        assert!(matches!(
173            result,
174            Err(SealedCipherBlobError::CborDecodingError)
175        ));
176    }
177
178    #[test]
179    #[ignore]
180    fn generate_sealed_test_vector() {
181        let store: KeyStore<KeySlotIds> = KeyStore::default();
182        let mut ctx = store.context_mut();
183        let wrapping_key = ctx.generate_symmetric_key();
184
185        let sealed = SealedCipherBlob::seal(test_cipher_blob(), &wrapping_key, &mut ctx).unwrap();
186        let opaque = sealed.to_opaque_string().unwrap();
187
188        #[allow(deprecated)]
189        let key = ctx.dangerous_get_symmetric_key(wrapping_key).unwrap();
190        println!(
191            "const TEST_VECTOR_WRAPPING_KEY: &str = \"{}\";",
192            key.to_base64()
193        );
194        println!("const TEST_VECTOR_SEALED_BLOB: &str = \"{}\";", opaque);
195    }
196
197    const TEST_VECTOR_WRAPPING_KEY: &str =
198        "e0MSZ4/Z4AS7fzjxMos7MXibNALU4mDJQwmge+uVwahg9P25cuaNiSpLvYMk2BgJfntbQs4FszcnY5nPe2FpVA==";
199    const TEST_VECTOR_SEALED_BLOB: &str = "o25mb3JtYXRfdmVyc2lvbgFrd3JhcHBlZF9jZWt4tDIub1dJMUloMDVleWxpeGxCQUM4V253QT09fDdOTVFiU3JXS3ZOWFNoTkNHdmZZWld0T2doMEcvZ294YXdod01UWm5PR1hLeVZ6RXA1WWRXRUhoRnQ0UFVrbVVOT204Z2JMRlhyTFN4MW5CU25PdjlEeEJLNFp6ejNJVFp3dm92Z3NBTFQwPXxBMzhlZkFhSlhmMnk2aFdxTHBUanJ6NlF5OS9FRERMWnpJOWZFSGhtVExJPWhlbnZlbG9wZXkBKGcxaExwUUU2QUFFUmJ3TjRJMkZ3Y0d4cFkyRjBhVzl1TDNndVltbDBkMkZ5WkdWdUxtTmliM0l0Y0dGa1pHVmtCRkFQV0dnR1lPblBYVGlNY2NUOVVrVUFPZ0FCT0lFQ09nQUJPSUFCb1FWWUdDeWk1cEtQSHQ2NXAwU0MxR1FGMTZ1TE85SEtUODFmZWxoeFF2UDBrTlYyQXpibks5RXlSUjlSRUUvUURYK0JVcE53bkxjUTZKZldJb2cycHp4TjBBNUlKTmhmZ1Uzd0NMSS9WOVZHcThkM1RZanBLSm9MNitKSVhVQnI0UWtHeGgzekZmci8rQThGN3RwR2dSK0tnLzVQRGJLMk9ENjdkM0ZnOW12b2t2UVBzQ0F5MnlIaVJ6aHdONUU9";
200
201    #[test]
202    fn test_recorded_sealed_blob_test_vector() {
203        let wrapping_key =
204            SymmetricCryptoKey::try_from(B64::try_from(TEST_VECTOR_WRAPPING_KEY).unwrap()).unwrap();
205
206        let store: KeyStore<KeySlotIds> = KeyStore::default();
207        let mut ctx = store.context_mut();
208        let wrapping_key_id = ctx.add_local_symmetric_key(wrapping_key);
209
210        let sealed = SealedCipherBlob::from_opaque_string(TEST_VECTOR_SEALED_BLOB).expect(
211            "SealedCipherBlob container format has changed in a backwards-incompatible way. \
212             Existing sealed data must remain deserializable.",
213        );
214        let unsealed = sealed.unseal(&wrapping_key_id, &mut ctx).expect(
215            "SealedCipherBlob container format has changed in a backwards-incompatible way. \
216             Existing sealed data must remain deserializable.",
217        );
218
219        assert_eq!(test_cipher_blob(), unsealed);
220    }
221}