Skip to main content

bitwarden_exporters/
models.rs

1use bitwarden_core::{MissingFieldError, key_management::KeySlotIds, require};
2use bitwarden_crypto::KeyStore;
3use bitwarden_vault::{
4    CardView, Cipher, CipherType, CipherView, Fido2CredentialFullView, FieldType, FieldView,
5    FolderView, IdentityView, LoginUriView, PasswordHistoryView, SecureNoteType, SecureNoteView,
6    SshKeyView,
7};
8
9impl TryFrom<FolderView> for crate::Folder {
10    type Error = MissingFieldError;
11
12    fn try_from(value: FolderView) -> Result<Self, Self::Error> {
13        Ok(Self {
14            id: require!(value.id).into(),
15            name: value.name,
16        })
17    }
18}
19
20impl crate::Cipher {
21    pub(crate) fn from_cipher(
22        key_store: &KeyStore<KeySlotIds>,
23        cipher: Cipher,
24    ) -> Result<Self, crate::error::ExportError> {
25        let view: CipherView = key_store.decrypt(&cipher)?;
26
27        let r = match view.r#type {
28            CipherType::Login => crate::CipherType::Login(Box::new(from_login(&view, key_store)?)),
29            CipherType::SecureNote => {
30                let s = require!(view.secure_note);
31                crate::CipherType::SecureNote(Box::new(s.into()))
32            }
33            CipherType::Card => {
34                let c = require!(view.card);
35                crate::CipherType::Card(Box::new(c.into()))
36            }
37            CipherType::Identity => {
38                let i = require!(view.identity);
39                crate::CipherType::Identity(Box::new(i.into()))
40            }
41            CipherType::SshKey => {
42                let s = require!(view.ssh_key);
43                crate::CipherType::SshKey(Box::new(s.into()))
44            }
45            CipherType::BankAccount => {
46                // Bank accounts are not currently supported by the exporter, but we include this
47                // match arm to future-proof against the API returning bank account ciphers.
48                crate::CipherType::BankAccount
49            }
50            CipherType::Passport => {
51                // Passports are not currently supported by the exporter, but we include this
52                // match arm to future-proof against the API returning passport ciphers.
53                crate::CipherType::Passport
54            }
55            CipherType::DriversLicense => {
56                // Drivers licenses are not currently supported by the exporter, but we include this
57                // match arm to future-proof against the API returning drivers license ciphers.
58                crate::CipherType::DriversLicense
59            }
60        };
61
62        Ok(Self {
63            id: require!(view.id).into(),
64            folder_id: view.folder_id.map(|f| f.into()),
65            name: view.name,
66            notes: view.notes,
67            r#type: r,
68            favorite: view.favorite,
69            reprompt: view.reprompt as u8,
70            fields: view
71                .fields
72                .unwrap_or_default()
73                .into_iter()
74                .map(|f| f.into())
75                .collect(),
76            password_history: view
77                .password_history
78                .map(|h| h.into_iter().map(|p| p.into()).collect()),
79            revision_date: view.revision_date,
80            creation_date: view.creation_date,
81            deleted_date: view.deleted_date,
82        })
83    }
84}
85
86impl From<PasswordHistoryView> for crate::PasswordHistory {
87    fn from(value: PasswordHistoryView) -> Self {
88        Self {
89            password: value.password,
90            last_used_date: value.last_used_date,
91        }
92    }
93}
94
95/// Convert a `LoginView` into a `crate::Login`.
96fn from_login(
97    view: &CipherView,
98    key_store: &KeyStore<KeySlotIds>,
99) -> Result<crate::Login, MissingFieldError> {
100    let l = require!(view.login.clone());
101
102    Ok(crate::Login {
103        username: l.username,
104        password: l.password,
105        login_uris: l
106            .uris
107            .unwrap_or_default()
108            .into_iter()
109            .map(|u| u.into())
110            .collect(),
111        totp: l.totp,
112        fido2_credentials: l.fido2_credentials.as_ref().and_then(|_| {
113            let credentials = view.get_fido2_credentials(&mut key_store.context()).ok()?;
114            if credentials.is_empty() {
115                None
116            } else {
117                Some(credentials.into_iter().map(|c| c.into()).collect())
118            }
119        }),
120    })
121}
122
123impl From<LoginUriView> for crate::LoginUri {
124    fn from(value: LoginUriView) -> Self {
125        Self {
126            r#match: value.r#match.map(|v| v as u8),
127            uri: value.uri,
128        }
129    }
130}
131
132impl From<Fido2CredentialFullView> for crate::Fido2Credential {
133    fn from(value: Fido2CredentialFullView) -> Self {
134        Self {
135            credential_id: value.credential_id,
136            key_type: value.key_type,
137            key_algorithm: value.key_algorithm,
138            key_curve: value.key_curve,
139            key_value: value.key_value,
140            rp_id: value.rp_id,
141            user_handle: value.user_handle,
142            user_name: value.user_name,
143            counter: value.counter.parse().expect("Invalid counter"),
144            rp_name: value.rp_name,
145            user_display_name: value.user_display_name,
146            discoverable: value.discoverable,
147            creation_date: value.creation_date,
148        }
149    }
150}
151
152impl From<SecureNoteView> for crate::SecureNote {
153    fn from(view: SecureNoteView) -> Self {
154        crate::SecureNote {
155            r#type: view.r#type.into(),
156        }
157    }
158}
159
160impl From<CardView> for crate::Card {
161    fn from(view: CardView) -> Self {
162        crate::Card {
163            cardholder_name: view.cardholder_name,
164            exp_month: view.exp_month,
165            exp_year: view.exp_year,
166            code: view.code,
167            brand: view.brand,
168            number: view.number,
169        }
170    }
171}
172
173impl From<IdentityView> for crate::Identity {
174    fn from(view: IdentityView) -> Self {
175        crate::Identity {
176            title: view.title,
177            first_name: view.first_name,
178            middle_name: view.middle_name,
179            last_name: view.last_name,
180            address1: view.address1,
181            address2: view.address2,
182            address3: view.address3,
183            city: view.city,
184            state: view.state,
185            postal_code: view.postal_code,
186            country: view.country,
187            company: view.company,
188            email: view.email,
189            phone: view.phone,
190            ssn: view.ssn,
191            username: view.username,
192            passport_number: view.passport_number,
193            license_number: view.license_number,
194        }
195    }
196}
197
198impl From<SshKeyView> for crate::SshKey {
199    fn from(view: SshKeyView) -> Self {
200        crate::SshKey {
201            private_key: view.private_key,
202            public_key: view.public_key,
203            fingerprint: view.fingerprint,
204        }
205    }
206}
207
208impl From<FieldView> for crate::Field {
209    fn from(value: FieldView) -> Self {
210        Self {
211            name: value.name,
212            value: value.value,
213            r#type: value.r#type as u8,
214            linked_id: value.linked_id.map(|id| id.into()),
215        }
216    }
217}
218
219impl From<crate::Field> for FieldView {
220    fn from(value: crate::Field) -> Self {
221        Self {
222            name: value.name,
223            value: value.value,
224            r#type: value.r#type.try_into().unwrap_or(FieldType::Text),
225            linked_id: value.linked_id.and_then(|id| id.try_into().ok()),
226        }
227    }
228}
229
230impl From<SecureNoteType> for crate::SecureNoteType {
231    fn from(value: SecureNoteType) -> Self {
232        match value {
233            SecureNoteType::Generic => crate::SecureNoteType::Generic,
234        }
235    }
236}
237
238impl From<crate::SecureNoteType> for SecureNoteType {
239    fn from(value: crate::SecureNoteType) -> Self {
240        match value {
241            crate::SecureNoteType::Generic => SecureNoteType::Generic,
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use bitwarden_core::key_management::create_test_crypto_with_user_key;
249    use bitwarden_crypto::SymmetricCryptoKey;
250    use bitwarden_vault::{CipherId, CipherRepromptType, FolderId, LoginView};
251    use chrono::{DateTime, Utc};
252
253    use super::*;
254
255    #[test]
256    fn test_try_from_folder_view() {
257        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
258        let view = FolderView {
259            id: Some(FolderId::new(test_id)),
260            name: "test_name".to_string(),
261            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
262        };
263
264        let f: crate::Folder = view.try_into().unwrap();
265
266        assert_eq!(f.id, test_id);
267        assert_eq!(f.name, "test_name".to_string());
268    }
269
270    #[test]
271    fn test_from_login() {
272        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
273        let key_store = create_test_crypto_with_user_key(key);
274
275        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
276        let view = CipherView {
277            r#type: CipherType::Login,
278            login: Some(LoginView {
279                username: Some("test_username".to_string()),
280                password: Some("test_password".to_string()),
281                password_revision_date: None,
282                uris: None,
283                totp: None,
284                autofill_on_page_load: None,
285                fido2_credentials: None,
286            }),
287            id: Some(CipherId::new(test_id)),
288            organization_id: None,
289            folder_id: None,
290            collection_ids: vec![],
291            key: None,
292            name: "My login".to_string(),
293            notes: None,
294            identity: None,
295            card: None,
296            secure_note: None,
297            ssh_key: None,
298            bank_account: None,
299            passport: None,
300            drivers_license: None,
301            favorite: false,
302            reprompt: CipherRepromptType::None,
303            organization_use_totp: true,
304            edit: true,
305            permissions: None,
306            view_password: true,
307            local_data: None,
308            attachments: None,
309            attachment_decryption_failures: None,
310            fields: None,
311            password_history: None,
312            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
313            deleted_date: None,
314            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
315            archived_date: None,
316        };
317
318        let login = from_login(&view, &key_store).unwrap();
319
320        assert_eq!(login.username, Some("test_username".to_string()));
321        assert_eq!(login.password, Some("test_password".to_string()));
322        assert!(login.login_uris.is_empty());
323        assert_eq!(login.totp, None);
324    }
325
326    #[test]
327    fn test_from_cipher_login() {
328        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
329        let key_store = create_test_crypto_with_user_key(key);
330
331        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
332        let cipher_view = CipherView {
333            r#type: CipherType::Login,
334            login: Some(LoginView {
335                username: Some("test_username".to_string()),
336                password: Some("test_password".to_string()),
337                password_revision_date: None,
338                uris: None,
339                totp: None,
340                autofill_on_page_load: None,
341                fido2_credentials: None,
342            }),
343            id: Some(CipherId::new(test_id)),
344            organization_id: None,
345            folder_id: None,
346            collection_ids: vec![],
347            key: None,
348            name: "My login".to_string(),
349            notes: None,
350            identity: None,
351            card: None,
352            secure_note: None,
353            ssh_key: None,
354            bank_account: None,
355            passport: None,
356            drivers_license: None,
357            favorite: false,
358            reprompt: CipherRepromptType::None,
359            organization_use_totp: true,
360            edit: true,
361            permissions: None,
362            view_password: true,
363            local_data: None,
364            attachments: None,
365            attachment_decryption_failures: None,
366            fields: None,
367            password_history: None,
368            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
369            deleted_date: None,
370            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
371            archived_date: None,
372        };
373        let encrypted = key_store.encrypt(cipher_view).unwrap();
374
375        let cipher: crate::Cipher = crate::Cipher::from_cipher(&key_store, encrypted).unwrap();
376
377        assert_eq!(cipher.id, test_id);
378        assert_eq!(cipher.folder_id, None);
379        assert_eq!(cipher.name, "My login".to_string());
380        assert_eq!(cipher.notes, None);
381        assert!(!cipher.favorite);
382        assert_eq!(cipher.reprompt, 0);
383        assert!(cipher.fields.is_empty());
384        assert_eq!(
385            cipher.revision_date,
386            "2024-01-30T17:55:36.150Z".parse::<DateTime<Utc>>().unwrap()
387        );
388        assert_eq!(
389            cipher.creation_date,
390            "2024-01-30T17:55:36.150Z".parse::<DateTime<Utc>>().unwrap()
391        );
392        assert_eq!(cipher.deleted_date, None);
393
394        if let crate::CipherType::Login(l) = cipher.r#type {
395            assert_eq!(l.username, Some("test_username".to_string()));
396            assert_eq!(l.password, Some("test_password".to_string()));
397            assert!(l.login_uris.is_empty());
398            assert_eq!(l.totp, None);
399        } else {
400            panic!("Expected login type");
401        }
402    }
403}