bitwarden_vault/cipher/blob/
sealed.rs1use 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#[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#[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 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 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 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 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(¬_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}