Skip to main content

bitwarden_vault/cipher/blob/conversions/
mod.rs

1use 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
24/// Generates bidirectional `From` impls between two types that share
25/// identical field names. Every field is `.clone()`d.
26macro_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}