bitwarden_vault/cipher/blob/conversions/
mod.rs1use bitwarden_crypto::CryptoError;
2
3use super::v1::*;
4use crate::{
5 CipherView, PasswordHistoryView,
6 cipher::{
7 bank_account::BankAccountView,
8 card::CardView,
9 cipher::CipherType,
10 drivers_license::DriversLicenseView,
11 field::FieldView,
12 identity::IdentityView,
13 login::{Fido2CredentialFullView, Fido2CredentialView, LoginUriView, LoginView},
14 passport::PassportView,
15 secure_note::SecureNoteView,
16 ssh_key::SshKeyView,
17 },
18};
19
20fn none_if_empty<T>(v: Vec<T>) -> Option<Vec<T>> {
21 if v.is_empty() { None } else { Some(v) }
22}
23
24macro_rules! impl_bidirectional_from {
27 ($type_a:ty, $type_b:ty, [$($field:ident),+ $(,)?]) => {
28 impl From<&$type_a> for $type_b {
29 fn from(src: &$type_a) -> Self {
30 Self { $($field: src.$field.clone()),+ }
31 }
32 }
33 impl From<&$type_b> for $type_a {
34 fn from(src: &$type_b) -> Self {
35 Self { $($field: src.$field.clone()),+ }
36 }
37 }
38 };
39}
40
41impl_bidirectional_from!(FieldView, FieldDataV1, [name, value, r#type, linked_id,]);
42
43impl_bidirectional_from!(
44 PasswordHistoryView,
45 PasswordHistoryDataV1,
46 [password, last_used_date,]
47);
48
49mod bank_account;
50mod card;
51mod drivers_license;
52mod identity;
53mod login;
54mod passport;
55mod secure_note;
56mod ssh_key;
57
58impl CipherBlobV1 {
59 pub(crate) fn from_cipher_view(view: &CipherView) -> Result<Self, CryptoError> {
60 let type_data = match view.r#type {
61 CipherType::Login => {
62 let login = view
63 .login
64 .as_ref()
65 .ok_or(CryptoError::MissingField("login"))?;
66
67 let fido2_credentials = login
68 .fido2_credentials
69 .as_deref()
70 .map(|creds| {
71 creds
72 .iter()
73 .map(|v| {
74 Fido2CredentialDataV1::from(&Fido2CredentialFullView::from(
75 v.clone(),
76 ))
77 })
78 .collect()
79 })
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(&self, view: &mut CipherView) -> Result<(), CryptoError> {
165 view.name = self.name.clone();
166 view.notes = self.notes.clone();
167 view.fields = none_if_empty(self.fields.iter().map(FieldView::from).collect());
168 view.password_history = none_if_empty(
169 self.password_history
170 .iter()
171 .map(PasswordHistoryView::from)
172 .collect(),
173 );
174
175 view.login = None;
176 view.card = None;
177 view.identity = None;
178 view.secure_note = None;
179 view.ssh_key = None;
180 view.bank_account = None;
181 view.drivers_license = None;
182 view.passport = None;
183
184 match &self.type_data {
185 CipherTypeDataV1::Login(login_data) => {
186 let fido2_credentials = if login_data.fido2_credentials.is_empty() {
187 None
188 } else {
189 Some(
190 login_data
191 .fido2_credentials
192 .iter()
193 .map(|d| Fido2CredentialView::from(Fido2CredentialFullView::from(d)))
194 .collect::<Vec<_>>(),
195 )
196 };
197
198 view.r#type = CipherType::Login;
199 view.login = Some(LoginView {
200 username: login_data.username.clone(),
201 password: login_data.password.clone(),
202 password_revision_date: login_data.password_revision_date,
203 uris: none_if_empty(login_data.uris.iter().map(LoginUriView::from).collect()),
204 totp: login_data.totp.clone(),
205 autofill_on_page_load: login_data.autofill_on_page_load,
206 fido2_credentials,
207 });
208 }
209 CipherTypeDataV1::Card(card_data) => {
210 view.r#type = CipherType::Card;
211 view.card = Some(CardView::from(card_data));
212 }
213 CipherTypeDataV1::Identity(identity_data) => {
214 view.r#type = CipherType::Identity;
215 view.identity = Some(IdentityView::from(identity_data));
216 }
217 CipherTypeDataV1::SecureNote(secure_note_data) => {
218 view.r#type = CipherType::SecureNote;
219 view.secure_note = Some(SecureNoteView::from(secure_note_data));
220 }
221 CipherTypeDataV1::SshKey(ssh_key_data) => {
222 view.r#type = CipherType::SshKey;
223 view.ssh_key = Some(SshKeyView::from(ssh_key_data));
224 }
225 CipherTypeDataV1::BankAccount(bank_account_data) => {
226 view.r#type = CipherType::BankAccount;
227 view.bank_account = Some(BankAccountView::from(bank_account_data));
228 }
229 CipherTypeDataV1::DriversLicense(drivers_license_data) => {
230 view.r#type = CipherType::DriversLicense;
231 view.drivers_license = Some(DriversLicenseView::from(drivers_license_data));
232 }
233 CipherTypeDataV1::Passport(passport_data) => {
234 view.r#type = CipherType::Passport;
235 view.passport = Some(PassportView::from(passport_data));
236 }
237 }
238
239 Ok(())
240 }
241}
242
243#[cfg(test)]
244pub(crate) mod test_support {
245 use bitwarden_core::key_management::{
246 KeySlotIds, SymmetricKeySlotId, create_test_crypto_with_user_key,
247 };
248 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
249 use chrono::{TimeZone, Utc};
250
251 use crate::{
252 CipherView,
253 cipher::cipher::{CipherRepromptType, CipherType},
254 };
255
256 pub(crate) fn create_test_key_store() -> (KeyStore<KeySlotIds>, SymmetricKeySlotId) {
257 let key = SymmetricCryptoKey::try_from(
258 "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string(),
259 )
260 .unwrap();
261 let key_store = create_test_crypto_with_user_key(key);
262 (key_store, SymmetricKeySlotId::User)
263 }
264
265 pub(crate) fn create_shell_cipher_view(cipher_type: CipherType) -> CipherView {
266 CipherView {
267 partial: false,
268 id: None,
269 organization_id: None,
270 folder_id: None,
271 collection_ids: vec![],
272 key: None,
273 name: String::new(),
274 notes: None,
275 r#type: cipher_type,
276 login: None,
277 identity: None,
278 card: None,
279 secure_note: None,
280 ssh_key: None,
281 bank_account: None,
282 drivers_license: None,
283 passport: None,
284 favorite: false,
285 reprompt: CipherRepromptType::None,
286 organization_use_totp: false,
287 edit: true,
288 permissions: None,
289 view_password: true,
290 local_data: None,
291 attachments: None,
292 attachment_decryption_failures: None,
293 fields: None,
294 password_history: None,
295 creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
296 deleted_date: None,
297 revision_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
298 archived_date: None,
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::{CipherBlobV1, CipherTypeDataV1, test_support::*};
306 use crate::cipher::{
307 cipher::CipherType,
308 login::LoginView,
309 secure_note::{SecureNoteType, SecureNoteView},
310 };
311
312 #[test]
313 fn test_option_vec_normalization_none_to_empty_to_none() {
314 let original = crate::CipherView {
315 name: "Minimal Note".to_string(),
316 notes: None,
317 r#type: CipherType::SecureNote,
318 secure_note: Some(SecureNoteView {
319 r#type: SecureNoteType::Generic,
320 }),
321 fields: None,
322 password_history: None,
323 ..create_shell_cipher_view(CipherType::SecureNote)
324 };
325
326 let blob = CipherBlobV1::from_cipher_view(&original).unwrap();
327
328 assert!(blob.fields.is_empty());
329 assert!(blob.password_history.is_empty());
330
331 let mut restored = create_shell_cipher_view(CipherType::SecureNote);
332 blob.apply_to_cipher_view(&mut restored).unwrap();
333 assert!(restored.fields.is_none());
334 assert!(restored.password_history.is_none());
335 }
336
337 #[test]
338 fn test_login_none_uris_and_fido2_normalization() {
339 let original = crate::CipherView {
340 name: "Simple Login".to_string(),
341 notes: None,
342 r#type: CipherType::Login,
343 login: Some(LoginView {
344 username: Some("user".to_string()),
345 password: None,
346 password_revision_date: None,
347 uris: None,
348 totp: None,
349 autofill_on_page_load: None,
350 fido2_credentials: None,
351 }),
352 ..create_shell_cipher_view(CipherType::Login)
353 };
354
355 let blob = CipherBlobV1::from_cipher_view(&original).unwrap();
356
357 if let CipherTypeDataV1::Login(ref login_data) = blob.type_data {
358 assert!(login_data.uris.is_empty());
359 assert!(login_data.fido2_credentials.is_empty());
360 } else {
361 panic!("Expected Login type data");
362 }
363
364 let mut restored = create_shell_cipher_view(CipherType::Login);
365 blob.apply_to_cipher_view(&mut restored).unwrap();
366
367 let login = restored.login.unwrap();
368 assert!(login.uris.is_none());
369 assert!(login.fido2_credentials.is_none());
370 }
371}