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_last_breach_alert_dismissed: p.time_last_breach_alert_dismissed,
115 },
116 fields,
117 sec_fields,
118 },
119 unknown,
120 })
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
126#[serde(rename_all = "camelCase")]
127pub struct LoginPayload {
128 #[serde(rename = "id")]
129 pub guid: Guid,
130
131 pub hostname: String,
133
134 #[serde(rename = "formSubmitURL")]
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub form_submit_url: Option<String>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub http_realm: Option<String>,
143
144 #[serde(default)]
145 pub username: String,
146
147 pub password: String,
148
149 #[serde(default)]
150 pub username_field: String,
151
152 #[serde(default)]
153 pub password_field: String,
154
155 #[serde(default)]
156 #[serde(deserialize_with = "deserialize_timestamp")]
157 pub time_created: i64,
158
159 #[serde(default)]
160 #[serde(deserialize_with = "deserialize_timestamp")]
161 pub time_password_changed: i64,
162
163 #[serde(default)]
164 #[serde(deserialize_with = "deserialize_timestamp")]
165 pub time_last_used: i64,
166
167 #[serde(default)]
168 pub times_used: i64,
169
170 #[serde(default)]
171 #[serde(deserialize_with = "deserialize_optional_timestamp")]
172 pub time_last_breach_alert_dismissed: Option<i64>,
173
174 #[serde(flatten)]
176 unknown_fields: UnknownFields,
177}
178
179impl EncryptedLogin {
181 pub fn into_bso(
182 self,
183 encdec: &dyn EncryptorDecryptor,
184 enc_unknown_fields: Option<String>,
185 ) -> Result<OutgoingBso> {
186 let unknown_fields = match enc_unknown_fields {
187 Some(s) => UnknownFields::decrypt(&s, encdec)?,
188 None => Default::default(),
189 };
190 let sec_fields = SecureLoginFields::decrypt(&self.sec_fields, encdec, &self.meta.id)?;
191 Ok(OutgoingBso::from_content_with_id(
192 crate::sync::LoginPayload {
193 guid: self.guid(),
194 hostname: self.fields.origin,
195 form_submit_url: self.fields.form_action_origin,
196 http_realm: self.fields.http_realm,
197 username_field: self.fields.username_field,
198 password_field: self.fields.password_field,
199 username: sec_fields.username,
200 password: sec_fields.password,
201 time_created: self.meta.time_created,
202 time_password_changed: self.meta.time_password_changed,
203 time_last_used: self.meta.time_last_used,
204 times_used: self.meta.times_used,
205 time_last_breach_alert_dismissed: self.meta.time_last_breach_alert_dismissed,
206 unknown_fields,
207 },
208 )?)
209 }
210}
211
212#[allow(clippy::unnecessary_wraps)]
214fn deserialize_timestamp<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
215where
216 D: serde::de::Deserializer<'de>,
217{
218 use serde::de::Deserialize;
219 Ok(i64::deserialize(deserializer).unwrap_or_default().max(0))
224}
225
226#[allow(clippy::unnecessary_wraps)]
228fn deserialize_optional_timestamp<'de, D>(
229 deserializer: D,
230) -> std::result::Result<Option<i64>, D::Error>
231where
232 D: serde::de::Deserializer<'de>,
233{
234 use serde::de::Deserialize;
235 Ok(i64::deserialize(deserializer).ok())
236}
237
238#[cfg(not(feature = "keydb"))]
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::encryption::test_utils::{encrypt_struct, TEST_ENCDEC};
243 use crate::sync::merge::SyncLoginData;
244 use crate::{EncryptedLogin, LoginFields, LoginMeta, SecureLoginFields};
245 use sync15::bso::IncomingBso;
246
247 #[test]
248 fn test_payload_to_login() {
249 let bso = IncomingBso::from_test_content(serde_json::json!({
250 "id": "123412341234",
251 "httpRealm": "test",
252 "hostname": "https://www.example.com",
253 "username": "user",
254 "password": "password",
255 }));
256 let login = IncomingLogin::from_incoming_payload(
257 bso.into_content::<LoginPayload>().content().unwrap(),
258 &*TEST_ENCDEC,
259 )
260 .unwrap()
261 .login;
262 assert_eq!(login.meta.id, "123412341234");
263 assert_eq!(login.fields.http_realm, Some("test".to_string()));
264 assert_eq!(login.fields.origin, "https://www.example.com");
265 assert_eq!(login.fields.form_action_origin, None);
266 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
267 assert_eq!(sec_fields.username, "user");
268 assert_eq!(sec_fields.password, "password");
269 }
270
271 #[test]
275 fn test_payload_empty_form_action_to_login() {
276 let bso = IncomingBso::from_test_content(serde_json::json!({
277 "id": "123412341234",
278 "formSubmitURL": "",
279 "hostname": "https://www.example.com",
280 "username": "user",
281 "password": "password",
282 }));
283 let login = IncomingLogin::from_incoming_payload(
284 bso.into_content::<LoginPayload>().content().unwrap(),
285 &*TEST_ENCDEC,
286 )
287 .unwrap()
288 .login;
289 assert_eq!(login.meta.id, "123412341234");
290 assert_eq!(login.fields.form_action_origin, Some("".to_string()));
291 assert_eq!(login.fields.http_realm, None);
292 assert_eq!(login.fields.origin, "https://www.example.com");
293 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
294 assert_eq!(sec_fields.username, "user");
295 assert_eq!(sec_fields.password, "password");
296
297 let bso = login.into_bso(&*TEST_ENCDEC, None).unwrap();
298 assert_eq!(bso.envelope.id, "123412341234");
299 let payload_data: serde_json::Value = serde_json::from_str(&bso.payload).unwrap();
300 assert_eq!(payload_data["httpRealm"], serde_json::Value::Null);
301 assert_eq!(payload_data["formSubmitURL"], "".to_string());
302 }
303
304 #[test]
305 fn test_payload_unknown_fields() {
306 let bso = IncomingBso::from_test_content(serde_json::json!({
308 "id": "123412341234",
309 "httpRealm": "test",
310 "hostname": "https://www.example.com",
311 "username": "user",
312 "password": "password",
313 }));
314 let payload = bso.into_content::<LoginPayload>().content().unwrap();
315 assert!(payload.unknown_fields.is_empty());
316
317 let bso = IncomingBso::from_test_content(serde_json::json!({
319 "id": "123412341234",
320 "httpRealm": "test",
321 "hostname": "https://www.example.com",
322 "username": "user",
323 "password": "password",
324 "foo": "bar",
325 }));
326 let payload = bso.into_content::<LoginPayload>().content().unwrap();
327 assert_eq!(payload.unknown_fields.len(), 1);
328 assert_eq!(
329 payload.unknown_fields.get("foo").unwrap().as_str().unwrap(),
330 "bar"
331 );
332 let unknown = Some(encrypt_struct::<UnknownFields>(&payload.unknown_fields));
334 let login = IncomingLogin::from_incoming_payload(payload, &*TEST_ENCDEC)
335 .unwrap()
336 .login;
337 let outgoing = login.into_bso(&*TEST_ENCDEC, unknown).unwrap();
339 let json =
340 serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&outgoing.payload)
341 .unwrap();
342 assert_eq!(json.get("foo").unwrap().as_str().unwrap(), "bar");
343 }
344
345 #[test]
346 fn test_form_submit_payload_to_login() {
347 let bso = IncomingBso::from_test_content(serde_json::json!({
348 "id": "123412341234",
349 "hostname": "https://www.example.com",
350 "formSubmitURL": "https://www.example.com",
351 "usernameField": "username-field",
352 "username": "user",
353 "password": "password",
354 }));
355 let login = IncomingLogin::from_incoming_payload(
356 bso.into_content::<LoginPayload>().content().unwrap(),
357 &*TEST_ENCDEC,
358 )
359 .unwrap()
360 .login;
361 assert_eq!(login.meta.id, "123412341234");
362 assert_eq!(login.fields.http_realm, None);
363 assert_eq!(login.fields.origin, "https://www.example.com");
364 assert_eq!(
365 login.fields.form_action_origin,
366 Some("https://www.example.com".to_string())
367 );
368 assert_eq!(login.fields.username_field, "username-field");
369 let sec_fields = login.decrypt_fields(&*TEST_ENCDEC).unwrap();
370 assert_eq!(sec_fields.username, "user");
371 assert_eq!(sec_fields.password, "password");
372 }
373
374 #[test]
375 fn test_login_into_payload() {
376 let login = EncryptedLogin {
377 meta: LoginMeta {
378 id: "123412341234".into(),
379 ..Default::default()
380 },
381 fields: LoginFields {
382 http_realm: Some("test".into()),
383 origin: "https://www.example.com".into(),
384 ..Default::default()
385 },
386 sec_fields: encrypt_struct(&SecureLoginFields {
387 username: "user".into(),
388 password: "password".into(),
389 }),
390 };
391 let bso = login.into_bso(&*TEST_ENCDEC, None).unwrap();
392 assert_eq!(bso.envelope.id, "123412341234");
393 let payload_data: serde_json::Value = serde_json::from_str(&bso.payload).unwrap();
394 assert_eq!(payload_data["httpRealm"], "test".to_string());
395 assert_eq!(payload_data["hostname"], "https://www.example.com");
396 assert_eq!(payload_data["username"], "user");
397 assert_eq!(payload_data["password"], "password");
398 assert!(matches!(
399 payload_data["formActionOrigin"],
400 serde_json::Value::Null
401 ));
402 }
403
404 #[test]
405 fn test_username_field_requires_a_form_target() {
406 let bad_json = serde_json::json!({
407 "id": "123412341234",
408 "httpRealm": "test",
409 "hostname": "https://www.example.com",
410 "username": "test",
411 "password": "test",
412 "usernameField": "invalid"
413 });
414 let bad_bso = IncomingBso::from_test_content(bad_json.clone());
415
416 let login = IncomingLogin::from_incoming_payload(
418 bad_bso.into_content::<LoginPayload>().content().unwrap(),
419 &*TEST_ENCDEC,
420 )
421 .unwrap()
422 .login;
423 assert_eq!(login.fields.username_field, "");
424
425 let bad_bso = IncomingBso::from_test_content(bad_json);
427 let login = SyncLoginData::from_bso(bad_bso, &*TEST_ENCDEC)
428 .unwrap()
429 .inbound
430 .unwrap()
431 .login;
432 assert_eq!(login.fields.username_field, "");
433 }
434
435 #[test]
436 fn test_password_field_requires_a_form_target() {
437 let bad_bso = IncomingBso::from_test_content(serde_json::json!({
438 "id": "123412341234",
439 "httpRealm": "test",
440 "hostname": "https://www.example.com",
441 "username": "test",
442 "password": "test",
443 "passwordField": "invalid"
444 }));
445
446 let login = IncomingLogin::from_incoming_payload(
447 bad_bso.into_content::<LoginPayload>().content().unwrap(),
448 &*TEST_ENCDEC,
449 )
450 .unwrap()
451 .login;
452 assert_eq!(login.fields.password_field, "");
453 }
454}