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 };
74 let original_sec_fields = SecureLoginFields {
75 username: p.username,
76 password: p.password,
77 };
78 let original_login_entry = LoginEntry::new(original_fields, original_sec_fields);
80 let login_entry = original_login_entry
81 .maybe_fixup()?
82 .unwrap_or(original_login_entry);
83 let fields = LoginFields {
84 origin: login_entry.origin,
85 form_action_origin: login_entry.form_action_origin,
86 http_realm: login_entry.http_realm,
87 username_field: login_entry.username_field,
88 password_field: login_entry.password_field,
89 };
90 let id = String::from(p.guid);
91 let sec_fields = SecureLoginFields {
92 username: login_entry.username,
93 password: login_entry.password,
94 }
95 .encrypt(encdec, &id)?;
96
97 let unknown = if p.unknown_fields.is_empty() {
100 None
101 } else {
102 Some(p.unknown_fields.encrypt(encdec)?)
103 };
104
105 Ok(Self {
107 login: EncryptedLogin {
108 meta: LoginMeta {
109 id,
110 time_created: p.time_created,
111 time_password_changed: p.time_password_changed,
112 time_last_used: p.time_last_used,
113 times_used: p.times_used,
114 time_of_last_breach: p.time_of_last_breach,
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_of_last_breach: Option<i64>,
174
175 #[serde(default)]
176 #[serde(deserialize_with = "deserialize_optional_timestamp")]
177 pub time_last_breach_alert_dismissed: Option<i64>,
178
179 #[serde(flatten)]
181 unknown_fields: UnknownFields,
182}
183
184impl EncryptedLogin {
186 pub fn into_bso(
187 self,
188 encdec: &dyn EncryptorDecryptor,
189 enc_unknown_fields: Option<String>,
190 ) -> Result<OutgoingBso> {
191 let unknown_fields = match enc_unknown_fields {
192 Some(s) => UnknownFields::decrypt(&s, encdec)?,
193 None => Default::default(),
194 };
195 let sec_fields = SecureLoginFields::decrypt(&self.sec_fields, encdec, &self.meta.id)?;
196 Ok(OutgoingBso::from_content_with_id(
197 crate::sync::LoginPayload {
198 guid: self.guid(),
199 hostname: self.fields.origin,
200 form_submit_url: self.fields.form_action_origin,
201 http_realm: self.fields.http_realm,
202 username_field: self.fields.username_field,
203 password_field: self.fields.password_field,
204 username: sec_fields.username,
205 password: sec_fields.password,
206 time_created: self.meta.time_created,
207 time_password_changed: self.meta.time_password_changed,
208 time_last_used: self.meta.time_last_used,
209 times_used: self.meta.times_used,
210 time_of_last_breach: self.meta.time_of_last_breach,
211 time_last_breach_alert_dismissed: self.meta.time_last_breach_alert_dismissed,
212 unknown_fields,
213 },
214 )?)
215 }
216}
217
218#[allow(clippy::unnecessary_wraps)]
220fn deserialize_timestamp<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
221where
222 D: serde::de::Deserializer<'de>,
223{
224 use serde::de::Deserialize;
225 Ok(i64::deserialize(deserializer).unwrap_or_default().max(0))
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())
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]
281 fn test_payload_empty_form_action_to_login() {
282 let bso = IncomingBso::from_test_content(serde_json::json!({
283 "id": "123412341234",
284 "formSubmitURL": "",
285 "hostname": "https://www.example.com",
286 "username": "user",
287 "password": "password",
288 }));
289 let login = IncomingLogin::from_incoming_payload(
290 bso.into_content::<LoginPayload>().content().unwrap(),
291 &*TEST_ENCDEC,
292 )
293 .unwrap()
294 .login;
295 assert_eq!(login.meta.id, "123412341234");
296 assert_eq!(login.fields.form_action_origin, Some("".to_string()));
297 assert_eq!(login.fields.http_realm, None);
298 assert_eq!(login.fields.origin, "https://www.example.com");
299 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
300 assert_eq!(sec_fields.username, "user");
301 assert_eq!(sec_fields.password, "password");
302
303 let bso = login.into_bso(&*TEST_ENCDEC, None).unwrap();
304 assert_eq!(bso.envelope.id, "123412341234");
305 let payload_data: serde_json::Value = serde_json::from_str(&bso.payload).unwrap();
306 assert_eq!(payload_data["httpRealm"], serde_json::Value::Null);
307 assert_eq!(payload_data["formSubmitURL"], "".to_string());
308 }
309
310 #[test]
311 fn test_payload_unknown_fields() {
312 let bso = IncomingBso::from_test_content(serde_json::json!({
314 "id": "123412341234",
315 "httpRealm": "test",
316 "hostname": "https://www.example.com",
317 "username": "user",
318 "password": "password",
319 }));
320 let payload = bso.into_content::<LoginPayload>().content().unwrap();
321 assert!(payload.unknown_fields.is_empty());
322
323 let bso = IncomingBso::from_test_content(serde_json::json!({
325 "id": "123412341234",
326 "httpRealm": "test",
327 "hostname": "https://www.example.com",
328 "username": "user",
329 "password": "password",
330 "foo": "bar",
331 }));
332 let payload = bso.into_content::<LoginPayload>().content().unwrap();
333 assert_eq!(payload.unknown_fields.len(), 1);
334 assert_eq!(
335 payload.unknown_fields.get("foo").unwrap().as_str().unwrap(),
336 "bar"
337 );
338 let unknown = Some(encrypt_struct::<UnknownFields>(&payload.unknown_fields));
340 let login = IncomingLogin::from_incoming_payload(payload, &*TEST_ENCDEC)
341 .unwrap()
342 .login;
343 let outgoing = login.into_bso(&*TEST_ENCDEC, unknown).unwrap();
345 let json =
346 serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&outgoing.payload)
347 .unwrap();
348 assert_eq!(json.get("foo").unwrap().as_str().unwrap(), "bar");
349 }
350
351 #[test]
352 fn test_form_submit_payload_to_login() {
353 let bso = IncomingBso::from_test_content(serde_json::json!({
354 "id": "123412341234",
355 "hostname": "https://www.example.com",
356 "formSubmitURL": "https://www.example.com",
357 "usernameField": "username-field",
358 "username": "user",
359 "password": "password",
360 }));
361 let login = IncomingLogin::from_incoming_payload(
362 bso.into_content::<LoginPayload>().content().unwrap(),
363 &*TEST_ENCDEC,
364 )
365 .unwrap()
366 .login;
367 assert_eq!(login.meta.id, "123412341234");
368 assert_eq!(login.fields.http_realm, None);
369 assert_eq!(login.fields.origin, "https://www.example.com");
370 assert_eq!(
371 login.fields.form_action_origin,
372 Some("https://www.example.com".to_string())
373 );
374 assert_eq!(login.fields.username_field, "username-field");
375 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
376 assert_eq!(sec_fields.username, "user");
377 assert_eq!(sec_fields.password, "password");
378 }
379
380 #[test]
381 fn test_login_into_payload() {
382 let login = EncryptedLogin {
383 meta: LoginMeta {
384 id: "123412341234".into(),
385 ..Default::default()
386 },
387 fields: LoginFields {
388 http_realm: Some("test".into()),
389 origin: "https://www.example.com".into(),
390 ..Default::default()
391 },
392 sec_fields: encrypt_struct(&SecureLoginFields {
393 username: "user".into(),
394 password: "password".into(),
395 }),
396 };
397 let bso = login.into_bso(&*TEST_ENCDEC, None).unwrap();
398 assert_eq!(bso.envelope.id, "123412341234");
399 let payload_data: serde_json::Value = serde_json::from_str(&bso.payload).unwrap();
400 assert_eq!(payload_data["httpRealm"], "test".to_string());
401 assert_eq!(payload_data["hostname"], "https://www.example.com");
402 assert_eq!(payload_data["username"], "user");
403 assert_eq!(payload_data["password"], "password");
404 assert!(matches!(
405 payload_data["formActionOrigin"],
406 serde_json::Value::Null
407 ));
408 }
409
410 #[test]
411 fn test_username_field_requires_a_form_target() {
412 let bad_json = serde_json::json!({
413 "id": "123412341234",
414 "httpRealm": "test",
415 "hostname": "https://www.example.com",
416 "username": "test",
417 "password": "test",
418 "usernameField": "invalid"
419 });
420 let bad_bso = IncomingBso::from_test_content(bad_json.clone());
421
422 let login = IncomingLogin::from_incoming_payload(
424 bad_bso.into_content::<LoginPayload>().content().unwrap(),
425 &*TEST_ENCDEC,
426 )
427 .unwrap()
428 .login;
429 assert_eq!(login.fields.username_field, "");
430
431 let bad_bso = IncomingBso::from_test_content(bad_json);
433 let login = SyncLoginData::from_bso(bad_bso, &*TEST_ENCDEC)
434 .unwrap()
435 .inbound
436 .unwrap()
437 .login;
438 assert_eq!(login.fields.username_field, "");
439 }
440
441 #[test]
442 fn test_password_field_requires_a_form_target() {
443 let bad_bso = IncomingBso::from_test_content(serde_json::json!({
444 "id": "123412341234",
445 "httpRealm": "test",
446 "hostname": "https://www.example.com",
447 "username": "test",
448 "password": "test",
449 "passwordField": "invalid"
450 }));
451
452 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.password_field, "");
459 }
460}