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