Skip to main content

bitwarden_vault/cipher/blob/conversions/
login.rs

1use super::{Fido2CredentialDataV1, Fido2CredentialFullView, LoginUriDataV1, LoginUriView};
2
3// --- LoginUriView <-> LoginUriDataV1 ---
4
5impl From<&LoginUriView> for LoginUriDataV1 {
6    fn from(view: &LoginUriView) -> Self {
7        Self {
8            uri: view.uri.clone(),
9            r#match: view.r#match,
10        }
11    }
12}
13
14impl From<&LoginUriDataV1> for LoginUriView {
15    fn from(data: &LoginUriDataV1) -> Self {
16        Self {
17            uri: data.uri.clone(),
18            r#match: data.r#match,
19            uri_checksum: None,
20        }
21    }
22}
23
24// --- Fido2CredentialFullView <-> Fido2CredentialDataV1 ---
25
26impl From<&Fido2CredentialFullView> for Fido2CredentialDataV1 {
27    fn from(view: &Fido2CredentialFullView) -> Self {
28        Self {
29            credential_id: view.credential_id.clone(),
30            key_type: view.key_type.clone(),
31            key_algorithm: view.key_algorithm.clone(),
32            key_curve: view.key_curve.clone(),
33            key_value: view.key_value.clone(),
34            rp_id: view.rp_id.clone(),
35            user_handle: view.user_handle.clone(),
36            user_name: view.user_name.clone(),
37            counter: view.counter.parse().unwrap_or(0),
38            rp_name: view.rp_name.clone(),
39            user_display_name: view.user_display_name.clone(),
40            discoverable: view.discoverable == "true",
41            creation_date: view.creation_date,
42        }
43    }
44}
45
46impl From<&Fido2CredentialDataV1> for Fido2CredentialFullView {
47    fn from(data: &Fido2CredentialDataV1) -> Self {
48        Self {
49            credential_id: data.credential_id.clone(),
50            key_type: data.key_type.clone(),
51            key_algorithm: data.key_algorithm.clone(),
52            key_curve: data.key_curve.clone(),
53            key_value: data.key_value.clone(),
54            rp_id: data.rp_id.clone(),
55            user_handle: data.user_handle.clone(),
56            user_name: data.user_name.clone(),
57            counter: data.counter.to_string(),
58            rp_name: data.rp_name.clone(),
59            user_display_name: data.user_display_name.clone(),
60            discoverable: data.discoverable.to_string(),
61            creation_date: data.creation_date,
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use bitwarden_crypto::{CompositeEncryptable, Decryptable};
69    use chrono::{TimeZone, Utc};
70
71    use super::super::{CipherBlobV1, CipherTypeDataV1, LoginUriDataV1, test_support::*};
72    use crate::cipher::{
73        cipher::CipherType,
74        field::{FieldType, FieldView},
75        linked_id::{LinkedIdType, LoginLinkedIdType},
76        login::{Fido2Credential, Fido2CredentialFullView, LoginUriView, LoginView, UriMatchType},
77    };
78
79    #[test]
80    fn test_login_uri_view_to_data_drops_checksum() {
81        let view = LoginUriView {
82            uri: Some("https://example.com".to_string()),
83            r#match: Some(UriMatchType::Domain),
84            uri_checksum: Some("some-checksum-value".to_string()),
85        };
86
87        let data = LoginUriDataV1::from(&view);
88
89        assert_eq!(data.uri, Some("https://example.com".to_string()));
90        assert_eq!(data.r#match, Some(UriMatchType::Domain));
91    }
92
93    #[test]
94    fn test_login_uri_data_to_view_sets_checksum_none() {
95        let data = LoginUriDataV1 {
96            uri: Some("https://example.com".to_string()),
97            r#match: Some(UriMatchType::Domain),
98        };
99
100        let view = LoginUriView::from(&data);
101
102        assert_eq!(view.uri, Some("https://example.com".to_string()));
103        assert_eq!(view.r#match, Some(UriMatchType::Domain));
104        assert_eq!(view.uri_checksum, None);
105    }
106
107    #[test]
108    fn test_fido2_counter_parsing() {
109        let full_view = Fido2CredentialFullView {
110            credential_id: "cred-id".to_string(),
111            key_type: "public-key".to_string(),
112            key_algorithm: "ECDSA".to_string(),
113            key_curve: "P-256".to_string(),
114            key_value: "key-value".to_string(),
115            rp_id: "example.com".to_string(),
116            user_handle: None,
117            user_name: None,
118            counter: "42".to_string(),
119            rp_name: None,
120            user_display_name: None,
121            discoverable: "true".to_string(),
122            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
123        };
124
125        let data = super::Fido2CredentialDataV1::from(&full_view);
126        assert_eq!(data.counter, 42);
127        assert!(data.discoverable);
128
129        let round_tripped = Fido2CredentialFullView::from(&data);
130        assert_eq!(round_tripped.counter, "42");
131        assert_eq!(round_tripped.discoverable, "true");
132    }
133
134    #[test]
135    fn test_fido2_counter_zero() {
136        let full_view = Fido2CredentialFullView {
137            credential_id: "cred-id".to_string(),
138            key_type: "public-key".to_string(),
139            key_algorithm: "ECDSA".to_string(),
140            key_curve: "P-256".to_string(),
141            key_value: "key-value".to_string(),
142            rp_id: "example.com".to_string(),
143            user_handle: None,
144            user_name: None,
145            counter: "0".to_string(),
146            rp_name: None,
147            user_display_name: None,
148            discoverable: "false".to_string(),
149            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
150        };
151
152        let data = super::Fido2CredentialDataV1::from(&full_view);
153        assert_eq!(data.counter, 0);
154        assert!(!data.discoverable);
155
156        let round_tripped = Fido2CredentialFullView::from(&data);
157        assert_eq!(round_tripped.counter, "0");
158        assert_eq!(round_tripped.discoverable, "false");
159    }
160
161    #[test]
162    fn test_fido2_counter_invalid_defaults_to_zero() {
163        let full_view = Fido2CredentialFullView {
164            credential_id: "cred-id".to_string(),
165            key_type: "public-key".to_string(),
166            key_algorithm: "ECDSA".to_string(),
167            key_curve: "P-256".to_string(),
168            key_value: "key-value".to_string(),
169            rp_id: "example.com".to_string(),
170            user_handle: None,
171            user_name: None,
172            counter: "not-a-number".to_string(),
173            rp_name: None,
174            user_display_name: None,
175            discoverable: "true".to_string(),
176            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
177        };
178
179        let data = super::Fido2CredentialDataV1::from(&full_view);
180        assert_eq!(data.counter, 0);
181    }
182
183    #[test]
184    fn test_login_cipher_round_trip() {
185        let (key_store, key_id) = create_test_key_store();
186        let mut ctx = key_store.context_mut();
187
188        // Create fido2 credentials by encrypting a FullView
189        let fido2_full = Fido2CredentialFullView {
190            credential_id: "cred-123".to_string(),
191            key_type: "public-key".to_string(),
192            key_algorithm: "ECDSA".to_string(),
193            key_curve: "P-256".to_string(),
194            key_value: "key-value-base64".to_string(),
195            rp_id: "example.com".to_string(),
196            user_handle: Some("user-handle".to_string()),
197            user_name: Some("testuser".to_string()),
198            counter: "42".to_string(),
199            rp_name: Some("Example".to_string()),
200            user_display_name: Some("Test User".to_string()),
201            discoverable: "true".to_string(),
202            creation_date: Utc.with_ymd_and_hms(2024, 6, 1, 10, 30, 0).unwrap(),
203        };
204        let encrypted_fido2: Fido2Credential =
205            fido2_full.encrypt_composite(&mut ctx, key_id).unwrap();
206
207        let original = crate::CipherView {
208            name: "My Login".to_string(),
209            notes: Some("Login notes".to_string()),
210            r#type: CipherType::Login,
211            login: Some(LoginView {
212                username: Some("[email protected]".to_string()),
213                password: Some("p@ssw0rd123".to_string()),
214                password_revision_date: Some(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap()),
215                uris: Some(vec![LoginUriView {
216                    uri: Some("https://example.com/login".to_string()),
217                    r#match: Some(UriMatchType::Domain),
218                    uri_checksum: Some("some-checksum".to_string()),
219                }]),
220                totp: Some("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP".to_string()),
221                autofill_on_page_load: Some(true),
222                fido2_credentials: Some(vec![encrypted_fido2]),
223            }),
224            fields: Some(vec![FieldView {
225                name: Some("Custom Field".to_string()),
226                value: Some("custom-value".to_string()),
227                r#type: FieldType::Linked,
228                linked_id: Some(LinkedIdType::Login(LoginLinkedIdType::Username)),
229            }]),
230            password_history: Some(vec![crate::PasswordHistoryView {
231                password: "old-password-1".to_string(),
232                last_used_date: Utc.with_ymd_and_hms(2023, 12, 1, 8, 0, 0).unwrap(),
233            }]),
234            ..create_shell_cipher_view(CipherType::Login)
235        };
236
237        let blob = CipherBlobV1::from_cipher_view(&original, &mut ctx, key_id).unwrap();
238
239        // Verify blob intermediate state
240        assert_eq!(blob.name, "My Login");
241        assert_eq!(blob.notes, Some("Login notes".to_string()));
242        if let CipherTypeDataV1::Login(ref login_data) = blob.type_data {
243            assert_eq!(
244                login_data.username,
245                Some("[email protected]".to_string())
246            );
247            assert_eq!(login_data.fido2_credentials.len(), 1);
248            assert_eq!(login_data.fido2_credentials[0].counter, 42);
249            assert!(login_data.fido2_credentials[0].discoverable);
250            // URI checksum should be dropped
251            assert_eq!(login_data.uris.len(), 1);
252        } else {
253            panic!("Expected Login type data");
254        }
255
256        // Round-trip back
257        let mut restored = create_shell_cipher_view(CipherType::Login);
258        blob.apply_to_cipher_view(&mut restored, &mut ctx, key_id)
259            .unwrap();
260
261        assert_eq!(restored.name, "My Login");
262        assert_eq!(restored.notes, Some("Login notes".to_string()));
263        assert_eq!(restored.r#type, CipherType::Login);
264
265        let login = restored.login.unwrap();
266        assert_eq!(login.username, Some("[email protected]".to_string()));
267        assert_eq!(login.password, Some("p@ssw0rd123".to_string()));
268        assert_eq!(
269            login.totp,
270            Some("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP".to_string())
271        );
272        assert_eq!(login.autofill_on_page_load, Some(true));
273
274        // URIs should round-trip but checksum is None
275        let uris = login.uris.unwrap();
276        assert_eq!(uris.len(), 1);
277        assert_eq!(uris[0].uri, Some("https://example.com/login".to_string()));
278        assert_eq!(uris[0].r#match, Some(UriMatchType::Domain));
279        assert_eq!(uris[0].uri_checksum, None);
280
281        // Fido2 credentials should be re-encrypted
282        let fido2 = login.fido2_credentials.unwrap();
283        assert_eq!(fido2.len(), 1);
284        // Decrypt to verify content survived the round-trip
285        let decrypted: Fido2CredentialFullView = fido2[0].decrypt(&mut ctx, key_id).unwrap();
286        assert_eq!(decrypted.credential_id, "cred-123");
287        assert_eq!(decrypted.counter, "42");
288        assert_eq!(decrypted.discoverable, "true");
289        assert_eq!(decrypted.rp_id, "example.com");
290
291        // Fields and password history
292        assert_eq!(restored.fields.as_ref().unwrap().len(), 1);
293        assert_eq!(restored.password_history.as_ref().unwrap().len(), 1);
294
295        assert!(restored.card.is_none());
296        assert!(restored.identity.is_none());
297        assert!(restored.secure_note.is_none());
298        assert!(restored.ssh_key.is_none());
299    }
300}