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_logging::instrument;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use super::CipherBlob;
11
12const FORMAT_VERSION: u8 = 1;
13
14#[derive(Debug, Error)]
16pub enum SealedCipherBlobError {
17 #[error("Unsupported format version: {0}")]
19 UnsupportedFormatVersion(u8),
20 #[error("JSON encoding error")]
22 JsonEncoding,
23 #[error("JSON decoding error")]
25 JsonDecoding,
26 #[error(transparent)]
28 DataEnvelope(#[from] DataEnvelopeError),
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone)]
35pub(crate) struct SealedCipherBlob {
36 format_version: u8,
37 wrapped_cek: EncString,
38 envelope: DataEnvelope,
39}
40
41impl SealedCipherBlob {
42 pub(super) fn seal(
45 data: CipherBlob,
46 wrapping_key: &SymmetricKeySlotId,
47 ctx: &mut KeyStoreContext<KeySlotIds>,
48 ) -> Result<Self, SealedCipherBlobError> {
49 let (envelope, wrapped_cek) =
50 DataEnvelope::seal_with_wrapping_key(data, wrapping_key, ctx)?;
51 Ok(Self {
52 format_version: FORMAT_VERSION,
53 wrapped_cek,
54 envelope,
55 })
56 }
57
58 #[instrument(err, fields(format_version = self.format_version))]
60 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 serde_json::to_string(self).map_err(|_| SealedCipherBlobError::JsonEncoding)
78 }
79
80 pub(super) fn from_opaque_string(s: &str) -> Result<Self, SealedCipherBlobError> {
85 let value: serde_json::Value =
86 serde_json::from_str(s).map_err(|_| SealedCipherBlobError::JsonDecoding)?;
87 if !value
88 .as_object()
89 .is_some_and(|o| o.contains_key("format_version"))
90 {
91 return Err(SealedCipherBlobError::JsonDecoding);
92 }
93 serde_json::from_value(value).map_err(|_| SealedCipherBlobError::JsonDecoding)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use bitwarden_core::key_management::KeySlotIds;
100 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
101 use bitwarden_encoding::B64;
102
103 use super::*;
104 use crate::cipher::{blob::v1::*, secure_note::SecureNoteType};
105
106 fn test_cipher_blob() -> CipherBlob {
107 CipherBlobV1 {
108 name: "Test Cipher".to_string(),
109 notes: Some("Some notes".to_string()),
110 type_data: CipherTypeDataV1::SecureNote(SecureNoteDataV1 {
111 r#type: SecureNoteType::Generic,
112 }),
113 fields: Vec::new(),
114 password_history: Vec::new(),
115 }
116 .into()
117 }
118
119 #[test]
120 fn test_seal_unseal_round_trip() {
121 let store: KeyStore<KeySlotIds> = KeyStore::default();
122 let mut ctx = store.context_mut();
123 let wrapping_key = ctx.generate_symmetric_key();
124
125 let sealed = SealedCipherBlob::seal(test_cipher_blob(), &wrapping_key, &mut ctx).unwrap();
126 let unsealed = sealed.unseal(&wrapping_key, &mut ctx).unwrap();
127
128 assert_eq!(test_cipher_blob(), unsealed);
129 }
130
131 #[test]
132 fn test_opaque_string_round_trip() {
133 let store: KeyStore<KeySlotIds> = KeyStore::default();
134 let mut ctx = store.context_mut();
135 let wrapping_key = ctx.generate_symmetric_key();
136
137 let sealed = SealedCipherBlob::seal(test_cipher_blob(), &wrapping_key, &mut ctx).unwrap();
138
139 let opaque = sealed.to_opaque_string().unwrap();
140 let restored = SealedCipherBlob::from_opaque_string(&opaque).unwrap();
141 let unsealed = restored.unseal(&wrapping_key, &mut ctx).unwrap();
142
143 assert_eq!(test_cipher_blob(), unsealed);
144 }
145
146 #[test]
147 fn test_unsupported_format_version() {
148 let store: KeyStore<KeySlotIds> = KeyStore::default();
149 let mut ctx = store.context_mut();
150 let wrapping_key = ctx.generate_symmetric_key();
151
152 let blob = test_cipher_blob();
153 let mut sealed = SealedCipherBlob::seal(blob, &wrapping_key, &mut ctx).unwrap();
154 sealed.format_version = 99;
155
156 let result = sealed.unseal(&wrapping_key, &mut ctx);
157 assert!(matches!(
158 result,
159 Err(SealedCipherBlobError::UnsupportedFormatVersion(99))
160 ));
161 }
162
163 #[test]
164 fn test_invalid_json() {
165 let result = SealedCipherBlob::from_opaque_string("not json");
166 assert!(matches!(result, Err(SealedCipherBlobError::JsonDecoding)));
167 }
168
169 #[test]
170 fn test_missing_format_version() {
171 let result = SealedCipherBlob::from_opaque_string("{\"Name\":\"x\"}");
172 assert!(matches!(result, Err(SealedCipherBlobError::JsonDecoding)));
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 "z27dMz/RK4wboY/Ako0YVFr9jaiSjgQQyGkTZ4LIuNrOXyeDAjeD41qbhVKl0OSjP3QuN9xmAJQE8+V5/Tl7ig==";
196 const TEST_VECTOR_SEALED_BLOB: &str = r#"{"format_version":1,"wrapped_cek":"2.LQJf2BbznXX+NelBY4pSJg==|txMmjZEOhSMA7Jrm+rZt1LDfA6s3G2QU5Z8MqO4nG9s2ZXuzSLU/iYOUXD8xw+eHVSu7IUHu1LsCm4SLf+ZhkX5QIo4hJT3DHSbgu6VPUC0=|yuU/EWQWyihf2Yh9lQ1NP+zTROEpnXoRS//GfxDgC4k=","envelope":"g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFBoHnjLne8MPV72YPXuskd6OgABOIECOgABOIABoQVYGA00vxb7gF7Y3SUyoCMy34C1HrB3fSY3jVhxZXQmmotGEIwwRlG+SpTcyTl5m4lUnozWrjAYfWitl1+cz457Wq3iDW/MvrHE7c1g38QJxY6t1yhQL0dQy9DyDXQDiWGPtYzic2Ay+GtrlIERN37wOdhQ1HZDeoobHL+aKomvPTems/Ta2SqWC9HfE38="}"#;
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}