logins/
db.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/// Logins DB handling
6///
7/// The logins database works differently than other components because "mirror" and "local" mean
8/// different things.  At some point we should probably refactor to make it match them, but here's
9/// how it works for now:
10///
11///   - loginsM is the mirror table, which means it stores what we believe is on the server.  This
12///     means either the last record we fetched from the server or the last record we uploaded.
13///   - loginsL is the local table, which means it stores local changes that have not been sent to
14///     the server.
15///   - When we want to fetch a record, we need to look in both loginsL and loginsM for the data.
16///     If a record is in both tables, then we prefer the loginsL data.  GET_BY_GUID_SQL contains a
17///     clever UNION query to accomplish this.
18///   - If a record is in both the local and mirror tables, we call the local record the "overlay"
19///     and set the is_overridden flag on the mirror record.
20///   - When we sync, the presence of a record in loginsL means that there was a local change that
21///     we need to send to the the server and/or reconcile it with incoming changes from the
22///     server.
23///   - After we sync, we move all records from loginsL to loginsM, overwriting any previous data.
24///     loginsL will be an empty table after this.  See mark_as_synchronized() for the details.
25use crate::encryption::EncryptorDecryptor;
26use crate::error::*;
27use crate::login::*;
28use crate::schema;
29use crate::sync::SyncStatus;
30use crate::util;
31use interrupt_support::{SqlInterruptHandle, SqlInterruptScope};
32use lazy_static::lazy_static;
33use rusqlite::{
34    named_params,
35    types::{FromSql, ToSql},
36    Connection,
37};
38use sql_support::ConnExt;
39use std::ops::Deref;
40use std::path::Path;
41use std::sync::Arc;
42use std::time::SystemTime;
43use sync_guid::Guid;
44use url::{Host, Url};
45
46pub struct LoginDb {
47    pub db: Connection,
48    pub encdec: Arc<dyn EncryptorDecryptor>,
49    interrupt_handle: Arc<SqlInterruptHandle>,
50}
51
52pub struct LoginsDeletionMetrics {
53    pub local_deleted: u64,
54    pub mirror_deleted: u64,
55}
56
57impl LoginDb {
58    pub fn with_connection(db: Connection, encdec: Arc<dyn EncryptorDecryptor>) -> Result<Self> {
59        #[cfg(test)]
60        {
61            util::init_test_logging();
62        }
63
64        // `temp_store = 2` is required on Android to force the DB to keep temp
65        // files in memory, since on Android there's no tmp partition. See
66        // https://github.com/mozilla/mentat/issues/505. Ideally we'd only
67        // do this on Android, or allow caller to configure it.
68        db.set_pragma("temp_store", 2)?;
69
70        let mut logins = Self {
71            interrupt_handle: Arc::new(SqlInterruptHandle::new(&db)),
72            encdec,
73            db,
74        };
75        let tx = logins.db.transaction()?;
76        schema::init(&tx)?;
77        tx.commit()?;
78        Ok(logins)
79    }
80
81    pub fn open(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> Result<Self> {
82        Self::with_connection(Connection::open(path)?, encdec)
83    }
84
85    #[cfg(test)]
86    pub fn open_in_memory() -> Self {
87        let encdec: Arc<dyn EncryptorDecryptor> =
88            crate::encryption::test_utils::TEST_ENCDEC.clone();
89        Self::with_connection(Connection::open_in_memory().unwrap(), encdec).unwrap()
90    }
91
92    pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
93        Arc::clone(&self.interrupt_handle)
94    }
95
96    #[inline]
97    pub fn begin_interrupt_scope(&self) -> Result<SqlInterruptScope> {
98        Ok(self.interrupt_handle.begin_interrupt_scope()?)
99    }
100}
101
102impl ConnExt for LoginDb {
103    #[inline]
104    fn conn(&self) -> &Connection {
105        &self.db
106    }
107}
108
109impl Deref for LoginDb {
110    type Target = Connection;
111    #[inline]
112    fn deref(&self) -> &Connection {
113        &self.db
114    }
115}
116
117// login specific stuff.
118
119impl LoginDb {
120    pub(crate) fn put_meta(&self, key: &str, value: &dyn ToSql) -> Result<()> {
121        self.execute_cached(
122            "REPLACE INTO loginsSyncMeta (key, value) VALUES (:key, :value)",
123            named_params! { ":key": key, ":value": value },
124        )?;
125        Ok(())
126    }
127
128    pub(crate) fn get_meta<T: FromSql>(&self, key: &str) -> Result<Option<T>> {
129        self.try_query_row(
130            "SELECT value FROM loginsSyncMeta WHERE key = :key",
131            named_params! { ":key": key },
132            |row| Ok::<_, Error>(row.get(0)?),
133            true,
134        )
135    }
136
137    pub(crate) fn delete_meta(&self, key: &str) -> Result<()> {
138        self.execute_cached(
139            "DELETE FROM loginsSyncMeta WHERE key = :key",
140            named_params! { ":key": key },
141        )?;
142        Ok(())
143    }
144
145    pub fn count_all(&self) -> Result<i64> {
146        let mut stmt = self.db.prepare_cached(&COUNT_ALL_SQL)?;
147
148        let count: i64 = stmt.query_row([], |row| row.get(0))?;
149        Ok(count)
150    }
151
152    pub fn count_by_origin(&self, origin: &str) -> Result<i64> {
153        match LoginEntry::validate_and_fixup_origin(origin) {
154            Ok(result) => {
155                let origin = result.unwrap_or(origin.to_string());
156                let mut stmt = self.db.prepare_cached(&COUNT_BY_ORIGIN_SQL)?;
157                let count: i64 =
158                    stmt.query_row(named_params! { ":origin": origin }, |row| row.get(0))?;
159                Ok(count)
160            }
161            Err(e) => {
162                // don't log the input string as it's PII.
163                warn!("count_by_origin was passed an invalid origin: {}", e);
164                Ok(0)
165            }
166        }
167    }
168
169    pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> Result<i64> {
170        match LoginEntry::validate_and_fixup_origin(form_action_origin) {
171            Ok(result) => {
172                let form_action_origin = result.unwrap_or(form_action_origin.to_string());
173                let mut stmt = self.db.prepare_cached(&COUNT_BY_FORM_ACTION_ORIGIN_SQL)?;
174                let count: i64 = stmt.query_row(
175                    named_params! { ":form_action_origin": form_action_origin },
176                    |row| row.get(0),
177                )?;
178                Ok(count)
179            }
180            Err(e) => {
181                // don't log the input string as it's PII.
182                warn!("count_by_origin was passed an invalid origin: {}", e);
183                Ok(0)
184            }
185        }
186    }
187
188    pub fn get_all(&self) -> Result<Vec<EncryptedLogin>> {
189        let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
190        let rows = stmt.query_and_then([], EncryptedLogin::from_row)?;
191        rows.collect::<Result<_>>()
192    }
193
194    pub fn get_by_base_domain(&self, base_domain: &str) -> Result<Vec<EncryptedLogin>> {
195        // We first parse the input string as a host so it is normalized.
196        let base_host = match Host::parse(base_domain) {
197            Ok(d) => d,
198            Err(e) => {
199                // don't log the input string as it's PII.
200                warn!("get_by_base_domain was passed an invalid domain: {}", e);
201                return Ok(vec![]);
202            }
203        };
204        // We just do a linear scan. Another option is to have an indexed
205        // reverse-host column or similar, but current thinking is that it's
206        // extra complexity for (probably) zero actual benefit given the record
207        // counts are expected to be so low.
208        // A regex would probably make this simpler, but we don't want to drag
209        // in a regex lib just for this.
210        let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
211        let rows = stmt
212            .query_and_then([], EncryptedLogin::from_row)?
213            .filter(|r| {
214                let login = r
215                    .as_ref()
216                    .ok()
217                    .and_then(|login| Url::parse(&login.fields.origin).ok());
218                let this_host = login.as_ref().and_then(|url| url.host());
219                match (&base_host, this_host) {
220                    (Host::Domain(base), Some(Host::Domain(look))) => {
221                        // a fairly long-winded way of saying
222                        // `login.fields.origin == base_domain ||
223                        //  login.fields.origin.ends_with('.' + base_domain);`
224                        let mut rev_input = base.chars().rev();
225                        let mut rev_host = look.chars().rev();
226                        loop {
227                            match (rev_input.next(), rev_host.next()) {
228                                (Some(ref a), Some(ref b)) if a == b => continue,
229                                (None, None) => return true, // exactly equal
230                                (None, Some(ref h)) => return *h == '.',
231                                _ => return false,
232                            }
233                        }
234                    }
235                    // ip addresses must match exactly.
236                    (Host::Ipv4(base), Some(Host::Ipv4(look))) => *base == look,
237                    (Host::Ipv6(base), Some(Host::Ipv6(look))) => *base == look,
238                    // all "mismatches" in domain types are false.
239                    _ => false,
240                }
241            });
242        rows.collect::<Result<_>>()
243    }
244
245    pub fn get_by_id(&self, id: &str) -> Result<Option<EncryptedLogin>> {
246        self.try_query_row(
247            &GET_BY_GUID_SQL,
248            &[(":guid", &id as &dyn ToSql)],
249            EncryptedLogin::from_row,
250            true,
251        )
252    }
253
254    // Match a `LoginEntry` being saved to existing logins in the DB
255    //
256    // When a user is saving new login, there are several cases for how we want to save the data:
257    //
258    //  - Adding a new login: `None` will be returned
259    //  - Updating an existing login: `Some(login)` will be returned and the username will match
260    //    the one for look.
261    //  - Filling in a blank username for an existing login: `Some(login)` will be returned
262    //    with a blank username.
263    //
264    //  Returns an Err if the new login is not valid and could not be fixed up
265    pub fn find_login_to_update(
266        &self,
267        look: LoginEntry,
268        encdec: &dyn EncryptorDecryptor,
269    ) -> Result<Option<Login>> {
270        let look = look.fixup()?;
271        let logins = self
272            .get_by_entry_target(&look)?
273            .into_iter()
274            .map(|enc_login| enc_login.decrypt(encdec))
275            .collect::<Result<Vec<Login>>>()?;
276        Ok(logins
277            // First, try to match the username
278            .iter()
279            .find(|login| login.username == look.username)
280            // Fall back on a blank username
281            .or_else(|| logins.iter().find(|login| login.username.is_empty()))
282            // Clone the login to avoid ref issues when returning across the FFI
283            .cloned())
284    }
285
286    pub fn touch(&self, id: &str) -> Result<()> {
287        let tx = self.unchecked_transaction()?;
288        self.ensure_local_overlay_exists(id)?;
289        self.mark_mirror_overridden(id)?;
290        let now_ms = util::system_time_ms_i64(SystemTime::now());
291        // As on iOS, just using a record doesn't flip it's status to changed.
292        // TODO: this might be wrong for lockbox!
293        self.execute_cached(
294            "UPDATE loginsL
295             SET timeLastUsed = :now_millis,
296                 timesUsed = timesUsed + 1,
297                 local_modified = :now_millis
298             WHERE guid = :guid
299                 AND is_deleted = 0",
300            named_params! {
301                ":now_millis": now_ms,
302                ":guid": id,
303            },
304        )?;
305        tx.commit()?;
306        Ok(())
307    }
308
309    pub fn record_breach(&self, id: &str, timestamp: i64) -> Result<()> {
310        let tx = self.unchecked_transaction()?;
311        self.ensure_local_overlay_exists(id)?;
312        self.mark_mirror_overridden(id)?;
313        self.execute_cached(
314            "UPDATE loginsL
315             SET timeOfLastBreach = :now_millis
316             WHERE guid = :guid",
317            named_params! {
318                ":now_millis": timestamp,
319                ":guid": id,
320            },
321        )?;
322        tx.commit()?;
323        Ok(())
324    }
325
326    pub fn is_potentially_breached(&self, id: &str) -> Result<bool> {
327        let is_potentially_breached: bool = self.db.query_row(
328            "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid AND timeOfLastBreach IS NOT NULL AND timeOfLastBreach > timePasswordChanged)",
329            named_params! { ":guid": id },
330            |row| row.get(0),
331        )?;
332        Ok(is_potentially_breached)
333    }
334
335    pub fn reset_all_breaches(&self) -> Result<()> {
336        let tx = self.unchecked_transaction()?;
337        self.execute_cached(
338            "UPDATE loginsL
339             SET timeOfLastBreach = NULL
340             WHERE timeOfLastBreach IS NOT NULL",
341            [],
342        )?;
343        tx.commit()?;
344        Ok(())
345    }
346
347    pub fn is_breach_alert_dismissed(&self, id: &str) -> Result<bool> {
348        let is_breach_alert_dismissed: bool = self.db.query_row(
349            "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid AND timeOfLastBreach < timeLastBreachAlertDismissed)",
350            named_params! { ":guid": id },
351            |row| row.get(0),
352        )?;
353        Ok(is_breach_alert_dismissed)
354    }
355
356    /// Records that the user dismissed the breach alert for a login using the current time.
357    ///
358    /// For testing or when you need to specify a particular timestamp, use
359    /// [`record_breach_alert_dismissal_time`](Self::record_breach_alert_dismissal_time) instead.
360    pub fn record_breach_alert_dismissal(&self, id: &str) -> Result<()> {
361        let timestamp = util::system_time_ms_i64(SystemTime::now());
362        self.record_breach_alert_dismissal_time(id, timestamp)
363    }
364
365    /// Records that the user dismissed the breach alert for a login at a specific time.
366    ///
367    /// This is primarily useful for testing or when syncing dismissal times from other devices.
368    /// For normal usage, prefer [`record_breach_alert_dismissal`](Self::record_breach_alert_dismissal)
369    /// which automatically uses the current time.
370    pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> Result<()> {
371        let tx = self.unchecked_transaction()?;
372        self.ensure_local_overlay_exists(id)?;
373        self.mark_mirror_overridden(id)?;
374        self.execute_cached(
375            "UPDATE loginsL
376             SET timeLastBreachAlertDismissed = :now_millis
377             WHERE guid = :guid",
378            named_params! {
379                ":now_millis": timestamp,
380                ":guid": id,
381            },
382        )?;
383        tx.commit()?;
384        Ok(())
385    }
386
387    // The single place we insert new rows or update existing local rows.
388    // just the SQL - no validation or anything.
389    fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
390        let sql = format!(
391            "INSERT OR REPLACE INTO loginsL (
392                origin,
393                httpRealm,
394                formActionOrigin,
395                usernameField,
396                passwordField,
397                timesUsed,
398                secFields,
399                guid,
400                timeCreated,
401                timeLastUsed,
402                timePasswordChanged,
403                timeOfLastBreach,
404                timeLastBreachAlertDismissed,
405                local_modified,
406                is_deleted,
407                sync_status
408            ) VALUES (
409                :origin,
410                :http_realm,
411                :form_action_origin,
412                :username_field,
413                :password_field,
414                :times_used,
415                :sec_fields,
416                :guid,
417                :time_created,
418                :time_last_used,
419                :time_password_changed,
420                :time_of_last_breach,
421                :time_last_breach_alert_dismissed,
422                :local_modified,
423                0, -- is_deleted
424                {new} -- sync_status
425            )",
426            new = SyncStatus::New as u8
427        );
428
429        self.execute(
430            &sql,
431            named_params! {
432                ":origin": login.fields.origin,
433                ":http_realm": login.fields.http_realm,
434                ":form_action_origin": login.fields.form_action_origin,
435                ":username_field": login.fields.username_field,
436                ":password_field": login.fields.password_field,
437                ":time_created": login.meta.time_created,
438                ":times_used": login.meta.times_used,
439                ":time_last_used": login.meta.time_last_used,
440                ":time_password_changed": login.meta.time_password_changed,
441                ":time_of_last_breach": login.fields.time_of_last_breach,
442                ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed,
443                ":local_modified": login.meta.time_created,
444                ":sec_fields": login.sec_fields,
445                ":guid": login.guid(),
446            },
447        )?;
448        Ok(())
449    }
450
451    fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
452        // assumes the "local overlay" exists, so the guid must too.
453        let sql = format!(
454            "UPDATE loginsL
455             SET local_modified                           = :now_millis,
456                 timeLastUsed                             = :time_last_used,
457                 timePasswordChanged                      = :time_password_changed,
458                 httpRealm                                = :http_realm,
459                 formActionOrigin                         = :form_action_origin,
460                 usernameField                            = :username_field,
461                 passwordField                            = :password_field,
462                 timesUsed                                = :times_used,
463                 secFields                                = :sec_fields,
464                 origin                                   = :origin,
465                 -- leave New records as they are, otherwise update them to `changed`
466                 sync_status                              = max(sync_status, {changed})
467             WHERE guid = :guid",
468            changed = SyncStatus::Changed as u8
469        );
470
471        self.db.execute(
472            &sql,
473            named_params! {
474                ":origin": login.fields.origin,
475                ":http_realm": login.fields.http_realm,
476                ":form_action_origin": login.fields.form_action_origin,
477                ":username_field": login.fields.username_field,
478                ":password_field": login.fields.password_field,
479                ":time_last_used": login.meta.time_last_used,
480                ":times_used": login.meta.times_used,
481                ":time_password_changed": login.meta.time_password_changed,
482                ":sec_fields": login.sec_fields,
483                ":guid": &login.meta.id,
484                // time_last_used has been set to now.
485                ":now_millis": login.meta.time_last_used,
486            },
487        )?;
488        Ok(())
489    }
490
491    /// Adds multiple logins within a single transaction and returns the successfully saved logins.
492    pub fn add_many(
493        &self,
494        entries: Vec<LoginEntry>,
495        encdec: &dyn EncryptorDecryptor,
496    ) -> Result<Vec<Result<EncryptedLogin>>> {
497        let now_ms = util::system_time_ms_i64(SystemTime::now());
498
499        let entries_with_meta = entries
500            .into_iter()
501            .map(|entry| {
502                let guid = Guid::random();
503                LoginEntryWithMeta {
504                    entry,
505                    meta: LoginMeta {
506                        id: guid.to_string(),
507                        time_created: now_ms,
508                        time_password_changed: now_ms,
509                        time_last_used: now_ms,
510                        times_used: 1,
511                    },
512                }
513            })
514            .collect();
515
516        self.add_many_with_meta(entries_with_meta, encdec)
517    }
518
519    /// Adds multiple logins **including metadata** within a single transaction and returns the successfully saved logins.
520    /// Normally, you will use `add_many` instead, and AS Logins will take care of the metadata (setting timestamps, generating an ID) itself.
521    /// However, in some cases, this method is necessary, for example when migrating data from another store that already contains the metadata.
522    pub fn add_many_with_meta(
523        &self,
524        entries_with_meta: Vec<LoginEntryWithMeta>,
525        encdec: &dyn EncryptorDecryptor,
526    ) -> Result<Vec<Result<EncryptedLogin>>> {
527        let tx = self.unchecked_transaction()?;
528        let mut results = vec![];
529        for entry_with_meta in entries_with_meta {
530            let guid = Guid::from_string(entry_with_meta.meta.id.clone());
531            match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry, encdec) {
532                Ok(new_entry) => {
533                    let sec_fields = SecureLoginFields {
534                        username: new_entry.username,
535                        password: new_entry.password,
536                    }
537                    .encrypt(encdec, &entry_with_meta.meta.id)?;
538                    let encrypted_login = EncryptedLogin {
539                        meta: entry_with_meta.meta,
540                        fields: LoginFields {
541                            origin: new_entry.origin,
542                            form_action_origin: new_entry.form_action_origin,
543                            http_realm: new_entry.http_realm,
544                            username_field: new_entry.username_field,
545                            password_field: new_entry.password_field,
546                            time_of_last_breach: None,
547                            time_last_breach_alert_dismissed: None,
548                        },
549                        sec_fields,
550                    };
551                    let result = self
552                        .insert_new_login(&encrypted_login)
553                        .map(|_| encrypted_login);
554                    results.push(result);
555                }
556
557                Err(error) => results.push(Err(error)),
558            }
559        }
560        tx.commit()?;
561        Ok(results)
562    }
563
564    pub fn add(
565        &self,
566        entry: LoginEntry,
567        encdec: &dyn EncryptorDecryptor,
568    ) -> Result<EncryptedLogin> {
569        let guid = Guid::random();
570        let now_ms = util::system_time_ms_i64(SystemTime::now());
571
572        let entry_with_meta = LoginEntryWithMeta {
573            entry,
574            meta: LoginMeta {
575                id: guid.to_string(),
576                time_created: now_ms,
577                time_password_changed: now_ms,
578                time_last_used: now_ms,
579                times_used: 1,
580            },
581        };
582
583        self.add_with_meta(entry_with_meta, encdec)
584    }
585
586    /// Adds a login **including metadata**.
587    /// Normally, you will use `add` instead, and AS Logins will take care of the metadata (setting timestamps, generating an ID) itself.
588    /// However, in some cases, this method is necessary, for example when migrating data from another store that already contains the metadata.
589    pub fn add_with_meta(
590        &self,
591        entry_with_meta: LoginEntryWithMeta,
592        encdec: &dyn EncryptorDecryptor,
593    ) -> Result<EncryptedLogin> {
594        let mut results = self.add_many_with_meta(vec![entry_with_meta], encdec)?;
595        results.pop().expect("there should be a single result")
596    }
597
598    pub fn update(
599        &self,
600        sguid: &str,
601        entry: LoginEntry,
602        encdec: &dyn EncryptorDecryptor,
603    ) -> Result<EncryptedLogin> {
604        let guid = Guid::new(sguid);
605        let now_ms = util::system_time_ms_i64(SystemTime::now());
606        let tx = self.unchecked_transaction()?;
607
608        let entry = entry.fixup()?;
609
610        // Check if there's an existing login that's the dupe of this login.  That indicates that
611        // something has gone wrong with our underlying logic.  However, if we do see a dupe login,
612        // just log an error and continue.  This avoids a crash on android-components
613        // (mozilla-mobile/android-components#11251).
614
615        if self.check_for_dupes(&guid, &entry, encdec).is_err() {
616            // Try to detect if sync is enabled by checking if there are any mirror logins
617            let has_mirror_row: bool = self
618                .db
619                .conn_ext_query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
620            let has_http_realm = entry.http_realm.is_some();
621            let has_form_action_origin = entry.form_action_origin.is_some();
622            report_error!(
623                "logins-duplicate-in-update",
624                "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
625        }
626
627        // Note: This fail with NoSuchRecord if the record doesn't exist.
628        self.ensure_local_overlay_exists(&guid)?;
629        self.mark_mirror_overridden(&guid)?;
630
631        // We must read the existing record so we can correctly manage timePasswordChanged.
632        let existing = match self.get_by_id(sguid)? {
633            Some(e) => e.decrypt(encdec)?,
634            None => return Err(Error::NoSuchRecord(sguid.to_owned())),
635        };
636        let time_password_changed = if existing.password == entry.password {
637            existing.time_password_changed
638        } else {
639            now_ms
640        };
641
642        // Make the final object here - every column will be updated.
643        let sec_fields = SecureLoginFields {
644            username: entry.username,
645            password: entry.password,
646        }
647        .encrypt(encdec, &existing.id)?;
648        let result = EncryptedLogin {
649            meta: LoginMeta {
650                id: existing.id,
651                time_created: existing.time_created,
652                time_password_changed,
653                time_last_used: now_ms,
654                times_used: existing.times_used + 1,
655            },
656            fields: LoginFields {
657                origin: entry.origin,
658                form_action_origin: entry.form_action_origin,
659                http_realm: entry.http_realm,
660                username_field: entry.username_field,
661                password_field: entry.password_field,
662                time_of_last_breach: None,
663                time_last_breach_alert_dismissed: None,
664            },
665            sec_fields,
666        };
667
668        self.update_existing_login(&result)?;
669        tx.commit()?;
670        Ok(result)
671    }
672
673    pub fn add_or_update(
674        &self,
675        entry: LoginEntry,
676        encdec: &dyn EncryptorDecryptor,
677    ) -> Result<EncryptedLogin> {
678        // Make sure to fixup the entry first, in case that changes the username
679        let entry = entry.fixup()?;
680        match self.find_login_to_update(entry.clone(), encdec)? {
681            Some(login) => self.update(&login.id, entry, encdec),
682            None => self.add(entry, encdec),
683        }
684    }
685
686    pub fn fixup_and_check_for_dupes(
687        &self,
688        guid: &Guid,
689        entry: LoginEntry,
690        encdec: &dyn EncryptorDecryptor,
691    ) -> Result<LoginEntry> {
692        let entry = entry.fixup()?;
693        self.check_for_dupes(guid, &entry, encdec)?;
694        Ok(entry)
695    }
696
697    pub fn check_for_dupes(
698        &self,
699        guid: &Guid,
700        entry: &LoginEntry,
701        encdec: &dyn EncryptorDecryptor,
702    ) -> Result<()> {
703        if self.dupe_exists(guid, entry, encdec)? {
704            return Err(InvalidLogin::DuplicateLogin.into());
705        }
706        Ok(())
707    }
708
709    pub fn dupe_exists(
710        &self,
711        guid: &Guid,
712        entry: &LoginEntry,
713        encdec: &dyn EncryptorDecryptor,
714    ) -> Result<bool> {
715        Ok(self.find_dupe(guid, entry, encdec)?.is_some())
716    }
717
718    pub fn find_dupe(
719        &self,
720        guid: &Guid,
721        entry: &LoginEntry,
722        encdec: &dyn EncryptorDecryptor,
723    ) -> Result<Option<Guid>> {
724        for possible in self.get_by_entry_target(entry)? {
725            if possible.guid() != *guid {
726                let pos_sec_fields = possible.decrypt_fields(encdec)?;
727                if pos_sec_fields.username == entry.username {
728                    return Ok(Some(possible.guid()));
729                }
730            }
731        }
732        Ok(None)
733    }
734
735    // Find saved logins that match the target for a `LoginEntry`
736    //
737    // This means that:
738    //   - `origin` matches
739    //   - Either `form_action_origin` or `http_realm` matches, depending on which one is non-null
740    //
741    // This is used for dupe-checking and `find_login_to_update()`
742    //
743    // Note that `entry` must be a normalized Login (via `fixup()`)
744    fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
745        // Could be lazy_static-ed...
746        lazy_static::lazy_static! {
747            static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
748                "SELECT {common_cols} FROM loginsL
749                WHERE is_deleted = 0
750                    AND origin = :origin
751                    AND formActionOrigin = :form_action_origin
752
753                UNION ALL
754
755                SELECT {common_cols} FROM loginsM
756                WHERE is_overridden = 0
757                    AND origin = :origin
758                    AND formActionOrigin = :form_action_origin
759                ",
760                common_cols = schema::COMMON_COLS
761            );
762            static ref GET_BY_HTTP_REALM: String = format!(
763                "SELECT {common_cols} FROM loginsL
764                WHERE is_deleted = 0
765                    AND origin = :origin
766                    AND httpRealm = :http_realm
767
768                UNION ALL
769
770                SELECT {common_cols} FROM loginsM
771                WHERE is_overridden = 0
772                    AND origin = :origin
773                    AND httpRealm = :http_realm
774                ",
775                common_cols = schema::COMMON_COLS
776            );
777        }
778        match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
779            (Some(form_action_origin), None) => {
780                let params = named_params! {
781                    ":origin": &entry.origin,
782                    ":form_action_origin": form_action_origin,
783                };
784                self.db
785                    .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
786                    .query_and_then(params, EncryptedLogin::from_row)?
787                    .collect()
788            }
789            (None, Some(http_realm)) => {
790                let params = named_params! {
791                    ":origin": &entry.origin,
792                    ":http_realm": http_realm,
793                };
794                self.db
795                    .prepare_cached(&GET_BY_HTTP_REALM)?
796                    .query_and_then(params, EncryptedLogin::from_row)?
797                    .collect()
798            }
799            (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
800            (None, None) => Err(InvalidLogin::NoTarget.into()),
801        }
802    }
803
804    pub fn exists(&self, id: &str) -> Result<bool> {
805        Ok(self.db.query_row(
806            "SELECT EXISTS(
807                 SELECT 1 FROM loginsL
808                 WHERE guid = :guid AND is_deleted = 0
809                 UNION ALL
810                 SELECT 1 FROM loginsM
811                 WHERE guid = :guid AND is_overridden IS NOT 1
812             )",
813            named_params! { ":guid": id },
814            |row| row.get(0),
815        )?)
816    }
817
818    /// Delete the record with the provided id. Returns true if the record
819    /// existed already.
820    pub fn delete(&self, id: &str) -> Result<bool> {
821        let mut results = self.delete_many(vec![id])?;
822        Ok(results.pop().expect("there should be a single result"))
823    }
824
825    /// Delete the records with the specified IDs. Returns a list of Boolean values
826    /// indicating whether the respective records already existed.
827    pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
828        let tx = self.unchecked_transaction_imm()?;
829        let sql = format!(
830            "
831            UPDATE loginsL
832            SET local_modified = :now_ms,
833                sync_status = {status_changed},
834                is_deleted = 1,
835                secFields = '',
836                origin = '',
837                httpRealm = NULL,
838                formActionOrigin = NULL
839            WHERE guid = :guid AND is_deleted IS FALSE
840            ",
841            status_changed = SyncStatus::Changed as u8
842        );
843        let mut stmt = self.db.prepare_cached(&sql)?;
844
845        let mut result = vec![];
846
847        for id in ids {
848            let now_ms = util::system_time_ms_i64(SystemTime::now());
849
850            // For IDs that have, mark is_deleted and clear sensitive fields
851            let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
852
853            let exists = update_result == 1;
854
855            // Mark the mirror as overridden
856            self.execute(
857                "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
858                named_params! { ":guid": id },
859            )?;
860
861            // If we don't have a local record for this ID, but do have it in the mirror
862            // insert a tombstone.
863            self.execute(&format!("
864                INSERT OR IGNORE INTO loginsL
865                        (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
866                SELECT   guid, :now_ms,        1,          {changed},   '',     timeCreated, :now_ms,             ''
867                FROM loginsM
868                WHERE guid = :guid",
869                changed = SyncStatus::Changed as u8),
870                named_params! { ":now_ms": now_ms, ":guid": id })?;
871
872            result.push(exists);
873        }
874
875        tx.commit()?;
876
877        Ok(result)
878    }
879
880    pub fn delete_undecryptable_records_for_remote_replacement(
881        &self,
882        encdec: &dyn EncryptorDecryptor,
883    ) -> Result<LoginsDeletionMetrics> {
884        // Retrieve a list of guids for logins that cannot be decrypted
885        let corrupted_logins = self
886            .get_all()?
887            .into_iter()
888            .filter(|login| login.clone().decrypt(encdec).is_err())
889            .collect::<Vec<_>>();
890        let ids = corrupted_logins
891            .iter()
892            .map(|login| login.guid_str())
893            .collect::<Vec<_>>();
894
895        self.delete_local_records_for_remote_replacement(ids)
896    }
897
898    pub fn delete_local_records_for_remote_replacement(
899        &self,
900        ids: Vec<&str>,
901    ) -> Result<LoginsDeletionMetrics> {
902        let tx = self.unchecked_transaction_imm()?;
903        let mut local_deleted = 0;
904        let mut mirror_deleted = 0;
905
906        sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
907            let deleted = self.execute(
908                &format!(
909                    "DELETE FROM loginsL WHERE guid IN ({})",
910                    sql_support::repeat_sql_values(chunk.len())
911                ),
912                rusqlite::params_from_iter(chunk),
913            )?;
914            local_deleted += deleted;
915            Ok(())
916        })?;
917
918        sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
919            let deleted = self.execute(
920                &format!(
921                    "DELETE FROM loginsM WHERE guid IN ({})",
922                    sql_support::repeat_sql_values(chunk.len())
923                ),
924                rusqlite::params_from_iter(chunk),
925            )?;
926            mirror_deleted += deleted;
927            Ok(())
928        })?;
929
930        tx.commit()?;
931        Ok(LoginsDeletionMetrics {
932            local_deleted: local_deleted as u64,
933            mirror_deleted: mirror_deleted as u64,
934        })
935    }
936
937    fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
938        self.execute_cached(
939            "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
940            named_params! { ":guid": guid },
941        )?;
942        Ok(())
943    }
944
945    fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
946        let already_have_local: bool = self.db.query_row(
947            "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
948            named_params! { ":guid": guid },
949            |row| row.get(0),
950        )?;
951
952        if already_have_local {
953            return Ok(());
954        }
955
956        debug!("No overlay; cloning one for {:?}.", guid);
957        let changed = self.clone_mirror_to_overlay(guid)?;
958        if changed == 0 {
959            report_error!(
960                "logins-local-overlay-error",
961                "Failed to create local overlay for GUID {guid:?}."
962            );
963            return Err(Error::NoSuchRecord(guid.to_owned()));
964        }
965        Ok(())
966    }
967
968    fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
969        Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
970    }
971
972    /// Wipe all local data, returns the number of rows deleted
973    pub fn wipe_local(&self) -> Result<usize> {
974        info!("Executing wipe_local on password engine!");
975        let tx = self.unchecked_transaction()?;
976        let mut row_count = 0;
977        row_count += self.execute("DELETE FROM loginsL", [])?;
978        row_count += self.execute("DELETE FROM loginsM", [])?;
979        row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
980        tx.commit()?;
981        Ok(row_count)
982    }
983
984    pub fn shutdown(self) -> Result<()> {
985        self.db.close().map_err(|(_, e)| Error::SqlError(e))
986    }
987}
988
989lazy_static! {
990    static ref GET_ALL_SQL: String = format!(
991        "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
992         UNION ALL
993         SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
994        common_cols = schema::COMMON_COLS,
995    );
996    static ref COUNT_ALL_SQL: String = format!(
997        "SELECT COUNT(*) FROM (
998          SELECT guid FROM loginsL WHERE is_deleted = 0
999          UNION ALL
1000          SELECT guid FROM loginsM WHERE is_overridden = 0
1001        )"
1002    );
1003    static ref COUNT_BY_ORIGIN_SQL: String = format!(
1004        "SELECT COUNT(*) FROM (
1005          SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
1006          UNION ALL
1007          SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
1008        )"
1009    );
1010    static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
1011        "SELECT COUNT(*) FROM (
1012          SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
1013          UNION ALL
1014          SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
1015        )"
1016    );
1017    static ref GET_BY_GUID_SQL: String = format!(
1018        "SELECT {common_cols}
1019         FROM loginsL
1020         WHERE is_deleted = 0
1021           AND guid = :guid
1022
1023         UNION ALL
1024
1025         SELECT {common_cols}
1026         FROM loginsM
1027         WHERE is_overridden IS NOT 1
1028           AND guid = :guid
1029         ORDER BY origin ASC
1030
1031         LIMIT 1",
1032        common_cols = schema::COMMON_COLS,
1033    );
1034    pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
1035        "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
1036         SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
1037         FROM loginsM",
1038        common_cols = schema::COMMON_COLS,
1039    );
1040    static ref CLONE_SINGLE_MIRROR_SQL: String =
1041        format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
1042}
1043
1044#[cfg(not(feature = "keydb"))]
1045#[cfg(test)]
1046pub mod test_utils {
1047    use super::*;
1048    use crate::encryption::test_utils::decrypt_struct;
1049    use crate::login::test_utils::enc_login;
1050    use crate::SecureLoginFields;
1051    use sync15::ServerTimestamp;
1052
1053    // Insert a login into the local and/or mirror tables.
1054    //
1055    // local_login and mirror_login are specified as Some(password_string)
1056    pub fn insert_login(
1057        db: &LoginDb,
1058        guid: &str,
1059        local_login: Option<&str>,
1060        mirror_login: Option<&str>,
1061    ) {
1062        if let Some(password) = mirror_login {
1063            add_mirror(
1064                db,
1065                &enc_login(guid, password),
1066                &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
1067                local_login.is_some(),
1068            )
1069            .unwrap();
1070        }
1071        if let Some(password) = local_login {
1072            db.insert_new_login(&enc_login(guid, password)).unwrap();
1073        }
1074    }
1075
1076    pub fn insert_encrypted_login(
1077        db: &LoginDb,
1078        local: &EncryptedLogin,
1079        mirror: &EncryptedLogin,
1080        server_modified: &ServerTimestamp,
1081    ) {
1082        db.insert_new_login(local).unwrap();
1083        add_mirror(db, mirror, server_modified, true).unwrap();
1084    }
1085
1086    pub fn add_mirror(
1087        db: &LoginDb,
1088        login: &EncryptedLogin,
1089        server_modified: &ServerTimestamp,
1090        is_overridden: bool,
1091    ) -> Result<()> {
1092        let sql = "
1093            INSERT OR IGNORE INTO loginsM (
1094                is_overridden,
1095                server_modified,
1096
1097                httpRealm,
1098                formActionOrigin,
1099                usernameField,
1100                passwordField,
1101                secFields,
1102                origin,
1103
1104                timesUsed,
1105                timeLastUsed,
1106                timePasswordChanged,
1107                timeCreated,
1108
1109                timeOfLastBreach,
1110                timeLastBreachAlertDismissed,
1111
1112                guid
1113            ) VALUES (
1114                :is_overridden,
1115                :server_modified,
1116
1117                :http_realm,
1118                :form_action_origin,
1119                :username_field,
1120                :password_field,
1121                :sec_fields,
1122                :origin,
1123
1124                :times_used,
1125                :time_last_used,
1126                :time_password_changed,
1127                :time_created,
1128
1129                :time_of_last_breach,
1130                :time_last_breach_alert_dismissed,
1131
1132                :guid
1133            )";
1134        let mut stmt = db.prepare_cached(sql)?;
1135
1136        stmt.execute(named_params! {
1137            ":is_overridden": is_overridden,
1138            ":server_modified": server_modified.as_millis(),
1139            ":http_realm": login.fields.http_realm,
1140            ":form_action_origin": login.fields.form_action_origin,
1141            ":username_field": login.fields.username_field,
1142            ":password_field": login.fields.password_field,
1143            ":origin": login.fields.origin,
1144            ":sec_fields": login.sec_fields,
1145            ":times_used": login.meta.times_used,
1146            ":time_last_used": login.meta.time_last_used,
1147            ":time_password_changed": login.meta.time_password_changed,
1148            ":time_created": login.meta.time_created,
1149            ":time_of_last_breach": login.fields.time_of_last_breach,
1150            ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed,
1151            ":guid": login.guid_str(),
1152        })?;
1153        Ok(())
1154    }
1155
1156    pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1157        get_guids(db, "SELECT guid FROM loginsL")
1158    }
1159
1160    pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1161        get_guids(db, "SELECT guid FROM loginsM")
1162    }
1163
1164    fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1165        let mut stmt = db.prepare_cached(sql).unwrap();
1166        let mut res: Vec<String> = stmt
1167            .query_map([], |r| r.get(0))
1168            .unwrap()
1169            .map(|r| r.unwrap())
1170            .collect();
1171        res.sort();
1172        res
1173    }
1174
1175    pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1176        db.conn_ext_query_one(&format!(
1177            "SELECT server_modified FROM loginsM WHERE guid='{}'",
1178            guid
1179        ))
1180        .unwrap()
1181    }
1182
1183    pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1184        let row: (String, i64, bool) = db
1185            .query_row(
1186                "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1187                [guid],
1188                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1189            )
1190            .unwrap();
1191        let enc: SecureLoginFields = decrypt_struct(row.0);
1192        assert_eq!(enc.password, password);
1193        assert!(row.1 >= local_modified_gte);
1194        assert!(!row.2);
1195    }
1196
1197    pub fn check_mirror_login(
1198        db: &LoginDb,
1199        guid: &str,
1200        password: &str,
1201        server_modified: i64,
1202        is_overridden: bool,
1203    ) {
1204        let row: (String, i64, bool) = db
1205            .query_row(
1206                "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1207                [guid],
1208                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1209            )
1210            .unwrap();
1211        let enc: SecureLoginFields = decrypt_struct(row.0);
1212        assert_eq!(enc.password, password);
1213        assert_eq!(row.1, server_modified);
1214        assert_eq!(row.2, is_overridden);
1215    }
1216}
1217
1218#[cfg(not(feature = "keydb"))]
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1223    use crate::encryption::test_utils::TEST_ENCDEC;
1224    use crate::sync::merge::LocalLogin;
1225    use nss::ensure_initialized;
1226    use std::{thread, time};
1227
1228    #[test]
1229    fn test_username_dupe_semantics() {
1230        ensure_initialized();
1231        let mut login = LoginEntry {
1232            origin: "https://www.example.com".into(),
1233            http_realm: Some("https://www.example.com".into()),
1234            username: "test".into(),
1235            password: "sekret".into(),
1236            ..LoginEntry::default()
1237        };
1238
1239        let db = LoginDb::open_in_memory();
1240        db.add(login.clone(), &*TEST_ENCDEC)
1241            .expect("should be able to add first login");
1242
1243        // We will reject new logins with the same username value...
1244        let exp_err = "Invalid login: Login already exists";
1245        assert_eq!(
1246            db.add(login.clone(), &*TEST_ENCDEC)
1247                .unwrap_err()
1248                .to_string(),
1249            exp_err
1250        );
1251
1252        // Add one with an empty username - not a dupe.
1253        login.username = "".to_string();
1254        db.add(login.clone(), &*TEST_ENCDEC)
1255            .expect("empty login isn't a dupe");
1256
1257        assert_eq!(
1258            db.add(login, &*TEST_ENCDEC).unwrap_err().to_string(),
1259            exp_err
1260        );
1261
1262        // one with a username, 1 without.
1263        assert_eq!(db.get_all().unwrap().len(), 2);
1264    }
1265
1266    #[test]
1267    fn test_add_many() {
1268        ensure_initialized();
1269
1270        let login_a = LoginEntry {
1271            origin: "https://a.example.com".into(),
1272            http_realm: Some("https://www.example.com".into()),
1273            username: "test".into(),
1274            password: "sekret".into(),
1275            ..LoginEntry::default()
1276        };
1277
1278        let login_b = LoginEntry {
1279            origin: "https://b.example.com".into(),
1280            http_realm: Some("https://www.example.com".into()),
1281            username: "test".into(),
1282            password: "sekret".into(),
1283            ..LoginEntry::default()
1284        };
1285
1286        let db = LoginDb::open_in_memory();
1287        let added = db
1288            .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1289            .expect("should be able to add logins");
1290
1291        let [added_a, added_b] = added.as_slice() else {
1292            panic!("there should really be 2")
1293        };
1294
1295        let fetched_a = db
1296            .get_by_id(&added_a.as_ref().unwrap().meta.id)
1297            .expect("should work")
1298            .expect("should get a record");
1299
1300        assert_eq!(fetched_a.fields.origin, login_a.origin);
1301
1302        let fetched_b = db
1303            .get_by_id(&added_b.as_ref().unwrap().meta.id)
1304            .expect("should work")
1305            .expect("should get a record");
1306
1307        assert_eq!(fetched_b.fields.origin, login_b.origin);
1308
1309        assert_eq!(db.count_all().unwrap(), 2);
1310    }
1311
1312    #[test]
1313    fn test_count_by_origin() {
1314        ensure_initialized();
1315
1316        let origin_a = "https://a.example.com";
1317        let login_a = LoginEntry {
1318            origin: origin_a.into(),
1319            http_realm: Some("https://www.example.com".into()),
1320            username: "test".into(),
1321            password: "sekret".into(),
1322            ..LoginEntry::default()
1323        };
1324
1325        let login_b = LoginEntry {
1326            origin: "https://b.example.com".into(),
1327            http_realm: Some("https://www.example.com".into()),
1328            username: "test".into(),
1329            password: "sekret".into(),
1330            ..LoginEntry::default()
1331        };
1332
1333        let origin_umlaut = "https://bücher.example.com";
1334        let login_umlaut = LoginEntry {
1335            origin: origin_umlaut.into(),
1336            http_realm: Some("https://www.example.com".into()),
1337            username: "test".into(),
1338            password: "sekret".into(),
1339            ..LoginEntry::default()
1340        };
1341
1342        let db = LoginDb::open_in_memory();
1343        db.add_many(
1344            vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1345            &*TEST_ENCDEC,
1346        )
1347        .expect("should be able to add logins");
1348
1349        assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1350        assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1351    }
1352
1353    #[test]
1354    fn test_count_by_form_action_origin() {
1355        ensure_initialized();
1356
1357        let origin_a = "https://a.example.com";
1358        let login_a = LoginEntry {
1359            origin: origin_a.into(),
1360            form_action_origin: Some(origin_a.into()),
1361            http_realm: Some("https://www.example.com".into()),
1362            username: "test".into(),
1363            password: "sekret".into(),
1364            ..LoginEntry::default()
1365        };
1366
1367        let login_b = LoginEntry {
1368            origin: "https://b.example.com".into(),
1369            form_action_origin: Some("https://b.example.com".into()),
1370            http_realm: Some("https://www.example.com".into()),
1371            username: "test".into(),
1372            password: "sekret".into(),
1373            ..LoginEntry::default()
1374        };
1375
1376        let origin_umlaut = "https://bücher.example.com";
1377        let login_umlaut = LoginEntry {
1378            origin: origin_umlaut.into(),
1379            form_action_origin: Some(origin_umlaut.into()),
1380            http_realm: Some("https://www.example.com".into()),
1381            username: "test".into(),
1382            password: "sekret".into(),
1383            ..LoginEntry::default()
1384        };
1385
1386        let db = LoginDb::open_in_memory();
1387        db.add_many(
1388            vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1389            &*TEST_ENCDEC,
1390        )
1391        .expect("should be able to add logins");
1392
1393        assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1394        assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1395    }
1396
1397    #[test]
1398    fn test_add_many_with_failed_constraint() {
1399        ensure_initialized();
1400
1401        let login_a = LoginEntry {
1402            origin: "https://example.com".into(),
1403            http_realm: Some("https://www.example.com".into()),
1404            username: "test".into(),
1405            password: "sekret".into(),
1406            ..LoginEntry::default()
1407        };
1408
1409        let login_b = LoginEntry {
1410            // same origin will result in duplicate error
1411            origin: "https://example.com".into(),
1412            http_realm: Some("https://www.example.com".into()),
1413            username: "test".into(),
1414            password: "sekret".into(),
1415            ..LoginEntry::default()
1416        };
1417
1418        let db = LoginDb::open_in_memory();
1419        let added = db
1420            .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1421            .expect("should be able to add logins");
1422
1423        let [added_a, added_b] = added.as_slice() else {
1424            panic!("there should really be 2")
1425        };
1426
1427        // first entry has been saved successfully
1428        let fetched_a = db
1429            .get_by_id(&added_a.as_ref().unwrap().meta.id)
1430            .expect("should work")
1431            .expect("should get a record");
1432
1433        assert_eq!(fetched_a.fields.origin, login_a.origin);
1434
1435        // second entry failed
1436        assert!(!added_b.is_ok());
1437    }
1438
1439    #[test]
1440    fn test_add_with_meta() {
1441        ensure_initialized();
1442
1443        let guid = Guid::random();
1444        let now_ms = util::system_time_ms_i64(SystemTime::now());
1445        let login = LoginEntry {
1446            origin: "https://www.example.com".into(),
1447            http_realm: Some("https://www.example.com".into()),
1448            username: "test".into(),
1449            password: "sekret".into(),
1450            ..LoginEntry::default()
1451        };
1452        let meta = LoginMeta {
1453            id: guid.to_string(),
1454            time_created: now_ms,
1455            time_password_changed: now_ms + 100,
1456            time_last_used: now_ms + 10,
1457            times_used: 42,
1458        };
1459
1460        let db = LoginDb::open_in_memory();
1461        let entry_with_meta = LoginEntryWithMeta {
1462            entry: login.clone(),
1463            meta: meta.clone(),
1464        };
1465
1466        db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1467            .expect("should be able to add login with record");
1468
1469        let fetched = db
1470            .get_by_id(&guid)
1471            .expect("should work")
1472            .expect("should get a record");
1473
1474        assert_eq!(fetched.meta, meta);
1475    }
1476
1477    #[test]
1478    fn test_add_with_meta_deleted() {
1479        ensure_initialized();
1480
1481        let guid = Guid::random();
1482        let now_ms = util::system_time_ms_i64(SystemTime::now());
1483        let login = LoginEntry {
1484            origin: "https://www.example.com".into(),
1485            http_realm: Some("https://www.example.com".into()),
1486            username: "test".into(),
1487            password: "sekret".into(),
1488            ..LoginEntry::default()
1489        };
1490        let meta = LoginMeta {
1491            id: guid.to_string(),
1492            time_created: now_ms,
1493            time_password_changed: now_ms + 100,
1494            time_last_used: now_ms + 10,
1495            times_used: 42,
1496        };
1497
1498        let db = LoginDb::open_in_memory();
1499        let entry_with_meta = LoginEntryWithMeta {
1500            entry: login.clone(),
1501            meta: meta.clone(),
1502        };
1503
1504        db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1505            .expect("should be able to add login with record");
1506
1507        db.delete(&guid).expect("should be able to delete login");
1508
1509        let entry_with_meta2 = LoginEntryWithMeta {
1510            entry: login.clone(),
1511            meta: meta.clone(),
1512        };
1513
1514        db.add_with_meta(entry_with_meta2, &*TEST_ENCDEC)
1515            .expect("should be able to re-add login with record");
1516
1517        let fetched = db
1518            .get_by_id(&guid)
1519            .expect("should work")
1520            .expect("should get a record");
1521
1522        assert_eq!(fetched.meta, meta);
1523    }
1524
1525    #[test]
1526    fn test_unicode_submit() {
1527        ensure_initialized();
1528        let db = LoginDb::open_in_memory();
1529        let added = db
1530            .add(
1531                LoginEntry {
1532                    form_action_origin: Some("http://😍.com".into()),
1533                    origin: "http://😍.com".into(),
1534                    http_realm: None,
1535                    username_field: "😍".into(),
1536                    password_field: "😍".into(),
1537                    username: "😍".into(),
1538                    password: "😍".into(),
1539                },
1540                &*TEST_ENCDEC,
1541            )
1542            .unwrap();
1543        let fetched = db
1544            .get_by_id(&added.meta.id)
1545            .expect("should work")
1546            .expect("should get a record");
1547        assert_eq!(added, fetched);
1548        assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1549        assert_eq!(
1550            fetched.fields.form_action_origin,
1551            Some("http://xn--r28h.com".to_string())
1552        );
1553        assert_eq!(fetched.fields.username_field, "😍");
1554        assert_eq!(fetched.fields.password_field, "😍");
1555        let sec_fields = fetched.decrypt_fields(&*TEST_ENCDEC).unwrap();
1556        assert_eq!(sec_fields.username, "😍");
1557        assert_eq!(sec_fields.password, "😍");
1558    }
1559
1560    #[test]
1561    fn test_unicode_realm() {
1562        ensure_initialized();
1563        let db = LoginDb::open_in_memory();
1564        let added = db
1565            .add(
1566                LoginEntry {
1567                    form_action_origin: None,
1568                    origin: "http://😍.com".into(),
1569                    http_realm: Some("😍😍".into()),
1570                    username: "😍".into(),
1571                    password: "😍".into(),
1572                    ..Default::default()
1573                },
1574                &*TEST_ENCDEC,
1575            )
1576            .unwrap();
1577        let fetched = db
1578            .get_by_id(&added.meta.id)
1579            .expect("should work")
1580            .expect("should get a record");
1581        assert_eq!(added, fetched);
1582        assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1583        assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1584    }
1585
1586    fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1587        let mut results = db
1588            .get_by_base_domain(query)
1589            .unwrap()
1590            .into_iter()
1591            .map(|l| l.fields.origin)
1592            .collect::<Vec<String>>();
1593        results.sort_unstable();
1594        let mut sorted = expected.to_owned();
1595        sorted.sort_unstable();
1596        assert_eq!(sorted, results);
1597    }
1598
1599    fn check_good_bad(
1600        good: Vec<&str>,
1601        bad: Vec<&str>,
1602        good_queries: Vec<&str>,
1603        zero_queries: Vec<&str>,
1604    ) {
1605        let db = LoginDb::open_in_memory();
1606        for h in good.iter().chain(bad.iter()) {
1607            db.add(
1608                LoginEntry {
1609                    origin: (*h).into(),
1610                    http_realm: Some((*h).into()),
1611                    password: "test".into(),
1612                    ..Default::default()
1613                },
1614                &*TEST_ENCDEC,
1615            )
1616            .unwrap();
1617        }
1618        for query in good_queries {
1619            check_matches(&db, query, &good);
1620        }
1621        for query in zero_queries {
1622            check_matches(&db, query, &[]);
1623        }
1624    }
1625
1626    #[test]
1627    fn test_get_by_base_domain_invalid() {
1628        check_good_bad(
1629            vec!["https://example.com"],
1630            vec![],
1631            vec![],
1632            vec!["invalid query"],
1633        );
1634    }
1635
1636    #[test]
1637    fn test_get_by_base_domain() {
1638        check_good_bad(
1639            vec![
1640                "https://example.com",
1641                "https://www.example.com",
1642                "http://www.example.com",
1643                "http://www.example.com:8080",
1644                "http://sub.example.com:8080",
1645                "https://sub.example.com:8080",
1646                "https://sub.sub.example.com",
1647                "ftp://sub.example.com",
1648            ],
1649            vec![
1650                "https://badexample.com",
1651                "https://example.co",
1652                "https://example.com.au",
1653            ],
1654            vec!["example.com"],
1655            vec!["foo.com"],
1656        );
1657    }
1658
1659    #[test]
1660    fn test_get_by_base_domain_punicode() {
1661        // punycode! This is likely to need adjusting once we normalize
1662        // on insert.
1663        check_good_bad(
1664            vec![
1665                "http://xn--r28h.com", // punycoded version of "http://😍.com"
1666            ],
1667            vec!["http://💖.com"],
1668            vec!["😍.com", "xn--r28h.com"],
1669            vec![],
1670        );
1671    }
1672
1673    #[test]
1674    fn test_get_by_base_domain_ipv4() {
1675        check_good_bad(
1676            vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1677            vec!["https://127.0.0.0", "https://example.com"],
1678            vec!["127.0.0.1"],
1679            vec!["127.0.0.2"],
1680        );
1681    }
1682
1683    #[test]
1684    fn test_get_by_base_domain_ipv6() {
1685        check_good_bad(
1686            vec!["http://[::1]", "https://[::1]:8000"],
1687            vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1688            vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1689            vec!["[0:0:0:0:0:0:1:2]"],
1690        );
1691    }
1692
1693    #[test]
1694    fn test_add() {
1695        ensure_initialized();
1696        let db = LoginDb::open_in_memory();
1697        let to_add = LoginEntry {
1698            origin: "https://www.example.com".into(),
1699            http_realm: Some("https://www.example.com".into()),
1700            username: "test_user".into(),
1701            password: "test_password".into(),
1702            ..Default::default()
1703        };
1704        let login = db.add(to_add, &*TEST_ENCDEC).unwrap();
1705        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1706
1707        assert_eq!(login.fields.origin, login2.fields.origin);
1708        assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1709        assert_eq!(login.sec_fields, login2.sec_fields);
1710    }
1711
1712    #[test]
1713    fn test_update() {
1714        ensure_initialized();
1715        let db = LoginDb::open_in_memory();
1716        let login = db
1717            .add(
1718                LoginEntry {
1719                    origin: "https://www.example.com".into(),
1720                    http_realm: Some("https://www.example.com".into()),
1721                    username: "user1".into(),
1722                    password: "password1".into(),
1723                    ..Default::default()
1724                },
1725                &*TEST_ENCDEC,
1726            )
1727            .unwrap();
1728        db.update(
1729            &login.meta.id,
1730            LoginEntry {
1731                origin: "https://www.example2.com".into(),
1732                http_realm: Some("https://www.example2.com".into()),
1733                username: "user2".into(),
1734                password: "password2".into(),
1735                ..Default::default() // TODO: check and fix if needed
1736            },
1737            &*TEST_ENCDEC,
1738        )
1739        .unwrap();
1740
1741        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1742
1743        assert_eq!(login2.fields.origin, "https://www.example2.com");
1744        assert_eq!(
1745            login2.fields.http_realm,
1746            Some("https://www.example2.com".into())
1747        );
1748        let sec_fields = login2.decrypt_fields(&*TEST_ENCDEC).unwrap();
1749        assert_eq!(sec_fields.username, "user2");
1750        assert_eq!(sec_fields.password, "password2");
1751    }
1752
1753    #[test]
1754    fn test_touch() {
1755        ensure_initialized();
1756        let db = LoginDb::open_in_memory();
1757        let login = db
1758            .add(
1759                LoginEntry {
1760                    origin: "https://www.example.com".into(),
1761                    http_realm: Some("https://www.example.com".into()),
1762                    username: "user1".into(),
1763                    password: "password1".into(),
1764                    ..Default::default()
1765                },
1766                &*TEST_ENCDEC,
1767            )
1768            .unwrap();
1769        // Simulate touch happening at another "time"
1770        thread::sleep(time::Duration::from_millis(50));
1771        db.touch(&login.meta.id).unwrap();
1772        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1773        assert!(login2.meta.time_last_used > login.meta.time_last_used);
1774        assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1775    }
1776
1777    #[test]
1778    fn test_breach_alerts() {
1779        ensure_initialized();
1780        let db = LoginDb::open_in_memory();
1781        let login = db
1782            .add(
1783                LoginEntry {
1784                    origin: "https://www.example.com".into(),
1785                    http_realm: Some("https://www.example.com".into()),
1786                    username: "user1".into(),
1787                    password: "password1".into(),
1788                    ..Default::default()
1789                },
1790                &*TEST_ENCDEC,
1791            )
1792            .unwrap();
1793        // initial state
1794        assert!(login.fields.time_of_last_breach.is_none());
1795        assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1796        assert!(login.fields.time_last_breach_alert_dismissed.is_none());
1797
1798        // Wait and use a time that's definitely after password was changed
1799        thread::sleep(time::Duration::from_millis(50));
1800        let breach_time = util::system_time_ms_i64(SystemTime::now());
1801        db.record_breach(&login.meta.id, breach_time).unwrap();
1802        assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1803        let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1804        assert!(login1.fields.time_of_last_breach.is_some());
1805
1806        // dismiss
1807        db.record_breach_alert_dismissal(&login.meta.id).unwrap();
1808        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1809        assert!(login2.fields.time_last_breach_alert_dismissed.is_some());
1810
1811        // reset
1812        db.reset_all_breaches().unwrap();
1813        assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1814        let login3 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1815        assert!(login3.fields.time_of_last_breach.is_none());
1816
1817        // Wait and use a time that's definitely after password was changed
1818        thread::sleep(time::Duration::from_millis(50));
1819        let breach_time = util::system_time_ms_i64(SystemTime::now());
1820        db.record_breach(&login.meta.id, breach_time).unwrap();
1821        assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1822
1823        // now change password
1824        db.update(
1825            &login.meta.id.clone(),
1826            LoginEntry {
1827                password: "changed-password".into(),
1828                ..login.clone().decrypt(&*TEST_ENCDEC).unwrap().entry()
1829            },
1830            &*TEST_ENCDEC,
1831        )
1832        .unwrap();
1833        // not breached anymore
1834        assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1835    }
1836
1837    #[test]
1838    fn test_breach_alert_fields_not_overwritten_by_update() {
1839        ensure_initialized();
1840        let db = LoginDb::open_in_memory();
1841        let login = db
1842            .add(
1843                LoginEntry {
1844                    origin: "https://www.example.com".into(),
1845                    http_realm: Some("https://www.example.com".into()),
1846                    username: "user1".into(),
1847                    password: "password1".into(),
1848                    ..Default::default()
1849                },
1850                &*TEST_ENCDEC,
1851            )
1852            .unwrap();
1853        assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1854
1855        // Wait and use a time that's definitely after password was changed
1856        thread::sleep(time::Duration::from_millis(50));
1857        let breach_time = util::system_time_ms_i64(SystemTime::now());
1858        db.record_breach(&login.meta.id, breach_time).unwrap();
1859        assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1860
1861        // change some fields
1862        db.update(
1863            &login.meta.id.clone(),
1864            LoginEntry {
1865                username_field: "changed-username-field".into(),
1866                ..login.clone().decrypt(&*TEST_ENCDEC).unwrap().entry()
1867            },
1868            &*TEST_ENCDEC,
1869        )
1870        .unwrap();
1871
1872        // breach still present
1873        assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1874    }
1875
1876    #[test]
1877    fn test_breach_alert_dismissal_with_specific_timestamp() {
1878        ensure_initialized();
1879        let db = LoginDb::open_in_memory();
1880        let login = db
1881            .add(
1882                LoginEntry {
1883                    origin: "https://www.example.com".into(),
1884                    http_realm: Some("https://www.example.com".into()),
1885                    username: "user1".into(),
1886                    password: "password1".into(),
1887                    ..Default::default()
1888                },
1889                &*TEST_ENCDEC,
1890            )
1891            .unwrap();
1892
1893        // Record a breach that happened after password was created
1894        // Use a timestamp that's definitely after the login's timePasswordChanged
1895        let breach_time = login.meta.time_password_changed + 1000;
1896        db.record_breach(&login.meta.id, breach_time).unwrap();
1897        assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1898
1899        // Dismiss with a specific timestamp after the breach
1900        let dismiss_time = breach_time + 500;
1901        db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
1902            .unwrap();
1903
1904        // Verify the exact timestamp was stored
1905        let retrieved = db
1906            .get_by_id(&login.meta.id)
1907            .unwrap()
1908            .unwrap()
1909            .decrypt(&*TEST_ENCDEC)
1910            .unwrap();
1911        assert_eq!(
1912            retrieved.time_last_breach_alert_dismissed,
1913            Some(dismiss_time)
1914        );
1915
1916        // Verify the breach alert is considered dismissed
1917        assert!(db.is_breach_alert_dismissed(&login.meta.id).unwrap());
1918
1919        // Test that dismissing before the breach time means it's not dismissed
1920        let earlier_dismiss_time = breach_time - 100;
1921        db.record_breach_alert_dismissal_time(&login.meta.id, earlier_dismiss_time)
1922            .unwrap();
1923        assert!(!db.is_breach_alert_dismissed(&login.meta.id).unwrap());
1924    }
1925
1926    #[test]
1927    fn test_delete() {
1928        ensure_initialized();
1929        let db = LoginDb::open_in_memory();
1930        let login = db
1931            .add(
1932                LoginEntry {
1933                    origin: "https://www.example.com".into(),
1934                    http_realm: Some("https://www.example.com".into()),
1935                    username: "test_user".into(),
1936                    password: "test_password".into(),
1937                    ..Default::default()
1938                },
1939                &*TEST_ENCDEC,
1940            )
1941            .unwrap();
1942
1943        assert!(db.delete(login.guid_str()).unwrap());
1944
1945        let local_login = db
1946            .query_row(
1947                "SELECT * FROM loginsL WHERE guid = :guid",
1948                named_params! { ":guid": login.guid_str() },
1949                |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
1950            )
1951            .unwrap();
1952        assert_eq!(local_login.fields.http_realm, None);
1953        assert_eq!(local_login.fields.form_action_origin, None);
1954
1955        assert!(!db.exists(login.guid_str()).unwrap());
1956    }
1957
1958    #[test]
1959    fn test_delete_many() {
1960        ensure_initialized();
1961        let db = LoginDb::open_in_memory();
1962
1963        let login_a = db
1964            .add(
1965                LoginEntry {
1966                    origin: "https://a.example.com".into(),
1967                    http_realm: Some("https://www.example.com".into()),
1968                    username: "test_user".into(),
1969                    password: "test_password".into(),
1970                    ..Default::default()
1971                },
1972                &*TEST_ENCDEC,
1973            )
1974            .unwrap();
1975
1976        let login_b = db
1977            .add(
1978                LoginEntry {
1979                    origin: "https://b.example.com".into(),
1980                    http_realm: Some("https://www.example.com".into()),
1981                    username: "test_user".into(),
1982                    password: "test_password".into(),
1983                    ..Default::default()
1984                },
1985                &*TEST_ENCDEC,
1986            )
1987            .unwrap();
1988
1989        let result = db
1990            .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
1991            .unwrap();
1992        assert!(result[0]);
1993        assert!(result[1]);
1994        assert!(!db.exists(login_a.guid_str()).unwrap());
1995        assert!(!db.exists(login_b.guid_str()).unwrap());
1996    }
1997
1998    #[test]
1999    fn test_subsequent_delete_many() {
2000        ensure_initialized();
2001        let db = LoginDb::open_in_memory();
2002
2003        let login = db
2004            .add(
2005                LoginEntry {
2006                    origin: "https://a.example.com".into(),
2007                    http_realm: Some("https://www.example.com".into()),
2008                    username: "test_user".into(),
2009                    password: "test_password".into(),
2010                    ..Default::default()
2011                },
2012                &*TEST_ENCDEC,
2013            )
2014            .unwrap();
2015
2016        let result = db.delete_many(vec![login.guid_str()]).unwrap();
2017        assert!(result[0]);
2018        assert!(!db.exists(login.guid_str()).unwrap());
2019
2020        let result = db.delete_many(vec![login.guid_str()]).unwrap();
2021        assert!(!result[0]);
2022    }
2023
2024    #[test]
2025    fn test_delete_many_with_non_existent_id() {
2026        ensure_initialized();
2027        let db = LoginDb::open_in_memory();
2028
2029        let result = db.delete_many(vec![&Guid::random()]).unwrap();
2030        assert!(!result[0]);
2031    }
2032
2033    #[test]
2034    fn test_delete_local_for_remote_replacement() {
2035        ensure_initialized();
2036        let db = LoginDb::open_in_memory();
2037        let login = db
2038            .add(
2039                LoginEntry {
2040                    origin: "https://www.example.com".into(),
2041                    http_realm: Some("https://www.example.com".into()),
2042                    username: "test_user".into(),
2043                    password: "test_password".into(),
2044                    ..Default::default()
2045                },
2046                &*TEST_ENCDEC,
2047            )
2048            .unwrap();
2049
2050        let result = db
2051            .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2052            .unwrap();
2053
2054        let local_guids = get_local_guids(&db);
2055        assert_eq!(local_guids.len(), 0);
2056
2057        let mirror_guids = get_mirror_guids(&db);
2058        assert_eq!(mirror_guids.len(), 0);
2059
2060        assert_eq!(result.local_deleted, 1);
2061    }
2062
2063    mod test_find_login_to_update {
2064        use super::*;
2065
2066        fn make_entry(username: &str, password: &str) -> LoginEntry {
2067            LoginEntry {
2068                origin: "https://www.example.com".into(),
2069                http_realm: Some("the website".into()),
2070                username: username.into(),
2071                password: password.into(),
2072                ..Default::default()
2073            }
2074        }
2075
2076        fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2077            db.add(make_entry(username, password), &*TEST_ENCDEC)
2078                .unwrap()
2079                .decrypt(&*TEST_ENCDEC)
2080                .unwrap()
2081        }
2082
2083        #[test]
2084        fn test_match() {
2085            ensure_initialized();
2086            let db = LoginDb::open_in_memory();
2087            let login = make_saved_login(&db, "user", "pass");
2088            assert_eq!(
2089                Some(login),
2090                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2091                    .unwrap(),
2092            );
2093        }
2094
2095        #[test]
2096        fn test_non_matches() {
2097            ensure_initialized();
2098            let db = LoginDb::open_in_memory();
2099            // Non-match because the username is different
2100            make_saved_login(&db, "other-user", "pass");
2101            // Non-match because the http_realm is different
2102            db.add(
2103                LoginEntry {
2104                    origin: "https://www.example.com".into(),
2105                    http_realm: Some("the other website".into()),
2106                    username: "user".into(),
2107                    password: "pass".into(),
2108                    ..Default::default()
2109                },
2110                &*TEST_ENCDEC,
2111            )
2112            .unwrap();
2113            // Non-match because it uses form_action_origin instead of http_realm
2114            db.add(
2115                LoginEntry {
2116                    origin: "https://www.example.com".into(),
2117                    form_action_origin: Some("https://www.example.com/".into()),
2118                    username: "user".into(),
2119                    password: "pass".into(),
2120                    ..Default::default()
2121                },
2122                &*TEST_ENCDEC,
2123            )
2124            .unwrap();
2125            assert_eq!(
2126                None,
2127                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2128                    .unwrap(),
2129            );
2130        }
2131
2132        #[test]
2133        fn test_match_blank_password() {
2134            ensure_initialized();
2135            let db = LoginDb::open_in_memory();
2136            let login = make_saved_login(&db, "", "pass");
2137            assert_eq!(
2138                Some(login),
2139                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2140                    .unwrap(),
2141            );
2142        }
2143
2144        #[test]
2145        fn test_username_match_takes_precedence_over_blank_username() {
2146            ensure_initialized();
2147            let db = LoginDb::open_in_memory();
2148            make_saved_login(&db, "", "pass");
2149            let username_match = make_saved_login(&db, "user", "pass");
2150            assert_eq!(
2151                Some(username_match),
2152                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2153                    .unwrap(),
2154            );
2155        }
2156
2157        #[test]
2158        fn test_invalid_login() {
2159            ensure_initialized();
2160            let db = LoginDb::open_in_memory();
2161            assert!(db
2162                .find_login_to_update(
2163                    LoginEntry {
2164                        http_realm: None,
2165                        form_action_origin: None,
2166                        ..LoginEntry::default()
2167                    },
2168                    &*TEST_ENCDEC
2169                )
2170                .is_err());
2171        }
2172
2173        #[test]
2174        fn test_update_with_duplicate_login() {
2175            ensure_initialized();
2176            // If we have duplicate logins in the database, it should be possible to update them
2177            // without triggering a DuplicateLogin error
2178            let db = LoginDb::open_in_memory();
2179            let login = make_saved_login(&db, "user", "pass");
2180            let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2181            dupe.meta.id = "different-guid".to_string();
2182            db.insert_new_login(&dupe).unwrap();
2183
2184            let mut entry = login.entry();
2185            entry.password = "pass2".to_string();
2186            db.update(&login.id, entry, &*TEST_ENCDEC).unwrap();
2187
2188            let mut entry = login.entry();
2189            entry.password = "pass3".to_string();
2190            db.add_or_update(entry, &*TEST_ENCDEC).unwrap();
2191        }
2192    }
2193}