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#[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#[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 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 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 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 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(¬_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}