1use super::{IncomingLogin, LoginPayload};
7use crate::encryption::EncryptorDecryptor;
8use crate::error::*;
9use crate::login::EncryptedLogin;
10use crate::util;
11use rusqlite::Row;
12use std::time::SystemTime;
13use sync15::bso::{IncomingBso, IncomingKind};
14use sync15::ServerTimestamp;
15use sync_guid::Guid;
16
17#[derive(Clone, Debug)]
18pub(crate) struct MirrorLogin {
19 pub login: EncryptedLogin,
20 pub server_modified: ServerTimestamp,
21}
22
23impl MirrorLogin {
24 #[inline]
25 pub fn guid_str(&self) -> &str {
26 &self.login.meta.id
27 }
28
29 pub(crate) fn from_row(row: &Row<'_>) -> Result<MirrorLogin> {
30 Ok(MirrorLogin {
31 login: EncryptedLogin::from_row(row)?,
32 server_modified: ServerTimestamp(row.get::<_, i64>("server_modified")?),
33 })
34 }
35}
36#[derive(Clone, Debug)]
37pub(crate) enum LocalLogin {
38 Tombstone {
39 id: String,
40 local_modified: SystemTime,
41 },
42 Alive {
43 login: EncryptedLogin,
44 local_modified: SystemTime,
45 },
46}
47
48impl LocalLogin {
49 #[inline]
50 pub fn guid_str(&self) -> &str {
51 match &self {
52 LocalLogin::Tombstone { id, .. } => id.as_str(),
53 LocalLogin::Alive { login, .. } => login.guid_str(),
54 }
55 }
56
57 pub fn local_modified(&self) -> SystemTime {
58 match &self {
59 LocalLogin::Tombstone { local_modified, .. }
60 | LocalLogin::Alive { local_modified, .. } => *local_modified,
61 }
62 }
63
64 pub(crate) fn from_row(row: &Row<'_>) -> Result<LocalLogin> {
65 let local_modified = util::system_time_millis_from_row(row, "local_modified")?;
66 Ok(if row.get("is_deleted")? {
67 let id = row.get("guid")?;
68 LocalLogin::Tombstone { id, local_modified }
69 } else {
70 let login = EncryptedLogin::from_row(row)?;
71 if login.sec_fields.is_empty() {
72 error_support::report_error!("logins-crypto", "empty ciphertext in the db",);
73 }
74 LocalLogin::Alive {
75 login,
76 local_modified,
77 }
78 })
79 }
80
81 #[cfg(not(feature = "keydb"))]
84 #[cfg(test)]
85 pub(crate) fn test_raw_from_row(row: &Row<'_>) -> Result<EncryptedLogin> {
86 EncryptedLogin::from_row(row)
87 }
88}
89
90macro_rules! impl_login {
91 ($ty:ty { $($fields:tt)* }) => {
92 impl AsRef<EncryptedLogin> for $ty {
93 #[inline]
94 fn as_ref(&self) -> &EncryptedLogin {
95 &self.login
96 }
97 }
98
99 impl AsMut<EncryptedLogin> for $ty {
100 #[inline]
101 fn as_mut(&mut self) -> &mut EncryptedLogin {
102 &mut self.login
103 }
104 }
105
106 impl From<$ty> for EncryptedLogin {
107 #[inline]
108 fn from(l: $ty) -> Self {
109 l.login
110 }
111 }
112
113 impl From<EncryptedLogin> for $ty {
114 #[inline]
115 fn from(login: EncryptedLogin) -> Self {
116 Self { login, $($fields)* }
117 }
118 }
119 };
120}
121
122impl_login!(MirrorLogin {
123 server_modified: ServerTimestamp(0)
124});
125
126#[derive(Debug)]
128pub(super) struct SyncLoginData {
129 pub guid: Guid,
130 pub local: Option<LocalLogin>,
131 pub mirror: Option<MirrorLogin>,
132 pub inbound: Option<IncomingLogin>,
134 pub inbound_ts: ServerTimestamp,
135}
136
137impl SyncLoginData {
138 #[inline]
139 pub fn guid_str(&self) -> &str {
140 self.guid.as_str()
141 }
142
143 #[inline]
144 pub fn guid(&self) -> &Guid {
145 &self.guid
146 }
147
148 pub fn from_bso(bso: IncomingBso, encdec: &dyn EncryptorDecryptor) -> Result<Self> {
149 let guid = bso.envelope.id.clone();
150 let inbound_ts = bso.envelope.modified;
151 let inbound = match bso.into_content::<LoginPayload>().kind {
152 IncomingKind::Content(p) => Some(IncomingLogin::from_incoming_payload(p, encdec)?),
153 IncomingKind::Tombstone => None,
154 IncomingKind::Malformed => return Err(Error::MalformedIncomingRecord),
157 };
158 Ok(Self {
159 guid,
160 local: None,
161 mirror: None,
162 inbound,
163 inbound_ts,
164 })
165 }
166}
167
168macro_rules! impl_login_setter {
169 ($setter_name:ident, $field:ident, $Login:ty) => {
170 impl SyncLoginData {
171 pub(crate) fn $setter_name(&mut self, record: $Login) -> Result<()> {
172 if self.$field.is_some() {
174 panic!(
177 "SyncLoginData::{} called on object that already has {} data",
178 stringify!($setter_name),
179 stringify!($field)
180 );
181 }
182
183 if self.guid_str() != record.guid_str() {
184 panic!(
186 "Wrong guid on login in {}: {:?} != {:?}",
187 stringify!($setter_name),
188 self.guid_str(),
189 record.guid_str()
190 );
191 }
192
193 self.$field = Some(record);
194 Ok(())
195 }
196 }
197 };
198}
199
200impl_login_setter!(set_local, local, LocalLogin);
201impl_login_setter!(set_mirror, mirror, MirrorLogin);
202
203#[derive(Debug, Default, Clone)]
204pub(crate) struct LoginDelta {
205 pub origin: Option<String>,
207 pub password: Option<String>,
208 pub username: Option<String>,
209 pub http_realm: Option<String>,
210 pub form_action_origin: Option<String>,
211
212 pub time_created: Option<i64>,
213 pub time_last_used: Option<i64>,
214 pub time_password_changed: Option<i64>,
215
216 pub password_field: Option<String>,
218 pub username_field: Option<String>,
219
220 pub times_used: i64,
222}
223
224macro_rules! merge_field {
225 ($merged:ident, $b:ident, $prefer_b:expr, $field:ident) => {
226 if let Some($field) = $b.$field.take() {
227 if $merged.$field.is_some() {
228 warn!("Collision merging login field {}", stringify!($field));
229 if $prefer_b {
230 $merged.$field = Some($field);
231 }
232 } else {
233 $merged.$field = Some($field);
234 }
235 }
236 };
237}
238
239impl LoginDelta {
240 #[allow(clippy::cognitive_complexity)] pub fn merge(self, mut b: LoginDelta, b_is_newer: bool) -> LoginDelta {
242 let mut merged = self;
243 merge_field!(merged, b, b_is_newer, origin);
244 merge_field!(merged, b, b_is_newer, password);
245 merge_field!(merged, b, b_is_newer, username);
246 merge_field!(merged, b, b_is_newer, http_realm);
247 merge_field!(merged, b, b_is_newer, form_action_origin);
248
249 merge_field!(merged, b, b_is_newer, time_created);
250 merge_field!(merged, b, b_is_newer, time_last_used);
251 merge_field!(merged, b, b_is_newer, time_password_changed);
252
253 merge_field!(merged, b, b_is_newer, password_field);
254 merge_field!(merged, b, b_is_newer, username_field);
255
256 merged.times_used += b.times_used;
258
259 merged
260 }
261}
262
263macro_rules! apply_field {
264 ($login:ident, $delta:ident, $field:ident) => {
265 if let Some($field) = $delta.$field.take() {
266 $login.fields.$field = $field.into();
267 }
268 };
269}
270
271macro_rules! apply_metadata_field {
272 ($login:ident, $delta:ident, $field:ident) => {
273 if let Some($field) = $delta.$field.take() {
274 $login.meta.$field = $field.into();
275 }
276 };
277}
278
279impl EncryptedLogin {
280 pub(crate) fn apply_delta(
281 &mut self,
282 mut delta: LoginDelta,
283 encdec: &dyn EncryptorDecryptor,
284 ) -> Result<()> {
285 apply_field!(self, delta, origin);
286
287 apply_metadata_field!(self, delta, time_created);
288 apply_metadata_field!(self, delta, time_last_used);
289 apply_metadata_field!(self, delta, time_password_changed);
290
291 apply_field!(self, delta, password_field);
292 apply_field!(self, delta, username_field);
293
294 let mut sec_fields = self.decrypt_fields(encdec)?;
295 if let Some(password) = delta.password.take() {
296 sec_fields.password = password;
297 }
298 if let Some(username) = delta.username.take() {
299 sec_fields.username = username;
300 }
301 self.sec_fields = sec_fields.encrypt(encdec, &self.meta.id)?;
302
303 if let Some(realm) = delta.http_realm.take() {
305 self.fields.http_realm = if realm.is_empty() { None } else { Some(realm) };
306 }
307
308 if let Some(url) = delta.form_action_origin.take() {
309 self.fields.form_action_origin = if url.is_empty() { None } else { Some(url) };
310 }
311
312 self.meta.times_used += delta.times_used;
313 Ok(())
314 }
315
316 pub(crate) fn delta(
317 &self,
318 older: &EncryptedLogin,
319 encdec: &dyn EncryptorDecryptor,
320 ) -> Result<LoginDelta> {
321 let mut delta = LoginDelta::default();
322
323 if self.fields.form_action_origin != older.fields.form_action_origin {
324 delta.form_action_origin =
325 Some(self.fields.form_action_origin.clone().unwrap_or_default());
326 }
327
328 if self.fields.http_realm != older.fields.http_realm {
329 delta.http_realm = Some(self.fields.http_realm.clone().unwrap_or_default());
330 }
331
332 if self.fields.origin != older.fields.origin {
333 delta.origin = Some(self.fields.origin.clone());
334 }
335 let older_sec_fields = older.decrypt_fields(encdec)?;
336 let self_sec_fields = self.decrypt_fields(encdec)?;
337 if self_sec_fields.username != older_sec_fields.username {
338 delta.username = Some(self_sec_fields.username.clone());
339 }
340 if self_sec_fields.password != older_sec_fields.password {
341 delta.password = Some(self_sec_fields.password);
342 }
343 if self.fields.password_field != older.fields.password_field {
344 delta.password_field = Some(self.fields.password_field.clone());
345 }
346 if self.fields.username_field != older.fields.username_field {
347 delta.username_field = Some(self.fields.username_field.clone());
348 }
349
350 if self.meta.time_created > 0 && self.meta.time_created != older.meta.time_created {
360 delta.time_created = Some(self.meta.time_created);
361 }
362 if self.meta.time_last_used > 0 && self.meta.time_last_used != older.meta.time_last_used {
363 delta.time_last_used = Some(self.meta.time_last_used);
364 }
365 if self.meta.time_password_changed > 0
366 && self.meta.time_password_changed != older.meta.time_password_changed
367 {
368 delta.time_password_changed = Some(self.meta.time_password_changed);
369 }
370
371 if self.meta.times_used > 0 && self.meta.times_used != older.meta.times_used {
372 delta.times_used = self.meta.times_used - older.meta.times_used;
373 }
374
375 Ok(delta)
376 }
377}
378
379#[cfg(not(feature = "keydb"))]
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::encryption::test_utils::TEST_ENCDEC;
384 use nss::ensure_initialized;
385
386 #[test]
387 fn test_invalid_payload_timestamps() {
388 ensure_initialized();
389 #[allow(clippy::unreadable_literal)]
390 let bad_timestamp = 18446732429235952000u64;
391 let bad_payload = IncomingBso::from_test_content(serde_json::json!({
392 "id": "123412341234",
393 "formSubmitURL": "https://www.example.com/submit",
394 "hostname": "https://www.example.com",
395 "username": "test",
396 "password": "test",
397 "timeCreated": bad_timestamp,
398 "timeLastUsed": "some other garbage",
399 "timePasswordChanged": -30, }));
401 let login = SyncLoginData::from_bso(bad_payload, &*TEST_ENCDEC)
402 .unwrap()
403 .inbound
404 .unwrap()
405 .login;
406 assert_eq!(login.meta.time_created, 0);
407 assert_eq!(login.meta.time_last_used, 0);
408 assert_eq!(login.meta.time_password_changed, 0);
409
410 let now64 = util::system_time_ms_i64(std::time::SystemTime::now());
411 let good_payload = IncomingBso::from_test_content(serde_json::json!({
412 "id": "123412341234",
413 "formSubmitURL": "https://www.example.com/submit",
414 "hostname": "https://www.example.com",
415 "username": "test",
416 "password": "test",
417 "timeCreated": now64 - 100,
418 "timeLastUsed": now64 - 50,
419 "timePasswordChanged": now64 - 25,
420 }));
421
422 let login = SyncLoginData::from_bso(good_payload, &*TEST_ENCDEC)
423 .unwrap()
424 .inbound
425 .unwrap()
426 .login;
427
428 assert_eq!(login.meta.time_created, now64 - 100);
429 assert_eq!(login.meta.time_last_used, now64 - 50);
430 assert_eq!(login.meta.time_password_changed, now64 - 25);
431 }
432}