bitwarden_exporters/
models.rs

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