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