bitwarden_vault/cipher/blob/conversions/
mod.rs1use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
2use bitwarden_crypto::{CompositeEncryptable, CryptoError, Decryptable, KeyStoreContext};
3
4use super::v1::*;
5use crate::{
6 CipherView, PasswordHistoryView,
7 cipher::{
8 bank_account::BankAccountView,
9 card::CardView,
10 cipher::CipherType,
11 drivers_license::DriversLicenseView,
12 field::FieldView,
13 identity::IdentityView,
14 login::{Fido2CredentialFullView, LoginUriView, LoginView},
15 passport::PassportView,
16 secure_note::SecureNoteView,
17 ssh_key::SshKeyView,
18 },
19};
20
21fn none_if_empty<T>(v: Vec<T>) -> Option<Vec<T>> {
22 if v.is_empty() { None } else { Some(v) }
23}
24
25macro_rules! impl_bidirectional_from {
28 ($type_a:ty, $type_b:ty, [$($field:ident),+ $(,)?]) => {
29 impl From<&$type_a> for $type_b {
30 fn from(src: &$type_a) -> Self {
31 Self { $($field: src.$field.clone()),+ }
32 }
33 }
34 impl From<&$type_b> for $type_a {
35 fn from(src: &$type_b) -> Self {
36 Self { $($field: src.$field.clone()),+ }
37 }
38 }
39 };
40}
41
42impl_bidirectional_from!(FieldView, FieldDataV1, [name, value, r#type, linked_id,]);
43
44impl_bidirectional_from!(
45 PasswordHistoryView,
46 PasswordHistoryDataV1,
47 [password, last_used_date,]
48);
49
50mod bank_account;
51mod card;
52mod drivers_license;
53mod identity;
54mod login;
55mod passport;
56mod secure_note;
57mod ssh_key;
58
59impl CipherBlobV1 {
60 pub(crate) fn from_cipher_view(
61 view: &CipherView,
62 ctx: &mut KeyStoreContext<KeySlotIds>,
63 key: SymmetricKeySlotId,
64 ) -> Result<Self, CryptoError> {
65 let type_data = match view.r#type {
66 CipherType::Login => {
67 let login = view
68 .login
69 .as_ref()
70 .ok_or(CryptoError::MissingField("login"))?;
71
72 let fido2_credentials: Vec<Fido2CredentialDataV1> = login
73 .fido2_credentials
74 .as_ref()
75 .map(|creds| -> Result<Vec<_>, CryptoError> {
76 let full_views: Vec<Fido2CredentialFullView> = creds.decrypt(ctx, key)?;
77 Ok(full_views.iter().map(Fido2CredentialDataV1::from).collect())
78 })
79 .transpose()?
80 .unwrap_or_default();
81
82 CipherTypeDataV1::Login(LoginDataV1 {
83 username: login.username.clone(),
84 password: login.password.clone(),
85 password_revision_date: login.password_revision_date,
86 uris: login
87 .uris
88 .as_ref()
89 .map(|u| u.iter().map(LoginUriDataV1::from).collect())
90 .unwrap_or_default(),
91 totp: login.totp.clone(),
92 autofill_on_page_load: login.autofill_on_page_load,
93 fido2_credentials,
94 })
95 }
96 CipherType::Card => {
97 let card = view
98 .card
99 .as_ref()
100 .ok_or(CryptoError::MissingField("card"))?;
101 CipherTypeDataV1::Card(CardDataV1::from(card))
102 }
103 CipherType::Identity => {
104 let identity = view
105 .identity
106 .as_ref()
107 .ok_or(CryptoError::MissingField("identity"))?;
108 CipherTypeDataV1::Identity(IdentityDataV1::from(identity))
109 }
110 CipherType::SecureNote => {
111 let secure_note = view
112 .secure_note
113 .as_ref()
114 .ok_or(CryptoError::MissingField("secure_note"))?;
115 CipherTypeDataV1::SecureNote(SecureNoteDataV1::from(secure_note))
116 }
117 CipherType::SshKey => {
118 let ssh_key = view
119 .ssh_key
120 .as_ref()
121 .ok_or(CryptoError::MissingField("ssh_key"))?;
122 CipherTypeDataV1::SshKey(SshKeyDataV1::from(ssh_key))
123 }
124 CipherType::BankAccount => {
125 let bank_account = view
126 .bank_account
127 .as_ref()
128 .ok_or(CryptoError::MissingField("bank_account"))?;
129 CipherTypeDataV1::BankAccount(BankAccountDataV1::from(bank_account))
130 }
131 CipherType::DriversLicense => {
132 let drivers_license = view
133 .drivers_license
134 .as_ref()
135 .ok_or(CryptoError::MissingField("drivers_license"))?;
136 CipherTypeDataV1::DriversLicense(DriversLicenseDataV1::from(drivers_license))
137 }
138 CipherType::Passport => {
139 let passport = view
140 .passport
141 .as_ref()
142 .ok_or(CryptoError::MissingField("passport"))?;
143 CipherTypeDataV1::Passport(PassportDataV1::from(passport))
144 }
145 };
146
147 Ok(Self {
148 name: view.name.clone(),
149 notes: view.notes.clone(),
150 type_data,
151 fields: view
152 .fields
153 .as_ref()
154 .map(|f| f.iter().map(FieldDataV1::from).collect())
155 .unwrap_or_default(),
156 password_history: view
157 .password_history
158 .as_ref()
159 .map(|h| h.iter().map(PasswordHistoryDataV1::from).collect())
160 .unwrap_or_default(),
161 })
162 }
163
164 pub(crate) fn apply_to_cipher_view(
165 &self,
166 view: &mut CipherView,
167 ctx: &mut KeyStoreContext<KeySlotIds>,
168 key: SymmetricKeySlotId,
169 ) -> Result<(), CryptoError> {
170 view.name = self.name.clone();
171 view.notes = self.notes.clone();
172 view.fields = none_if_empty(self.fields.iter().map(FieldView::from).collect());
173 view.password_history = none_if_empty(
174 self.password_history
175 .iter()
176 .map(PasswordHistoryView::from)
177 .collect(),
178 );
179
180 view.login = None;
181 view.card = None;
182 view.identity = None;
183 view.secure_note = None;
184 view.ssh_key = None;
185 view.bank_account = None;
186 view.drivers_license = None;
187 view.passport = None;
188
189 match &self.type_data {
190 CipherTypeDataV1::Login(login_data) => {
191 let fido2_credentials = if login_data.fido2_credentials.is_empty() {
192 None
193 } else {
194 let full_views: Vec<Fido2CredentialFullView> = login_data
195 .fido2_credentials
196 .iter()
197 .map(Fido2CredentialFullView::from)
198 .collect();
199 Some(full_views.encrypt_composite(ctx, key)?)
200 };
201
202 view.r#type = CipherType::Login;
203 view.login = Some(LoginView {
204 username: login_data.username.clone(),
205 password: login_data.password.clone(),
206 password_revision_date: login_data.password_revision_date,
207 uris: none_if_empty(login_data.uris.iter().map(LoginUriView::from).collect()),
208 totp: login_data.totp.clone(),
209 autofill_on_page_load: login_data.autofill_on_page_load,
210 fido2_credentials,
211 });
212 }
213 CipherTypeDataV1::Card(card_data) => {
214 view.r#type = CipherType::Card;
215 view.card = Some(CardView::from(card_data));
216 }
217 CipherTypeDataV1::Identity(identity_data) => {
218 view.r#type = CipherType::Identity;
219 view.identity = Some(IdentityView::from(identity_data));
220 }
221 CipherTypeDataV1::SecureNote(secure_note_data) => {
222 view.r#type = CipherType::SecureNote;
223 view.secure_note = Some(SecureNoteView::from(secure_note_data));
224 }
225 CipherTypeDataV1::SshKey(ssh_key_data) => {
226 view.r#type = CipherType::SshKey;
227 view.ssh_key = Some(SshKeyView::from(ssh_key_data));
228 }
229 CipherTypeDataV1::BankAccount(bank_account_data) => {
230 view.r#type = CipherType::BankAccount;
231 view.bank_account = Some(BankAccountView::from(bank_account_data));
232 }
233 CipherTypeDataV1::DriversLicense(drivers_license_data) => {
234 view.r#type = CipherType::DriversLicense;
235 view.drivers_license = Some(DriversLicenseView::from(drivers_license_data));
236 }
237 CipherTypeDataV1::Passport(passport_data) => {
238 view.r#type = CipherType::Passport;
239 view.passport = Some(PassportView::from(passport_data));
240 }
241 }
242
243 Ok(())
244 }
245}
246
247#[cfg(test)]
248pub(crate) mod test_support {
249 use bitwarden_core::key_management::{
250 KeySlotIds, SymmetricKeySlotId, create_test_crypto_with_user_key,
251 };
252 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
253 use chrono::{TimeZone, Utc};
254
255 use crate::{
256 CipherView,
257 cipher::cipher::{CipherRepromptType, CipherType},
258 };
259
260 pub(crate) fn create_test_key_store() -> (KeyStore<KeySlotIds>, SymmetricKeySlotId) {
261 let key = SymmetricCryptoKey::try_from(
262 "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string(),
263 )
264 .unwrap();
265 let key_store = create_test_crypto_with_user_key(key);
266 (key_store, SymmetricKeySlotId::User)
267 }
268
269 pub(crate) fn create_shell_cipher_view(cipher_type: CipherType) -> CipherView {
270 CipherView {
271 id: None,
272 organization_id: None,
273 folder_id: None,
274 collection_ids: vec![],
275 key: None,
276 name: String::new(),
277 notes: None,
278 r#type: cipher_type,
279 login: None,
280 identity: None,
281 card: None,
282 secure_note: None,
283 ssh_key: None,
284 bank_account: None,
285 drivers_license: None,
286 passport: None,
287 favorite: false,
288 reprompt: CipherRepromptType::None,
289 organization_use_totp: false,
290 edit: true,
291 permissions: None,
292 view_password: true,
293 local_data: None,
294 attachments: None,
295 attachment_decryption_failures: None,
296 fields: None,
297 password_history: None,
298 creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
299 deleted_date: None,
300 revision_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
301 archived_date: None,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::{CipherBlobV1, CipherTypeDataV1, test_support::*};
309 use crate::cipher::{
310 cipher::CipherType,
311 login::LoginView,
312 secure_note::{SecureNoteType, SecureNoteView},
313 };
314
315 #[test]
316 fn test_option_vec_normalization_none_to_empty_to_none() {
317 let (key_store, key_id) = create_test_key_store();
318 let mut ctx = key_store.context_mut();
319
320 let original = crate::CipherView {
321 name: "Minimal Note".to_string(),
322 notes: None,
323 r#type: CipherType::SecureNote,
324 secure_note: Some(SecureNoteView {
325 r#type: SecureNoteType::Generic,
326 }),
327 fields: None,
328 password_history: None,
329 ..create_shell_cipher_view(CipherType::SecureNote)
330 };
331
332 let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap();
333
334 assert!(blob.fields.is_empty());
335 assert!(blob.password_history.is_empty());
336
337 let mut restored = create_shell_cipher_view(CipherType::SecureNote);
338 blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id)
339 .unwrap();
340 assert!(restored.fields.is_none());
341 assert!(restored.password_history.is_none());
342 }
343
344 #[test]
345 fn test_login_none_uris_and_fido2_normalization() {
346 let (key_store, key_id) = create_test_key_store();
347 let mut ctx = key_store.context_mut();
348
349 let original = crate::CipherView {
350 name: "Simple Login".to_string(),
351 notes: None,
352 r#type: CipherType::Login,
353 login: Some(LoginView {
354 username: Some("user".to_string()),
355 password: None,
356 password_revision_date: None,
357 uris: None,
358 totp: None,
359 autofill_on_page_load: None,
360 fido2_credentials: None,
361 }),
362 ..create_shell_cipher_view(CipherType::Login)
363 };
364
365 let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap();
366
367 if let CipherTypeDataV1::Login(ref login_data) = blob.type_data {
368 assert!(login_data.uris.is_empty());
369 assert!(login_data.fido2_credentials.is_empty());
370 } else {
371 panic!("Expected Login type data");
372 }
373
374 let mut restored = create_shell_cipher_view(CipherType::Login);
375 blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id)
376 .unwrap();
377
378 let login = restored.login.unwrap();
379 assert!(login.uris.is_none());
380 assert!(login.fido2_credentials.is_none());
381 }
382}