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 bitwarden_logging::instrument;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use super::CipherBlob;
12
13const FORMAT_VERSION: u8 = 1;
14
15#[derive(Debug, Error)]
17pub enum SealedCipherBlobError {
18 #[error("Unsupported format version: {0}")]
20 UnsupportedFormatVersion(u8),
21 #[error("CBOR encoding error")]
23 CborEncoding,
24 #[error("CBOR decoding error")]
26 CborDecoding,
27 #[error("Base64 decoding error")]
29 Base64Decoding,
30 #[error(transparent)]
32 DataEnvelope(#[from] DataEnvelopeError),
33}
34
35#[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 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 #[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 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 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(¬_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}