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, FieldView, FolderView,
5    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),
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),
48            folder_id: view.folder_id,
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<SecureNoteType> for crate::SecureNoteType {
192    fn from(value: SecureNoteType) -> Self {
193        match value {
194            SecureNoteType::Generic => crate::SecureNoteType::Generic,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use bitwarden_core::key_management::create_test_crypto_with_user_key;
202    use bitwarden_crypto::SymmetricCryptoKey;
203    use bitwarden_vault::{CipherRepromptType, LoginView};
204    use chrono::{DateTime, Utc};
205
206    use super::*;
207
208    #[test]
209    fn test_try_from_folder_view() {
210        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
211        let view = FolderView {
212            id: Some(test_id),
213            name: "test_name".to_string(),
214            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
215        };
216
217        let f: crate::Folder = view.try_into().unwrap();
218
219        assert_eq!(f.id, test_id);
220        assert_eq!(f.name, "test_name".to_string());
221    }
222
223    #[test]
224    fn test_from_login() {
225        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
226        let key_store = create_test_crypto_with_user_key(key);
227
228        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
229        let view = CipherView {
230            r#type: CipherType::Login,
231            login: Some(LoginView {
232                username: Some("test_username".to_string()),
233                password: Some("test_password".to_string()),
234                password_revision_date: None,
235                uris: None,
236                totp: None,
237                autofill_on_page_load: None,
238                fido2_credentials: None,
239            }),
240            id: Some(test_id),
241            organization_id: None,
242            folder_id: None,
243            collection_ids: vec![],
244            key: None,
245            name: "My login".to_string(),
246            notes: None,
247            identity: None,
248            card: None,
249            secure_note: None,
250            ssh_key: None,
251            favorite: false,
252            reprompt: CipherRepromptType::None,
253            organization_use_totp: true,
254            edit: true,
255            permissions: None,
256            view_password: true,
257            local_data: None,
258            attachments: None,
259            fields: None,
260            password_history: None,
261            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
262            deleted_date: None,
263            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
264        };
265
266        let login = from_login(&view, &key_store).unwrap();
267
268        assert_eq!(login.username, Some("test_username".to_string()));
269        assert_eq!(login.password, Some("test_password".to_string()));
270        assert!(login.login_uris.is_empty());
271        assert_eq!(login.totp, None);
272    }
273
274    #[test]
275    fn test_from_cipher_login() {
276        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
277        let key_store = create_test_crypto_with_user_key(key);
278
279        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
280        let cipher_view = CipherView {
281            r#type: CipherType::Login,
282            login: Some(LoginView {
283                username: Some("test_username".to_string()),
284                password: Some("test_password".to_string()),
285                password_revision_date: None,
286                uris: None,
287                totp: None,
288                autofill_on_page_load: None,
289                fido2_credentials: None,
290            }),
291            id: Some(test_id),
292            organization_id: None,
293            folder_id: None,
294            collection_ids: vec![],
295            key: None,
296            name: "My login".to_string(),
297            notes: None,
298            identity: None,
299            card: None,
300            secure_note: None,
301            ssh_key: None,
302            favorite: false,
303            reprompt: CipherRepromptType::None,
304            organization_use_totp: true,
305            edit: true,
306            permissions: None,
307            view_password: true,
308            local_data: None,
309            attachments: 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        };
316        let encrypted = key_store.encrypt(cipher_view).unwrap();
317
318        let cipher: crate::Cipher = crate::Cipher::from_cipher(&key_store, encrypted).unwrap();
319
320        assert_eq!(cipher.id, test_id);
321        assert_eq!(cipher.folder_id, None);
322        assert_eq!(cipher.name, "My login".to_string());
323        assert_eq!(cipher.notes, None);
324        assert!(!cipher.favorite);
325        assert_eq!(cipher.reprompt, 0);
326        assert!(cipher.fields.is_empty());
327        assert_eq!(
328            cipher.revision_date,
329            "2024-01-30T17:55:36.150Z".parse::<DateTime<Utc>>().unwrap()
330        );
331        assert_eq!(
332            cipher.creation_date,
333            "2024-01-30T17:55:36.150Z".parse::<DateTime<Utc>>().unwrap()
334        );
335        assert_eq!(cipher.deleted_date, None);
336
337        if let crate::CipherType::Login(l) = cipher.r#type {
338            assert_eq!(l.username, Some("test_username".to_string()));
339            assert_eq!(l.password, Some("test_password".to_string()));
340            assert!(l.login_uris.is_empty());
341            assert_eq!(l.totp, None);
342        } else {
343            panic!("Expected login type");
344        }
345    }
346}