1use crate::encryption::EncryptorDecryptor;
11use crate::error::*;
12use crate::login::ValidateAndFixup;
13use crate::util::sanitize_timestamp;
14use crate::SecureLoginFields;
15use crate::{EncryptedLogin, LoginEntry, LoginFields, LoginMeta};
16use serde_derive::*;
17use sync15::bso::OutgoingBso;
18use sync_guid::Guid;
19
20type UnknownFields = serde_json::Map<String, serde_json::Value>;
21
22trait UnknownFieldsExt {
23 fn encrypt(&self, encdec: &dyn EncryptorDecryptor) -> Result<String>;
24 fn decrypt(ciphertext: &str, encdec: &dyn EncryptorDecryptor) -> Result<Self>
25 where
26 Self: Sized;
27}
28
29impl UnknownFieldsExt for UnknownFields {
30 fn encrypt(&self, encdec: &dyn EncryptorDecryptor) -> Result<String> {
31 let string = serde_json::to_string(&self)?;
32 let cipherbytes = encdec
33 .encrypt(string.as_bytes().into())
34 .map_err(|e| Error::EncryptionFailed(e.to_string()))?;
35 let ciphertext = std::str::from_utf8(&cipherbytes)
36 .map_err(|e| Error::EncryptionFailed(e.to_string()))?;
37 Ok(ciphertext.to_owned())
38 }
39
40 fn decrypt(ciphertext: &str, encdec: &dyn EncryptorDecryptor) -> Result<Self> {
41 let jsonbytes = encdec
42 .decrypt(ciphertext.as_bytes().into())
43 .map_err(|e| Error::DecryptionFailed(e.to_string()))?;
44 let json =
45 std::str::from_utf8(&jsonbytes).map_err(|e| Error::DecryptionFailed(e.to_string()))?;
46 Ok(serde_json::from_str(json)?)
47 }
48}
49
50#[derive(Debug)]
53pub(super) struct IncomingLogin {
54 pub login: EncryptedLogin,
55 pub unknown: Option<String>,
57}
58
59impl IncomingLogin {
60 pub fn guid(&self) -> Guid {
61 self.login.guid()
62 }
63
64 pub(super) fn from_incoming_payload(
65 p: LoginPayload,
66 encdec: &dyn EncryptorDecryptor,
67 ) -> Result<Self> {
68 let original_fields = LoginFields {
69 origin: p.hostname,
70 form_action_origin: p.form_submit_url,
71 http_realm: p.http_realm,
72 username_field: p.username_field,
73 password_field: p.password_field,
74 };
75 let original_sec_fields = SecureLoginFields {
76 username: p.username,
77 password: p.password,
78 };
79 let original_login_entry = LoginEntry::new(original_fields, original_sec_fields);
81 let login_entry = original_login_entry
82 .maybe_fixup()?
83 .unwrap_or(original_login_entry);
84 let fields = LoginFields {
85 origin: login_entry.origin,
86 form_action_origin: login_entry.form_action_origin,
87 http_realm: login_entry.http_realm,
88 username_field: login_entry.username_field,
89 password_field: login_entry.password_field,
90 };
91 let id = String::from(p.guid);
92 let sec_fields = SecureLoginFields {
93 username: login_entry.username,
94 password: login_entry.password,
95 }
96 .encrypt(encdec, &id)?;
97
98 let unknown = if p.unknown_fields.is_empty() {
101 None
102 } else {
103 Some(p.unknown_fields.encrypt(encdec)?)
104 };
105
106 Ok(Self {
108 login: EncryptedLogin {
109 meta: LoginMeta {
110 id,
111 time_created: p.time_created,
112 time_password_changed: p.time_password_changed,
113 time_last_used: p.time_last_used,
114 times_used: p.times_used,
115 time_last_breach_alert_dismissed: p.time_last_breach_alert_dismissed,
116 },
117 fields,
118 sec_fields,
119 },
120 unknown,
121 })
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, Default)]
127#[serde(rename_all = "camelCase")]
128pub struct LoginPayload {
129 #[serde(rename = "id")]
130 pub guid: Guid,
131
132 pub hostname: String,
134
135 #[serde(rename = "formSubmitURL")]
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub form_submit_url: Option<String>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub http_realm: Option<String>,
144
145 #[serde(default)]
146 pub username: String,
147
148 pub password: String,
149
150 #[serde(default)]
151 pub username_field: String,
152
153 #[serde(default)]
154 pub password_field: String,
155
156 #[serde(default)]
157 #[serde(deserialize_with = "deserialize_timestamp")]
158 pub time_created: i64,
159
160 #[serde(default)]
161 #[serde(deserialize_with = "deserialize_timestamp")]
162 pub time_password_changed: i64,
163
164 #[serde(default)]
165 #[serde(deserialize_with = "deserialize_timestamp")]
166 pub time_last_used: i64,
167
168 #[serde(default)]
169 pub times_used: i64,
170
171 #[serde(default)]
172 #[serde(deserialize_with = "deserialize_optional_timestamp")]
173 pub time_last_breach_alert_dismissed: Option<i64>,
174
175 #[serde(flatten)]
177 unknown_fields: UnknownFields,
178}
179
180impl EncryptedLogin {
182 pub fn into_bso(
183 self,
184 encdec: &dyn EncryptorDecryptor,
185 enc_unknown_fields: Option<String>,
186 ) -> Result<OutgoingBso> {
187 let unknown_fields = match enc_unknown_fields {
188 Some(s) => UnknownFields::decrypt(&s, encdec)?,
189 None => Default::default(),
190 };
191 let sec_fields = SecureLoginFields::decrypt(&self.sec_fields, encdec, &self.meta.id)?;
192 Ok(OutgoingBso::from_content_with_id(
193 crate::sync::LoginPayload {
194 guid: self.guid(),
195 hostname: self.fields.origin,
196 form_submit_url: self.fields.form_action_origin,
197 http_realm: self.fields.http_realm,
198 username_field: self.fields.username_field,
199 password_field: self.fields.password_field,
200 username: sec_fields.username,
201 password: sec_fields.password,
202 time_created: self.meta.time_created,
203 time_password_changed: self.meta.time_password_changed,
204 time_last_used: self.meta.time_last_used,
205 times_used: self.meta.times_used,
206 time_last_breach_alert_dismissed: self.meta.time_last_breach_alert_dismissed,
207 unknown_fields,
208 },
209 )?)
210 }
211}
212
213#[allow(clippy::unnecessary_wraps)]
215fn deserialize_timestamp<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
216where
217 D: serde::de::Deserializer<'de>,
218{
219 use serde::de::Deserialize;
220 Ok(sanitize_timestamp(
228 i64::deserialize(deserializer).unwrap_or_default(),
229 ))
230}
231
232#[allow(clippy::unnecessary_wraps)]
234fn deserialize_optional_timestamp<'de, D>(
235 deserializer: D,
236) -> std::result::Result<Option<i64>, D::Error>
237where
238 D: serde::de::Deserializer<'de>,
239{
240 use serde::de::Deserialize;
241 Ok(i64::deserialize(deserializer).ok().map(sanitize_timestamp))
242}
243
244#[cfg(not(feature = "keydb"))]
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::encryption::test_utils::{encrypt_struct, TEST_ENCDEC};
249 use crate::sync::merge::SyncLoginData;
250 use crate::{EncryptedLogin, LoginFields, LoginMeta, SecureLoginFields};
251 use sync15::bso::IncomingBso;
252
253 #[test]
254 fn test_payload_to_login() {
255 let bso = IncomingBso::from_test_content(serde_json::json!({
256 "id": "123412341234",
257 "httpRealm": "test",
258 "hostname": "https://www.example.com",
259 "username": "user",
260 "password": "password",
261 }));
262 let login = IncomingLogin::from_incoming_payload(
263 bso.into_content::<LoginPayload>().content().unwrap(),
264 &*TEST_ENCDEC,
265 )
266 .unwrap()
267 .login;
268 assert_eq!(login.meta.id, "123412341234");
269 assert_eq!(login.fields.http_realm, Some("test".to_string()));
270 assert_eq!(login.fields.origin, "https://www.example.com");
271 assert_eq!(login.fields.form_action_origin, None);
272 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
273 assert_eq!(sec_fields.username, "user");
274 assert_eq!(sec_fields.password, "password");
275 }
276
277 #[test]
280 fn test_payload_repairs_absurd_timestamps() {
281 let bso = IncomingBso::from_test_content(serde_json::json!({
282 "id": "123412341234",
283 "httpRealm": "test",
284 "hostname": "https://www.example.com",
285 "username": "user",
286 "password": "password",
287 "timeCreated": 18446744071857664i64,
289 "timePasswordChanged": 18446744073217880i64,
290 "timeLastUsed": -1,
291 "timeLastBreachAlertDismissed": i64::MAX,
292 }));
293 let login = IncomingLogin::from_incoming_payload(
294 bso.into_content::<LoginPayload>().content().unwrap(),
295 &*TEST_ENCDEC,
296 )
297 .unwrap()
298 .login;
299
300 assert_eq!(login.meta.time_created, 0);
301 assert_eq!(login.meta.time_password_changed, 0);
302 assert_eq!(login.meta.time_last_used, 0);
303 assert_eq!(login.meta.time_last_breach_alert_dismissed, Some(0));
304 }
305
306 #[test]
310 fn test_payload_empty_form_action_to_login() {
311 let bso = IncomingBso::from_test_content(serde_json::json!({
312 "id": "123412341234",
313 "formSubmitURL": "",
314 "hostname": "https://www.example.com",
315 "username": "user",
316 "password": "password",
317 }));
318 let login = IncomingLogin::from_incoming_payload(
319 bso.into_content::<LoginPayload>().content().unwrap(),
320 &*TEST_ENCDEC,
321 )
322 .unwrap()
323 .login;
324 assert_eq!(login.meta.id, "123412341234");
325 assert_eq!(login.fields.form_action_origin, Some("".to_string()));
326 assert_eq!(login.fields.http_realm, None);
327 assert_eq!(login.fields.origin, "https://www.example.com");
328 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
329 assert_eq!(sec_fields.username, "user");
330 assert_eq!(sec_fields.password, "password");
331
332 let bso = login.into_bso(&*TEST_ENCDEC, None).unwrap();
333 assert_eq!(bso.envelope.id, "123412341234");
334 let payload_data: serde_json::Value = serde_json::from_str(&bso.payload).unwrap();
335 assert_eq!(payload_data["httpRealm"], serde_json::Value::Null);
336 assert_eq!(payload_data["formSubmitURL"], "".to_string());
337 }
338
339 #[test]
340 fn test_payload_unknown_fields() {
341 let bso = IncomingBso::from_test_content(serde_json::json!({
343 "id": "123412341234",
344 "httpRealm": "test",
345 "hostname": "https://www.example.com",
346 "username": "user",
347 "password": "password",
348 }));
349 let payload = bso.into_content::<LoginPayload>().content().unwrap();
350 assert!(payload.unknown_fields.is_empty());
351
352 let bso = IncomingBso::from_test_content(serde_json::json!({
354 "id": "123412341234",
355 "httpRealm": "test",
356 "hostname": "https://www.example.com",
357 "username": "user",
358 "password": "password",
359 "foo": "bar",
360 }));
361 let payload = bso.into_content::<LoginPayload>().content().unwrap();
362 assert_eq!(payload.unknown_fields.len(), 1);
363 assert_eq!(
364 payload.unknown_fields.get("foo").unwrap().as_str().unwrap(),
365 "bar"
366 );
367 let unknown = Some(encrypt_struct::<UnknownFields>(&payload.unknown_fields));
369 let login = IncomingLogin::from_incoming_payload(payload, &*TEST_ENCDEC)
370 .unwrap()
371 .login;
372 let outgoing = login.into_bso(&*TEST_ENCDEC, unknown).unwrap();
374 let json =
375 serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&outgoing.payload)
376 .unwrap();
377 assert_eq!(json.get("foo").unwrap().as_str().unwrap(), "bar");
378 }
379
380 #[test]
381 fn test_form_submit_payload_to_login() {
382 let bso = IncomingBso::from_test_content(serde_json::json!({
383 "id": "123412341234",
384 "hostname": "https://www.example.com",
385 "formSubmitURL": "https://www.example.com",
386 "usernameField": "username-field",
387 "username": "user",
388 "password": "password",
389 }));
390 let login = IncomingLogin::from_incoming_payload(
391 bso.into_content::<LoginPayload>().content().unwrap(),
392 &*TEST_ENCDEC,
393 )
394 .unwrap()
395 .login;
396 assert_eq!(login.meta.id, "123412341234");
397 assert_eq!(login.fields.http_realm, None);
398 assert_eq!(login.fields.origin, "https://www.example.com");
399 assert_eq!(
400 login.fields.form_action_origin,
401 Some("https://www.example.com".to_string())
402 );
403 assert_eq!(login.fields.username_field, "username-field");
404 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
405 assert_eq!(sec_fields.username, "user");
406 assert_eq!(sec_fields.password, "password");
407 }
408
409 #[test]
410 fn test_login_into_payload() {
411 let login = EncryptedLogin {
412 meta: LoginMeta {
413 id: "123412341234".into(),
414 ..Default::default()
415 },
416 fields: LoginFields {
417 http_realm: Some("test".into()),
418 origin: "https://www.example.com".into(),
419 ..Default::default()
420 },
421 sec_fields: encrypt_struct(&SecureLoginFields {
422 username: "user".into(),
423 password: "password".into(),
424 }),
425 };
426 let bso = login.into_bso(&*TEST_ENCDEC, None).unwrap();
427 assert_eq!(bso.envelope.id, "123412341234");
428 let payload_data: serde_json::Value = serde_json::from_str(&bso.payload).unwrap();
429 assert_eq!(payload_data["httpRealm"], "test".to_string());
430 assert_eq!(payload_data["hostname"], "https://www.example.com");
431 assert_eq!(payload_data["username"], "user");
432 assert_eq!(payload_data["password"], "password");
433 assert!(matches!(
434 payload_data["formActionOrigin"],
435 serde_json::Value::Null
436 ));
437 }
438
439 #[test]
440 fn test_username_field_requires_a_form_target() {
441 let bad_json = serde_json::json!({
442 "id": "123412341234",
443 "httpRealm": "test",
444 "hostname": "https://www.example.com",
445 "username": "test",
446 "password": "test",
447 "usernameField": "invalid"
448 });
449 let bad_bso = IncomingBso::from_test_content(bad_json.clone());
450
451 let login = IncomingLogin::from_incoming_payload(
453 bad_bso.into_content::<LoginPayload>().content().unwrap(),
454 &*TEST_ENCDEC,
455 )
456 .unwrap()
457 .login;
458 assert_eq!(login.fields.username_field, "");
459
460 let bad_bso = IncomingBso::from_test_content(bad_json);
462 let login = SyncLoginData::from_bso(bad_bso, &*TEST_ENCDEC)
463 .unwrap()
464 .inbound
465 .unwrap()
466 .login;
467 assert_eq!(login.fields.username_field, "");
468 }
469
470 #[test]
471 fn test_password_field_requires_a_form_target() {
472 let bad_bso = IncomingBso::from_test_content(serde_json::json!({
473 "id": "123412341234",
474 "httpRealm": "test",
475 "hostname": "https://www.example.com",
476 "username": "test",
477 "password": "test",
478 "passwordField": "invalid"
479 }));
480
481 let login = IncomingLogin::from_incoming_payload(
482 bad_bso.into_content::<LoginPayload>().content().unwrap(),
483 &*TEST_ENCDEC,
484 )
485 .unwrap()
486 .login;
487 assert_eq!(login.fields.password_field, "");
488 }
489}