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