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