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