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 chrono::{TimeZone, Utc};
69
70    use super::super::{CipherBlobV1, CipherTypeDataV1, LoginUriDataV1, test_support::*};
71    use crate::cipher::{
72        cipher::CipherType,
73        field::{FieldType, FieldView},
74        linked_id::{LinkedIdType, LoginLinkedIdType},
75        login::{
76            Fido2CredentialFullView, Fido2CredentialView, LoginUriView, LoginView, UriMatchType,
77        },
78    };
79
80    #[test]
81    fn test_login_uri_view_to_data_drops_checksum() {
82        let view = LoginUriView {
83            uri: Some("https://example.com".to_string()),
84            r#match: Some(UriMatchType::Domain),
85            uri_checksum: Some("some-checksum-value".to_string()),
86        };
87
88        let data = LoginUriDataV1::from(&view);
89
90        assert_eq!(data.uri, Some("https://example.com".to_string()));
91        assert_eq!(data.r#match, Some(UriMatchType::Domain));
92    }
93
94    #[test]
95    fn test_login_uri_data_to_view_sets_checksum_none() {
96        let data = LoginUriDataV1 {
97            uri: Some("https://example.com".to_string()),
98            r#match: Some(UriMatchType::Domain),
99        };
100
101        let view = LoginUriView::from(&data);
102
103        assert_eq!(view.uri, Some("https://example.com".to_string()));
104        assert_eq!(view.r#match, Some(UriMatchType::Domain));
105        assert_eq!(view.uri_checksum, None);
106    }
107
108    #[test]
109    fn test_fido2_counter_parsing() {
110        let full_view = Fido2CredentialFullView {
111            credential_id: "cred-id".to_string(),
112            key_type: "public-key".to_string(),
113            key_algorithm: "ECDSA".to_string(),
114            key_curve: "P-256".to_string(),
115            key_value: "key-value".to_string(),
116            rp_id: "example.com".to_string(),
117            user_handle: None,
118            user_name: None,
119            counter: "42".to_string(),
120            rp_name: None,
121            user_display_name: None,
122            discoverable: "true".to_string(),
123            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
124        };
125
126        let data = super::Fido2CredentialDataV1::from(&full_view);
127        assert_eq!(data.counter, 42);
128        assert!(data.discoverable);
129
130        let round_tripped = Fido2CredentialFullView::from(&data);
131        assert_eq!(round_tripped.counter, "42");
132        assert_eq!(round_tripped.discoverable, "true");
133    }
134
135    #[test]
136    fn test_fido2_counter_zero() {
137        let full_view = Fido2CredentialFullView {
138            credential_id: "cred-id".to_string(),
139            key_type: "public-key".to_string(),
140            key_algorithm: "ECDSA".to_string(),
141            key_curve: "P-256".to_string(),
142            key_value: "key-value".to_string(),
143            rp_id: "example.com".to_string(),
144            user_handle: None,
145            user_name: None,
146            counter: "0".to_string(),
147            rp_name: None,
148            user_display_name: None,
149            discoverable: "false".to_string(),
150            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
151        };
152
153        let data = super::Fido2CredentialDataV1::from(&full_view);
154        assert_eq!(data.counter, 0);
155        assert!(!data.discoverable);
156
157        let round_tripped = Fido2CredentialFullView::from(&data);
158        assert_eq!(round_tripped.counter, "0");
159        assert_eq!(round_tripped.discoverable, "false");
160    }
161
162    #[test]
163    fn test_fido2_counter_invalid_defaults_to_zero() {
164        let full_view = Fido2CredentialFullView {
165            credential_id: "cred-id".to_string(),
166            key_type: "public-key".to_string(),
167            key_algorithm: "ECDSA".to_string(),
168            key_curve: "P-256".to_string(),
169            key_value: "key-value".to_string(),
170            rp_id: "example.com".to_string(),
171            user_handle: None,
172            user_name: None,
173            counter: "not-a-number".to_string(),
174            rp_name: None,
175            user_display_name: None,
176            discoverable: "true".to_string(),
177            creation_date: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
178        };
179
180        let data = super::Fido2CredentialDataV1::from(&full_view);
181        assert_eq!(data.counter, 0);
182    }
183
184    #[test]
185    fn test_login_cipher_round_trip() {
186        // Create a decrypted fido2 credential view
187        let fido2_view = Fido2CredentialView {
188            credential_id: "cred-123".to_string(),
189            key_type: "public-key".to_string(),
190            key_algorithm: "ECDSA".to_string(),
191            key_curve: "P-256".to_string(),
192            key_value: "key-value-base64".to_string(),
193            rp_id: "example.com".to_string(),
194            user_handle: Some("user-handle".to_string()),
195            user_name: Some("testuser".to_string()),
196            counter: "42".to_string(),
197            rp_name: Some("Example".to_string()),
198            user_display_name: Some("Test User".to_string()),
199            discoverable: "true".to_string(),
200            creation_date: Utc.with_ymd_and_hms(2024, 6, 1, 10, 30, 0).unwrap(),
201        };
202
203        let original = crate::CipherView {
204            name: "My Login".to_string(),
205            notes: Some("Login notes".to_string()),
206            r#type: CipherType::Login,
207            login: Some(LoginView {
208                username: Some("[email protected]".to_string()),
209                password: Some("p@ssw0rd123".to_string()),
210                password_revision_date: Some(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap()),
211                uris: Some(vec![LoginUriView {
212                    uri: Some("https://example.com/login".to_string()),
213                    r#match: Some(UriMatchType::Domain),
214                    uri_checksum: Some("some-checksum".to_string()),
215                }]),
216                totp: Some("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP".to_string()),
217                autofill_on_page_load: Some(true),
218                fido2_credentials: Some(vec![fido2_view]),
219            }),
220            fields: Some(vec![FieldView {
221                name: Some("Custom Field".to_string()),
222                value: Some("custom-value".to_string()),
223                r#type: FieldType::Linked,
224                linked_id: Some(LinkedIdType::Login(LoginLinkedIdType::Username)),
225            }]),
226            password_history: Some(vec![crate::PasswordHistoryView {
227                password: "old-password-1".to_string(),
228                last_used_date: Utc.with_ymd_and_hms(2023, 12, 1, 8, 0, 0).unwrap(),
229            }]),
230            ..create_shell_cipher_view(CipherType::Login)
231        };
232
233        let blob = CipherBlobV1::from_cipher_view(&original).unwrap();
234
235        // Verify blob intermediate state
236        assert_eq!(blob.name, "My Login");
237        assert_eq!(blob.notes, Some("Login notes".to_string()));
238        if let CipherTypeDataV1::Login(ref login_data) = blob.type_data {
239            assert_eq!(
240                login_data.username,
241                Some("[email protected]".to_string())
242            );
243            assert_eq!(login_data.fido2_credentials.len(), 1);
244            assert_eq!(login_data.fido2_credentials[0].counter, 42);
245            assert!(login_data.fido2_credentials[0].discoverable);
246            // URI checksum should be dropped
247            assert_eq!(login_data.uris.len(), 1);
248        } else {
249            panic!("Expected Login type data");
250        }
251
252        // Round-trip back
253        let mut restored = create_shell_cipher_view(CipherType::Login);
254        blob.apply_to_cipher_view(&mut restored).unwrap();
255
256        assert_eq!(restored.name, "My Login");
257        assert_eq!(restored.notes, Some("Login notes".to_string()));
258        assert_eq!(restored.r#type, CipherType::Login);
259
260        let login = restored.login.unwrap();
261        assert_eq!(login.username, Some("[email protected]".to_string()));
262        assert_eq!(login.password, Some("p@ssw0rd123".to_string()));
263        assert_eq!(
264            login.totp,
265            Some("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP".to_string())
266        );
267        assert_eq!(login.autofill_on_page_load, Some(true));
268
269        // URIs should round-trip but checksum is None
270        let uris = login.uris.unwrap();
271        assert_eq!(uris.len(), 1);
272        assert_eq!(uris[0].uri, Some("https://example.com/login".to_string()));
273        assert_eq!(uris[0].r#match, Some(UriMatchType::Domain));
274        assert_eq!(uris[0].uri_checksum, None);
275
276        // Fido2 credentials should round-trip as decrypted views
277        let fido2 = login.fido2_credentials.unwrap();
278        assert_eq!(fido2.len(), 1);
279        assert_eq!(fido2[0].credential_id, "cred-123");
280        assert_eq!(fido2[0].counter, "42");
281        assert_eq!(fido2[0].discoverable, "true");
282        assert_eq!(fido2[0].rp_id, "example.com");
283
284        // Fields and password history
285        assert_eq!(restored.fields.as_ref().unwrap().len(), 1);
286        assert_eq!(restored.password_history.as_ref().unwrap().len(), 1);
287
288        assert!(restored.card.is_none());
289        assert!(restored.identity.is_none());
290        assert!(restored.secure_note.is_none());
291        assert!(restored.ssh_key.is_none());
292    }
293}