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)?)),
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(view: &CipherView) -> Result<crate::Login, MissingFieldError> {
97    let l = require!(view.login.clone());
98
99    Ok(crate::Login {
100        username: l.username,
101        password: l.password,
102        login_uris: l
103            .uris
104            .unwrap_or_default()
105            .into_iter()
106            .map(|u| u.into())
107            .collect(),
108        totp: l.totp,
109        fido2_credentials: l.fido2_credentials.as_ref().and_then(|_| {
110            let credentials = view.get_fido2_credentials();
111            if credentials.is_empty() {
112                None
113            } else {
114                Some(
115                    credentials
116                        .into_iter()
117                        .map(|c| Fido2CredentialFullView::from(c).into())
118                        .collect(),
119                )
120            }
121        }),
122    })
123}
124
125impl From<LoginUriView> for crate::LoginUri {
126    fn from(value: LoginUriView) -> Self {
127        Self {
128            r#match: value.r#match.map(|v| v as u8),
129            uri: value.uri,
130        }
131    }
132}
133
134impl From<Fido2CredentialFullView> for crate::Fido2Credential {
135    fn from(value: Fido2CredentialFullView) -> Self {
136        Self {
137            credential_id: value.credential_id,
138            key_type: value.key_type,
139            key_algorithm: value.key_algorithm,
140            key_curve: value.key_curve,
141            key_value: value.key_value,
142            rp_id: value.rp_id,
143            user_handle: value.user_handle,
144            user_name: value.user_name,
145            counter: value.counter.parse().expect("Invalid counter"),
146            rp_name: value.rp_name,
147            user_display_name: value.user_display_name,
148            discoverable: value.discoverable,
149            creation_date: value.creation_date,
150        }
151    }
152}
153
154impl From<SecureNoteView> for crate::SecureNote {
155    fn from(view: SecureNoteView) -> Self {
156        crate::SecureNote {
157            r#type: view.r#type.into(),
158        }
159    }
160}
161
162impl From<CardView> for crate::Card {
163    fn from(view: CardView) -> Self {
164        crate::Card {
165            cardholder_name: view.cardholder_name,
166            exp_month: view.exp_month,
167            exp_year: view.exp_year,
168            code: view.code,
169            brand: view.brand,
170            number: view.number,
171        }
172    }
173}
174
175impl From<IdentityView> for crate::Identity {
176    fn from(view: IdentityView) -> Self {
177        crate::Identity {
178            title: view.title,
179            first_name: view.first_name,
180            middle_name: view.middle_name,
181            last_name: view.last_name,
182            address1: view.address1,
183            address2: view.address2,
184            address3: view.address3,
185            city: view.city,
186            state: view.state,
187            postal_code: view.postal_code,
188            country: view.country,
189            company: view.company,
190            email: view.email,
191            phone: view.phone,
192            ssn: view.ssn,
193            username: view.username,
194            passport_number: view.passport_number,
195            license_number: view.license_number,
196        }
197    }
198}
199
200impl From<SshKeyView> for crate::SshKey {
201    fn from(view: SshKeyView) -> Self {
202        crate::SshKey {
203            private_key: view.private_key,
204            public_key: view.public_key,
205            fingerprint: view.fingerprint,
206        }
207    }
208}
209
210impl From<FieldView> for crate::Field {
211    fn from(value: FieldView) -> Self {
212        Self {
213            name: value.name,
214            value: value.value,
215            r#type: value.r#type as u8,
216            linked_id: value.linked_id.map(|id| id.into()),
217        }
218    }
219}
220
221impl From<crate::Field> for FieldView {
222    fn from(value: crate::Field) -> Self {
223        Self {
224            name: value.name,
225            value: value.value,
226            r#type: value.r#type.try_into().unwrap_or(FieldType::Text),
227            linked_id: value.linked_id.and_then(|id| id.try_into().ok()),
228        }
229    }
230}
231
232impl From<SecureNoteType> for crate::SecureNoteType {
233    fn from(value: SecureNoteType) -> Self {
234        match value {
235            SecureNoteType::Generic => crate::SecureNoteType::Generic,
236        }
237    }
238}
239
240impl From<crate::SecureNoteType> for SecureNoteType {
241    fn from(value: crate::SecureNoteType) -> Self {
242        match value {
243            crate::SecureNoteType::Generic => SecureNoteType::Generic,
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use bitwarden_core::key_management::create_test_crypto_with_user_key;
251    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
252    use bitwarden_vault::{CipherId, CipherRepromptType, EncryptMode, FolderId, LoginView};
253    use chrono::{DateTime, Utc};
254
255    use super::*;
256
257    #[test]
258    fn test_try_from_folder_view() {
259        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
260        let view = FolderView {
261            id: Some(FolderId::new(test_id)),
262            name: "test_name".to_string(),
263            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
264        };
265
266        let f: crate::Folder = view.try_into().unwrap();
267
268        assert_eq!(f.id, test_id);
269        assert_eq!(f.name, "test_name".to_string());
270    }
271
272    #[test]
273    fn test_from_login() {
274        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
275        let view = CipherView {
276            partial: false,
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).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(SymmetricKeyAlgorithm::Aes256CbcHmac);
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            partial: false,
334            r#type: CipherType::Login,
335            login: Some(LoginView {
336                username: Some("test_username".to_string()),
337                password: Some("test_password".to_string()),
338                password_revision_date: None,
339                uris: None,
340                totp: None,
341                autofill_on_page_load: None,
342                fido2_credentials: None,
343            }),
344            id: Some(CipherId::new(test_id)),
345            organization_id: None,
346            folder_id: None,
347            collection_ids: vec![],
348            key: None,
349            name: "My login".to_string(),
350            notes: None,
351            identity: None,
352            card: None,
353            secure_note: None,
354            ssh_key: None,
355            bank_account: None,
356            passport: None,
357            drivers_license: None,
358            favorite: false,
359            reprompt: CipherRepromptType::None,
360            organization_use_totp: true,
361            edit: true,
362            permissions: None,
363            view_password: true,
364            local_data: None,
365            attachments: None,
366            attachment_decryption_failures: None,
367            fields: None,
368            password_history: None,
369            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
370            deleted_date: None,
371            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
372            archived_date: None,
373        };
374        let encrypted = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
375
376        let cipher: crate::Cipher = crate::Cipher::from_cipher(&key_store, encrypted).unwrap();
377
378        assert_eq!(cipher.id, test_id);
379        assert_eq!(cipher.folder_id, None);
380        assert_eq!(cipher.name, "My login".to_string());
381        assert_eq!(cipher.notes, None);
382        assert!(!cipher.favorite);
383        assert_eq!(cipher.reprompt, 0);
384        assert!(cipher.fields.is_empty());
385        assert_eq!(
386            cipher.revision_date,
387            "2024-01-30T17:55:36.150Z".parse::<DateTime<Utc>>().unwrap()
388        );
389        assert_eq!(
390            cipher.creation_date,
391            "2024-01-30T17:55:36.150Z".parse::<DateTime<Utc>>().unwrap()
392        );
393        assert_eq!(cipher.deleted_date, None);
394
395        if let crate::CipherType::Login(l) = cipher.r#type {
396            assert_eq!(l.username, Some("test_username".to_string()));
397            assert_eq!(l.password, Some("test_password".to_string()));
398            assert!(l.login_uris.is_empty());
399            assert_eq!(l.totp, None);
400        } else {
401            panic!("Expected login type");
402        }
403    }
404}