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