Skip to main content

bitwarden_vault/cipher/blob/conversions/
mod.rs

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