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        let mut stmt = self.db.prepare_cached(&COUNT_BY_ORIGIN_SQL)?;
154
155        let count: i64 = stmt.query_row(named_params! { ":origin": origin }, |row| row.get(0))?;
156        Ok(count)
157    }
158
159    pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> Result<i64> {
160        let mut stmt = self.db.prepare_cached(&COUNT_BY_FORM_ACTION_ORIGIN_SQL)?;
161
162        let count: i64 = stmt.query_row(
163            named_params! { ":form_action_origin": form_action_origin },
164            |row| row.get(0),
165        )?;
166        Ok(count)
167    }
168
169    pub fn get_all(&self) -> Result<Vec<EncryptedLogin>> {
170        let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
171        let rows = stmt.query_and_then([], EncryptedLogin::from_row)?;
172        rows.collect::<Result<_>>()
173    }
174
175    pub fn get_by_base_domain(&self, base_domain: &str) -> Result<Vec<EncryptedLogin>> {
176        // We first parse the input string as a host so it is normalized.
177        let base_host = match Host::parse(base_domain) {
178            Ok(d) => d,
179            Err(e) => {
180                // don't log the input string as it's PII.
181                warn!("get_by_base_domain was passed an invalid domain: {}", e);
182                return Ok(vec![]);
183            }
184        };
185        // We just do a linear scan. Another option is to have an indexed
186        // reverse-host column or similar, but current thinking is that it's
187        // extra complexity for (probably) zero actual benefit given the record
188        // counts are expected to be so low.
189        // A regex would probably make this simpler, but we don't want to drag
190        // in a regex lib just for this.
191        let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
192        let rows = stmt
193            .query_and_then([], EncryptedLogin::from_row)?
194            .filter(|r| {
195                let login = r
196                    .as_ref()
197                    .ok()
198                    .and_then(|login| Url::parse(&login.fields.origin).ok());
199                let this_host = login.as_ref().and_then(|url| url.host());
200                match (&base_host, this_host) {
201                    (Host::Domain(base), Some(Host::Domain(look))) => {
202                        // a fairly long-winded way of saying
203                        // `login.fields.origin == base_domain ||
204                        //  login.fields.origin.ends_with('.' + base_domain);`
205                        let mut rev_input = base.chars().rev();
206                        let mut rev_host = look.chars().rev();
207                        loop {
208                            match (rev_input.next(), rev_host.next()) {
209                                (Some(ref a), Some(ref b)) if a == b => continue,
210                                (None, None) => return true, // exactly equal
211                                (None, Some(ref h)) => return *h == '.',
212                                _ => return false,
213                            }
214                        }
215                    }
216                    // ip addresses must match exactly.
217                    (Host::Ipv4(base), Some(Host::Ipv4(look))) => *base == look,
218                    (Host::Ipv6(base), Some(Host::Ipv6(look))) => *base == look,
219                    // all "mismatches" in domain types are false.
220                    _ => false,
221                }
222            });
223        rows.collect::<Result<_>>()
224    }
225
226    pub fn get_by_id(&self, id: &str) -> Result<Option<EncryptedLogin>> {
227        self.try_query_row(
228            &GET_BY_GUID_SQL,
229            &[(":guid", &id as &dyn ToSql)],
230            EncryptedLogin::from_row,
231            true,
232        )
233    }
234
235    // Match a `LoginEntry` being saved to existing logins in the DB
236    //
237    // When a user is saving new login, there are several cases for how we want to save the data:
238    //
239    //  - Adding a new login: `None` will be returned
240    //  - Updating an existing login: `Some(login)` will be returned and the username will match
241    //    the one for look.
242    //  - Filling in a blank username for an existing login: `Some(login)` will be returned
243    //    with a blank username.
244    //
245    //  Returns an Err if the new login is not valid and could not be fixed up
246    pub fn find_login_to_update(
247        &self,
248        look: LoginEntry,
249        encdec: &dyn EncryptorDecryptor,
250    ) -> Result<Option<Login>> {
251        let look = look.fixup()?;
252        let logins = self
253            .get_by_entry_target(&look)?
254            .into_iter()
255            .map(|enc_login| enc_login.decrypt(encdec))
256            .collect::<Result<Vec<Login>>>()?;
257        Ok(logins
258            // First, try to match the username
259            .iter()
260            .find(|login| login.username == look.username)
261            // Fall back on a blank username
262            .or_else(|| logins.iter().find(|login| login.username.is_empty()))
263            // Clone the login to avoid ref issues when returning across the FFI
264            .cloned())
265    }
266
267    pub fn touch(&self, id: &str) -> Result<()> {
268        let tx = self.unchecked_transaction()?;
269        self.ensure_local_overlay_exists(id)?;
270        self.mark_mirror_overridden(id)?;
271        let now_ms = util::system_time_ms_i64(SystemTime::now());
272        // As on iOS, just using a record doesn't flip it's status to changed.
273        // TODO: this might be wrong for lockbox!
274        self.execute_cached(
275            "UPDATE loginsL
276             SET timeLastUsed = :now_millis,
277                 timesUsed = timesUsed + 1,
278                 local_modified = :now_millis
279             WHERE guid = :guid
280                 AND is_deleted = 0",
281            named_params! {
282                ":now_millis": now_ms,
283                ":guid": id,
284            },
285        )?;
286        tx.commit()?;
287        Ok(())
288    }
289
290    // The single place we insert new rows or update existing local rows.
291    // just the SQL - no validation or anything.
292    fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
293        let sql = format!(
294            "INSERT INTO loginsL (
295                origin,
296                httpRealm,
297                formActionOrigin,
298                usernameField,
299                passwordField,
300                timesUsed,
301                secFields,
302                guid,
303                timeCreated,
304                timeLastUsed,
305                timePasswordChanged,
306                local_modified,
307                is_deleted,
308                sync_status
309            ) VALUES (
310                :origin,
311                :http_realm,
312                :form_action_origin,
313                :username_field,
314                :password_field,
315                :times_used,
316                :sec_fields,
317                :guid,
318                :time_created,
319                :time_last_used,
320                :time_password_changed,
321                :local_modified,
322                0, -- is_deleted
323                {new} -- sync_status
324            )",
325            new = SyncStatus::New as u8
326        );
327
328        self.execute(
329            &sql,
330            named_params! {
331                ":origin": login.fields.origin,
332                ":http_realm": login.fields.http_realm,
333                ":form_action_origin": login.fields.form_action_origin,
334                ":username_field": login.fields.username_field,
335                ":password_field": login.fields.password_field,
336                ":time_created": login.meta.time_created,
337                ":times_used": login.meta.times_used,
338                ":time_last_used": login.meta.time_last_used,
339                ":time_password_changed": login.meta.time_password_changed,
340                ":local_modified": login.meta.time_created,
341                ":sec_fields": login.sec_fields,
342                ":guid": login.guid(),
343            },
344        )?;
345        Ok(())
346    }
347
348    fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
349        // assumes the "local overlay" exists, so the guid must too.
350        let sql = format!(
351            "UPDATE loginsL
352             SET local_modified      = :now_millis,
353                 timeLastUsed        = :time_last_used,
354                 timePasswordChanged = :time_password_changed,
355                 httpRealm           = :http_realm,
356                 formActionOrigin    = :form_action_origin,
357                 usernameField       = :username_field,
358                 passwordField       = :password_field,
359                 timesUsed           = :times_used,
360                 secFields           = :sec_fields,
361                 origin              = :origin,
362                 -- leave New records as they are, otherwise update them to `changed`
363                 sync_status         = max(sync_status, {changed})
364             WHERE guid = :guid",
365            changed = SyncStatus::Changed as u8
366        );
367
368        self.db.execute(
369            &sql,
370            named_params! {
371                ":origin": login.fields.origin,
372                ":http_realm": login.fields.http_realm,
373                ":form_action_origin": login.fields.form_action_origin,
374                ":username_field": login.fields.username_field,
375                ":password_field": login.fields.password_field,
376                ":time_last_used": login.meta.time_last_used,
377                ":times_used": login.meta.times_used,
378                ":time_password_changed": login.meta.time_password_changed,
379                ":sec_fields": login.sec_fields,
380                ":guid": &login.meta.id,
381                // time_last_used has been set to now.
382                ":now_millis": login.meta.time_last_used,
383            },
384        )?;
385        Ok(())
386    }
387
388    /// Adds multiple logins within a single transaction and returns the successfully saved logins.
389    pub fn add_many(
390        &self,
391        entries: Vec<LoginEntry>,
392        encdec: &dyn EncryptorDecryptor,
393    ) -> Result<Vec<Result<EncryptedLogin>>> {
394        let now_ms = util::system_time_ms_i64(SystemTime::now());
395
396        let entries_with_meta = entries
397            .into_iter()
398            .map(|entry| {
399                let guid = Guid::random();
400                LoginEntryWithMeta {
401                    entry,
402                    meta: LoginMeta {
403                        id: guid.to_string(),
404                        time_created: now_ms,
405                        time_password_changed: now_ms,
406                        time_last_used: now_ms,
407                        times_used: 1,
408                    },
409                }
410            })
411            .collect();
412
413        self.add_many_with_meta(entries_with_meta, encdec)
414    }
415
416    /// Adds multiple logins **including metadata** within a single transaction and returns the successfully saved logins.
417    /// Normally, you will use `add_many` instead, and AS Logins will take care of the metadata (setting timestamps, generating an ID) itself.
418    /// However, in some cases, this method is necessary, for example when migrating data from another store that already contains the metadata.
419    pub fn add_many_with_meta(
420        &self,
421        entries_with_meta: Vec<LoginEntryWithMeta>,
422        encdec: &dyn EncryptorDecryptor,
423    ) -> Result<Vec<Result<EncryptedLogin>>> {
424        let tx = self.unchecked_transaction()?;
425        let mut results = vec![];
426        for entry_with_meta in entries_with_meta {
427            let guid = Guid::from_string(entry_with_meta.meta.id.clone());
428            match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry, encdec) {
429                Ok(new_entry) => {
430                    let sec_fields = SecureLoginFields {
431                        username: new_entry.username,
432                        password: new_entry.password,
433                    }
434                    .encrypt(encdec, &entry_with_meta.meta.id)?;
435                    let encrypted_login = EncryptedLogin {
436                        meta: entry_with_meta.meta,
437                        fields: LoginFields {
438                            origin: new_entry.origin,
439                            form_action_origin: new_entry.form_action_origin,
440                            http_realm: new_entry.http_realm,
441                            username_field: new_entry.username_field,
442                            password_field: new_entry.password_field,
443                        },
444                        sec_fields,
445                    };
446                    let result = self
447                        .insert_new_login(&encrypted_login)
448                        .map(|_| encrypted_login);
449                    results.push(result);
450                }
451
452                Err(error) => results.push(Err(error)),
453            }
454        }
455        tx.commit()?;
456        Ok(results)
457    }
458
459    pub fn add(
460        &self,
461        entry: LoginEntry,
462        encdec: &dyn EncryptorDecryptor,
463    ) -> Result<EncryptedLogin> {
464        let guid = Guid::random();
465        let now_ms = util::system_time_ms_i64(SystemTime::now());
466
467        let entry_with_meta = LoginEntryWithMeta {
468            entry,
469            meta: LoginMeta {
470                id: guid.to_string(),
471                time_created: now_ms,
472                time_password_changed: now_ms,
473                time_last_used: now_ms,
474                times_used: 1,
475            },
476        };
477
478        self.add_with_meta(entry_with_meta, encdec)
479    }
480
481    /// Adds a login **including metadata**.
482    /// Normally, you will use `add` instead, and AS Logins will take care of the metadata (setting timestamps, generating an ID) itself.
483    /// However, in some cases, this method is necessary, for example when migrating data from another store that already contains the metadata.
484    pub fn add_with_meta(
485        &self,
486        entry_with_meta: LoginEntryWithMeta,
487        encdec: &dyn EncryptorDecryptor,
488    ) -> Result<EncryptedLogin> {
489        let mut results = self.add_many_with_meta(vec![entry_with_meta], encdec)?;
490        results.pop().expect("there should be a single result")
491    }
492
493    pub fn update(
494        &self,
495        sguid: &str,
496        entry: LoginEntry,
497        encdec: &dyn EncryptorDecryptor,
498    ) -> Result<EncryptedLogin> {
499        let guid = Guid::new(sguid);
500        let now_ms = util::system_time_ms_i64(SystemTime::now());
501        let tx = self.unchecked_transaction()?;
502
503        let entry = entry.fixup()?;
504
505        // Check if there's an existing login that's the dupe of this login.  That indicates that
506        // something has gone wrong with our underlying logic.  However, if we do see a dupe login,
507        // just log an error and continue.  This avoids a crash on android-components
508        // (mozilla-mobile/android-components#11251).
509
510        if self.check_for_dupes(&guid, &entry, encdec).is_err() {
511            // Try to detect if sync is enabled by checking if there are any mirror logins
512            let has_mirror_row: bool =
513                self.db.query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
514            let has_http_realm = entry.http_realm.is_some();
515            let has_form_action_origin = entry.form_action_origin.is_some();
516            report_error!(
517                "logins-duplicate-in-update",
518                "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
519        }
520
521        // Note: This fail with NoSuchRecord if the record doesn't exist.
522        self.ensure_local_overlay_exists(&guid)?;
523        self.mark_mirror_overridden(&guid)?;
524
525        // We must read the existing record so we can correctly manage timePasswordChanged.
526        let existing = match self.get_by_id(sguid)? {
527            Some(e) => e.decrypt(encdec)?,
528            None => return Err(Error::NoSuchRecord(sguid.to_owned())),
529        };
530        let time_password_changed = if existing.password == entry.password {
531            existing.time_password_changed
532        } else {
533            now_ms
534        };
535
536        // Make the final object here - every column will be updated.
537        let sec_fields = SecureLoginFields {
538            username: entry.username,
539            password: entry.password,
540        }
541        .encrypt(encdec, &existing.id)?;
542        let result = EncryptedLogin {
543            meta: LoginMeta {
544                id: existing.id,
545                time_created: existing.time_created,
546                time_password_changed,
547                time_last_used: now_ms,
548                times_used: existing.times_used + 1,
549            },
550            fields: LoginFields {
551                origin: entry.origin,
552                form_action_origin: entry.form_action_origin,
553                http_realm: entry.http_realm,
554                username_field: entry.username_field,
555                password_field: entry.password_field,
556            },
557            sec_fields,
558        };
559
560        self.update_existing_login(&result)?;
561        tx.commit()?;
562        Ok(result)
563    }
564
565    pub fn add_or_update(
566        &self,
567        entry: LoginEntry,
568        encdec: &dyn EncryptorDecryptor,
569    ) -> Result<EncryptedLogin> {
570        // Make sure to fixup the entry first, in case that changes the username
571        let entry = entry.fixup()?;
572        match self.find_login_to_update(entry.clone(), encdec)? {
573            Some(login) => self.update(&login.id, entry, encdec),
574            None => self.add(entry, encdec),
575        }
576    }
577
578    pub fn fixup_and_check_for_dupes(
579        &self,
580        guid: &Guid,
581        entry: LoginEntry,
582        encdec: &dyn EncryptorDecryptor,
583    ) -> Result<LoginEntry> {
584        let entry = entry.fixup()?;
585        self.check_for_dupes(guid, &entry, encdec)?;
586        Ok(entry)
587    }
588
589    pub fn check_for_dupes(
590        &self,
591        guid: &Guid,
592        entry: &LoginEntry,
593        encdec: &dyn EncryptorDecryptor,
594    ) -> Result<()> {
595        if self.dupe_exists(guid, entry, encdec)? {
596            return Err(InvalidLogin::DuplicateLogin.into());
597        }
598        Ok(())
599    }
600
601    pub fn dupe_exists(
602        &self,
603        guid: &Guid,
604        entry: &LoginEntry,
605        encdec: &dyn EncryptorDecryptor,
606    ) -> Result<bool> {
607        Ok(self.find_dupe(guid, entry, encdec)?.is_some())
608    }
609
610    pub fn find_dupe(
611        &self,
612        guid: &Guid,
613        entry: &LoginEntry,
614        encdec: &dyn EncryptorDecryptor,
615    ) -> Result<Option<Guid>> {
616        for possible in self.get_by_entry_target(entry)? {
617            if possible.guid() != *guid {
618                let pos_sec_fields = possible.decrypt_fields(encdec)?;
619                if pos_sec_fields.username == entry.username {
620                    return Ok(Some(possible.guid()));
621                }
622            }
623        }
624        Ok(None)
625    }
626
627    // Find saved logins that match the target for a `LoginEntry`
628    //
629    // This means that:
630    //   - `origin` matches
631    //   - Either `form_action_origin` or `http_realm` matches, depending on which one is non-null
632    //
633    // This is used for dupe-checking and `find_login_to_update()`
634    fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
635        // Could be lazy_static-ed...
636        lazy_static::lazy_static! {
637            static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
638                "SELECT {common_cols} FROM loginsL
639                WHERE is_deleted = 0
640                    AND origin = :origin
641                    AND formActionOrigin = :form_action_origin
642
643                UNION ALL
644
645                SELECT {common_cols} FROM loginsM
646                WHERE is_overridden = 0
647                    AND origin = :origin
648                    AND formActionOrigin = :form_action_origin
649                ",
650                common_cols = schema::COMMON_COLS
651            );
652            static ref GET_BY_HTTP_REALM: String = format!(
653                "SELECT {common_cols} FROM loginsL
654                WHERE is_deleted = 0
655                    AND origin = :origin
656                    AND httpRealm = :http_realm
657
658                UNION ALL
659
660                SELECT {common_cols} FROM loginsM
661                WHERE is_overridden = 0
662                    AND origin = :origin
663                    AND httpRealm = :http_realm
664                ",
665                common_cols = schema::COMMON_COLS
666            );
667        }
668        match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
669            (Some(form_action_origin), None) => {
670                let params = named_params! {
671                    ":origin": &entry.origin,
672                    ":form_action_origin": form_action_origin,
673                };
674                self.db
675                    .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
676                    .query_and_then(params, EncryptedLogin::from_row)?
677                    .collect()
678            }
679            (None, Some(http_realm)) => {
680                let params = named_params! {
681                    ":origin": &entry.origin,
682                    ":http_realm": http_realm,
683                };
684                self.db
685                    .prepare_cached(&GET_BY_HTTP_REALM)?
686                    .query_and_then(params, EncryptedLogin::from_row)?
687                    .collect()
688            }
689            (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
690            (None, None) => Err(InvalidLogin::NoTarget.into()),
691        }
692    }
693
694    pub fn exists(&self, id: &str) -> Result<bool> {
695        Ok(self.db.query_row(
696            "SELECT EXISTS(
697                 SELECT 1 FROM loginsL
698                 WHERE guid = :guid AND is_deleted = 0
699                 UNION ALL
700                 SELECT 1 FROM loginsM
701                 WHERE guid = :guid AND is_overridden IS NOT 1
702             )",
703            named_params! { ":guid": id },
704            |row| row.get(0),
705        )?)
706    }
707
708    /// Delete the record with the provided id. Returns true if the record
709    /// existed already.
710    pub fn delete(&self, id: &str) -> Result<bool> {
711        let mut results = self.delete_many(vec![id])?;
712        Ok(results.pop().expect("there should be a single result"))
713    }
714
715    /// Delete the records with the specified IDs. Returns a list of Boolean values
716    /// indicating whether the respective records already existed.
717    pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
718        let tx = self.unchecked_transaction_imm()?;
719        let sql = format!(
720            "
721            UPDATE loginsL
722            SET local_modified = :now_ms,
723                sync_status = {status_changed},
724                is_deleted = 1,
725                secFields = '',
726                origin = '',
727                httpRealm = NULL,
728                formActionOrigin = NULL
729            WHERE guid = :guid AND is_deleted IS FALSE
730            ",
731            status_changed = SyncStatus::Changed as u8
732        );
733        let mut stmt = self.db.prepare_cached(&sql)?;
734
735        let mut result = vec![];
736
737        for id in ids {
738            let now_ms = util::system_time_ms_i64(SystemTime::now());
739
740            // For IDs that have, mark is_deleted and clear sensitive fields
741            let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
742
743            let exists = update_result == 1;
744
745            // Mark the mirror as overridden
746            self.execute(
747                "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
748                named_params! { ":guid": id },
749            )?;
750
751            // If we don't have a local record for this ID, but do have it in the mirror
752            // insert a tombstone.
753            self.execute(&format!("
754                INSERT OR IGNORE INTO loginsL
755                        (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
756                SELECT   guid, :now_ms,        1,          {changed},   '',     timeCreated, :now_ms,             ''
757                FROM loginsM
758                WHERE guid = :guid",
759                changed = SyncStatus::Changed as u8),
760                named_params! { ":now_ms": now_ms, ":guid": id })?;
761
762            result.push(exists);
763        }
764
765        tx.commit()?;
766
767        Ok(result)
768    }
769
770    pub fn delete_undecryptable_records_for_remote_replacement(
771        &self,
772        encdec: &dyn EncryptorDecryptor,
773    ) -> Result<LoginsDeletionMetrics> {
774        // Retrieve a list of guids for logins that cannot be decrypted
775        let corrupted_logins = self
776            .get_all()?
777            .into_iter()
778            .filter(|login| login.clone().decrypt(encdec).is_err())
779            .collect::<Vec<_>>();
780        let ids = corrupted_logins
781            .iter()
782            .map(|login| login.guid_str())
783            .collect::<Vec<_>>();
784
785        self.delete_local_records_for_remote_replacement(ids)
786    }
787
788    pub fn delete_local_records_for_remote_replacement(
789        &self,
790        ids: Vec<&str>,
791    ) -> Result<LoginsDeletionMetrics> {
792        let tx = self.unchecked_transaction_imm()?;
793        let mut local_deleted = 0;
794        let mut mirror_deleted = 0;
795
796        sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
797            let deleted = self.execute(
798                &format!(
799                    "DELETE FROM loginsL WHERE guid IN ({})",
800                    sql_support::repeat_sql_values(chunk.len())
801                ),
802                rusqlite::params_from_iter(chunk),
803            )?;
804            local_deleted += deleted;
805            Ok(())
806        })?;
807
808        sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
809            let deleted = self.execute(
810                &format!(
811                    "DELETE FROM loginsM WHERE guid IN ({})",
812                    sql_support::repeat_sql_values(chunk.len())
813                ),
814                rusqlite::params_from_iter(chunk),
815            )?;
816            mirror_deleted += deleted;
817            Ok(())
818        })?;
819
820        tx.commit()?;
821        Ok(LoginsDeletionMetrics {
822            local_deleted: local_deleted as u64,
823            mirror_deleted: mirror_deleted as u64,
824        })
825    }
826
827    fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
828        self.execute_cached(
829            "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
830            named_params! { ":guid": guid },
831        )?;
832        Ok(())
833    }
834
835    fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
836        let already_have_local: bool = self.db.query_row(
837            "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
838            named_params! { ":guid": guid },
839            |row| row.get(0),
840        )?;
841
842        if already_have_local {
843            return Ok(());
844        }
845
846        debug!("No overlay; cloning one for {:?}.", guid);
847        let changed = self.clone_mirror_to_overlay(guid)?;
848        if changed == 0 {
849            report_error!(
850                "logins-local-overlay-error",
851                "Failed to create local overlay for GUID {guid:?}."
852            );
853            return Err(Error::NoSuchRecord(guid.to_owned()));
854        }
855        Ok(())
856    }
857
858    fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
859        Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
860    }
861
862    /// Wipe all local data, returns the number of rows deleted
863    pub fn wipe_local(&self) -> Result<usize> {
864        info!("Executing wipe_local on password engine!");
865        let tx = self.unchecked_transaction()?;
866        let mut row_count = 0;
867        row_count += self.execute("DELETE FROM loginsL", [])?;
868        row_count += self.execute("DELETE FROM loginsM", [])?;
869        row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
870        tx.commit()?;
871        Ok(row_count)
872    }
873
874    pub fn shutdown(self) -> Result<()> {
875        self.db.close().map_err(|(_, e)| Error::SqlError(e))
876    }
877}
878
879lazy_static! {
880    static ref GET_ALL_SQL: String = format!(
881        "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
882         UNION ALL
883         SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
884        common_cols = schema::COMMON_COLS,
885    );
886    static ref COUNT_ALL_SQL: String = format!(
887        "SELECT COUNT(*) FROM (
888          SELECT guid FROM loginsL WHERE is_deleted = 0
889          UNION ALL
890          SELECT guid FROM loginsM WHERE is_overridden = 0
891        )"
892    );
893    static ref COUNT_BY_ORIGIN_SQL: String = format!(
894        "SELECT COUNT(*) FROM (
895          SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
896          UNION ALL
897          SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
898        )"
899    );
900    static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
901        "SELECT COUNT(*) FROM (
902          SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
903          UNION ALL
904          SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
905        )"
906    );
907    static ref GET_BY_GUID_SQL: String = format!(
908        "SELECT {common_cols}
909         FROM loginsL
910         WHERE is_deleted = 0
911           AND guid = :guid
912
913         UNION ALL
914
915         SELECT {common_cols}
916         FROM loginsM
917         WHERE is_overridden IS NOT 1
918           AND guid = :guid
919         ORDER BY origin ASC
920
921         LIMIT 1",
922        common_cols = schema::COMMON_COLS,
923    );
924    pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
925        "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
926         SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
927         FROM loginsM",
928        common_cols = schema::COMMON_COLS,
929    );
930    static ref CLONE_SINGLE_MIRROR_SQL: String =
931        format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
932}
933
934#[cfg(not(feature = "keydb"))]
935#[cfg(test)]
936pub mod test_utils {
937    use super::*;
938    use crate::encryption::test_utils::decrypt_struct;
939    use crate::login::test_utils::enc_login;
940    use crate::SecureLoginFields;
941    use sync15::ServerTimestamp;
942
943    // Insert a login into the local and/or mirror tables.
944    //
945    // local_login and mirror_login are specified as Some(password_string)
946    pub fn insert_login(
947        db: &LoginDb,
948        guid: &str,
949        local_login: Option<&str>,
950        mirror_login: Option<&str>,
951    ) {
952        if let Some(password) = mirror_login {
953            add_mirror(
954                db,
955                &enc_login(guid, password),
956                &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
957                local_login.is_some(),
958            )
959            .unwrap();
960        }
961        if let Some(password) = local_login {
962            db.insert_new_login(&enc_login(guid, password)).unwrap();
963        }
964    }
965
966    pub fn insert_encrypted_login(
967        db: &LoginDb,
968        local: &EncryptedLogin,
969        mirror: &EncryptedLogin,
970        server_modified: &ServerTimestamp,
971    ) {
972        db.insert_new_login(local).unwrap();
973        add_mirror(db, mirror, server_modified, true).unwrap();
974    }
975
976    pub fn add_mirror(
977        db: &LoginDb,
978        login: &EncryptedLogin,
979        server_modified: &ServerTimestamp,
980        is_overridden: bool,
981    ) -> Result<()> {
982        let sql = "
983            INSERT OR IGNORE INTO loginsM (
984                is_overridden,
985                server_modified,
986
987                httpRealm,
988                formActionOrigin,
989                usernameField,
990                passwordField,
991                secFields,
992                origin,
993
994                timesUsed,
995                timeLastUsed,
996                timePasswordChanged,
997                timeCreated,
998
999                guid
1000            ) VALUES (
1001                :is_overridden,
1002                :server_modified,
1003
1004                :http_realm,
1005                :form_action_origin,
1006                :username_field,
1007                :password_field,
1008                :sec_fields,
1009                :origin,
1010
1011                :times_used,
1012                :time_last_used,
1013                :time_password_changed,
1014                :time_created,
1015
1016                :guid
1017            )";
1018        let mut stmt = db.prepare_cached(sql)?;
1019
1020        stmt.execute(named_params! {
1021            ":is_overridden": is_overridden,
1022            ":server_modified": server_modified.as_millis(),
1023            ":http_realm": login.fields.http_realm,
1024            ":form_action_origin": login.fields.form_action_origin,
1025            ":username_field": login.fields.username_field,
1026            ":password_field": login.fields.password_field,
1027            ":origin": login.fields.origin,
1028            ":sec_fields": login.sec_fields,
1029            ":times_used": login.meta.times_used,
1030            ":time_last_used": login.meta.time_last_used,
1031            ":time_password_changed": login.meta.time_password_changed,
1032            ":time_created": login.meta.time_created,
1033            ":guid": login.guid_str(),
1034        })?;
1035        Ok(())
1036    }
1037
1038    pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1039        get_guids(db, "SELECT guid FROM loginsL")
1040    }
1041
1042    pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1043        get_guids(db, "SELECT guid FROM loginsM")
1044    }
1045
1046    fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1047        let mut stmt = db.prepare_cached(sql).unwrap();
1048        let mut res: Vec<String> = stmt
1049            .query_map([], |r| r.get(0))
1050            .unwrap()
1051            .map(|r| r.unwrap())
1052            .collect();
1053        res.sort();
1054        res
1055    }
1056
1057    pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1058        db.query_one(&format!(
1059            "SELECT server_modified FROM loginsM WHERE guid='{}'",
1060            guid
1061        ))
1062        .unwrap()
1063    }
1064
1065    pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1066        let row: (String, i64, bool) = db
1067            .query_row(
1068                "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1069                [guid],
1070                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1071            )
1072            .unwrap();
1073        let enc: SecureLoginFields = decrypt_struct(row.0);
1074        assert_eq!(enc.password, password);
1075        assert!(row.1 >= local_modified_gte);
1076        assert!(!row.2);
1077    }
1078
1079    pub fn check_mirror_login(
1080        db: &LoginDb,
1081        guid: &str,
1082        password: &str,
1083        server_modified: i64,
1084        is_overridden: bool,
1085    ) {
1086        let row: (String, i64, bool) = db
1087            .query_row(
1088                "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1089                [guid],
1090                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1091            )
1092            .unwrap();
1093        let enc: SecureLoginFields = decrypt_struct(row.0);
1094        assert_eq!(enc.password, password);
1095        assert_eq!(row.1, server_modified);
1096        assert_eq!(row.2, is_overridden);
1097    }
1098}
1099
1100#[cfg(not(feature = "keydb"))]
1101#[cfg(test)]
1102mod tests {
1103    use super::*;
1104    use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1105    use crate::encryption::test_utils::TEST_ENCDEC;
1106    use crate::sync::merge::LocalLogin;
1107    use nss::ensure_initialized;
1108    use std::{thread, time};
1109
1110    #[test]
1111    fn test_username_dupe_semantics() {
1112        ensure_initialized();
1113        let mut login = LoginEntry {
1114            origin: "https://www.example.com".into(),
1115            http_realm: Some("https://www.example.com".into()),
1116            username: "test".into(),
1117            password: "sekret".into(),
1118            ..LoginEntry::default()
1119        };
1120
1121        let db = LoginDb::open_in_memory();
1122        db.add(login.clone(), &*TEST_ENCDEC)
1123            .expect("should be able to add first login");
1124
1125        // We will reject new logins with the same username value...
1126        let exp_err = "Invalid login: Login already exists";
1127        assert_eq!(
1128            db.add(login.clone(), &*TEST_ENCDEC)
1129                .unwrap_err()
1130                .to_string(),
1131            exp_err
1132        );
1133
1134        // Add one with an empty username - not a dupe.
1135        login.username = "".to_string();
1136        db.add(login.clone(), &*TEST_ENCDEC)
1137            .expect("empty login isn't a dupe");
1138
1139        assert_eq!(
1140            db.add(login, &*TEST_ENCDEC).unwrap_err().to_string(),
1141            exp_err
1142        );
1143
1144        // one with a username, 1 without.
1145        assert_eq!(db.get_all().unwrap().len(), 2);
1146    }
1147
1148    #[test]
1149    fn test_add_many() {
1150        ensure_initialized();
1151
1152        let login_a = LoginEntry {
1153            origin: "https://a.example.com".into(),
1154            http_realm: Some("https://www.example.com".into()),
1155            username: "test".into(),
1156            password: "sekret".into(),
1157            ..LoginEntry::default()
1158        };
1159
1160        let login_b = LoginEntry {
1161            origin: "https://b.example.com".into(),
1162            http_realm: Some("https://www.example.com".into()),
1163            username: "test".into(),
1164            password: "sekret".into(),
1165            ..LoginEntry::default()
1166        };
1167
1168        let db = LoginDb::open_in_memory();
1169        let added = db
1170            .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1171            .expect("should be able to add logins");
1172
1173        let [added_a, added_b] = added.as_slice() else {
1174            panic!("there should really be 2")
1175        };
1176
1177        let fetched_a = db
1178            .get_by_id(&added_a.as_ref().unwrap().meta.id)
1179            .expect("should work")
1180            .expect("should get a record");
1181
1182        assert_eq!(fetched_a.fields.origin, login_a.origin);
1183
1184        let fetched_b = db
1185            .get_by_id(&added_b.as_ref().unwrap().meta.id)
1186            .expect("should work")
1187            .expect("should get a record");
1188
1189        assert_eq!(fetched_b.fields.origin, login_b.origin);
1190
1191        assert_eq!(db.count_all().unwrap(), 2);
1192    }
1193
1194    #[test]
1195    fn test_count_by_origin() {
1196        ensure_initialized();
1197
1198        let origin_a = "https://a.example.com";
1199        let login_a = LoginEntry {
1200            origin: origin_a.into(),
1201            http_realm: Some("https://www.example.com".into()),
1202            username: "test".into(),
1203            password: "sekret".into(),
1204            ..LoginEntry::default()
1205        };
1206
1207        let login_b = LoginEntry {
1208            origin: "https://b.example.com".into(),
1209            http_realm: Some("https://www.example.com".into()),
1210            username: "test".into(),
1211            password: "sekret".into(),
1212            ..LoginEntry::default()
1213        };
1214
1215        let db = LoginDb::open_in_memory();
1216        db.add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1217            .expect("should be able to add logins");
1218
1219        assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1220    }
1221
1222    #[test]
1223    fn test_count_by_form_action_origin() {
1224        ensure_initialized();
1225
1226        let origin_a = "https://a.example.com";
1227        let login_a = LoginEntry {
1228            origin: origin_a.into(),
1229            form_action_origin: Some(origin_a.into()),
1230            http_realm: Some("https://www.example.com".into()),
1231            username: "test".into(),
1232            password: "sekret".into(),
1233            ..LoginEntry::default()
1234        };
1235
1236        let login_b = LoginEntry {
1237            origin: "https://b.example.com".into(),
1238            form_action_origin: Some("https://b.example.com".into()),
1239            http_realm: Some("https://www.example.com".into()),
1240            username: "test".into(),
1241            password: "sekret".into(),
1242            ..LoginEntry::default()
1243        };
1244
1245        let db = LoginDb::open_in_memory();
1246        db.add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1247            .expect("should be able to add logins");
1248
1249        assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1250    }
1251
1252    #[test]
1253    fn test_add_many_with_failed_constraint() {
1254        ensure_initialized();
1255
1256        let login_a = LoginEntry {
1257            origin: "https://example.com".into(),
1258            http_realm: Some("https://www.example.com".into()),
1259            username: "test".into(),
1260            password: "sekret".into(),
1261            ..LoginEntry::default()
1262        };
1263
1264        let login_b = LoginEntry {
1265            // same origin will result in duplicate error
1266            origin: "https://example.com".into(),
1267            http_realm: Some("https://www.example.com".into()),
1268            username: "test".into(),
1269            password: "sekret".into(),
1270            ..LoginEntry::default()
1271        };
1272
1273        let db = LoginDb::open_in_memory();
1274        let added = db
1275            .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1276            .expect("should be able to add logins");
1277
1278        let [added_a, added_b] = added.as_slice() else {
1279            panic!("there should really be 2")
1280        };
1281
1282        // first entry has been saved successfully
1283        let fetched_a = db
1284            .get_by_id(&added_a.as_ref().unwrap().meta.id)
1285            .expect("should work")
1286            .expect("should get a record");
1287
1288        assert_eq!(fetched_a.fields.origin, login_a.origin);
1289
1290        // second entry failed
1291        assert!(!added_b.is_ok());
1292    }
1293
1294    #[test]
1295    fn test_add_with_meta() {
1296        ensure_initialized();
1297
1298        let guid = Guid::random();
1299        let now_ms = util::system_time_ms_i64(SystemTime::now());
1300        let login = LoginEntry {
1301            origin: "https://www.example.com".into(),
1302            http_realm: Some("https://www.example.com".into()),
1303            username: "test".into(),
1304            password: "sekret".into(),
1305            ..LoginEntry::default()
1306        };
1307        let meta = LoginMeta {
1308            id: guid.to_string(),
1309            time_created: now_ms,
1310            time_password_changed: now_ms + 100,
1311            time_last_used: now_ms + 10,
1312            times_used: 42,
1313        };
1314
1315        let db = LoginDb::open_in_memory();
1316        let entry_with_meta = LoginEntryWithMeta {
1317            entry: login.clone(),
1318            meta: meta.clone(),
1319        };
1320        let added = db
1321            .add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1322            .expect("should be able to add login with record");
1323
1324        let fetched = db
1325            .get_by_id(&added.meta.id)
1326            .expect("should work")
1327            .expect("should get a record");
1328
1329        assert_eq!(fetched.meta, meta);
1330    }
1331
1332    #[test]
1333    fn test_unicode_submit() {
1334        ensure_initialized();
1335        let db = LoginDb::open_in_memory();
1336        let added = db
1337            .add(
1338                LoginEntry {
1339                    form_action_origin: Some("http://😍.com".into()),
1340                    origin: "http://😍.com".into(),
1341                    http_realm: None,
1342                    username_field: "😍".into(),
1343                    password_field: "😍".into(),
1344                    username: "😍".into(),
1345                    password: "😍".into(),
1346                },
1347                &*TEST_ENCDEC,
1348            )
1349            .unwrap();
1350        let fetched = db
1351            .get_by_id(&added.meta.id)
1352            .expect("should work")
1353            .expect("should get a record");
1354        assert_eq!(added, fetched);
1355        assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1356        assert_eq!(
1357            fetched.fields.form_action_origin,
1358            Some("http://xn--r28h.com".to_string())
1359        );
1360        assert_eq!(fetched.fields.username_field, "😍");
1361        assert_eq!(fetched.fields.password_field, "😍");
1362        let sec_fields = fetched.decrypt_fields(&*TEST_ENCDEC).unwrap();
1363        assert_eq!(sec_fields.username, "😍");
1364        assert_eq!(sec_fields.password, "😍");
1365    }
1366
1367    #[test]
1368    fn test_unicode_realm() {
1369        ensure_initialized();
1370        let db = LoginDb::open_in_memory();
1371        let added = db
1372            .add(
1373                LoginEntry {
1374                    form_action_origin: None,
1375                    origin: "http://😍.com".into(),
1376                    http_realm: Some("😍😍".into()),
1377                    username: "😍".into(),
1378                    password: "😍".into(),
1379                    ..Default::default()
1380                },
1381                &*TEST_ENCDEC,
1382            )
1383            .unwrap();
1384        let fetched = db
1385            .get_by_id(&added.meta.id)
1386            .expect("should work")
1387            .expect("should get a record");
1388        assert_eq!(added, fetched);
1389        assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1390        assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1391    }
1392
1393    fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1394        let mut results = db
1395            .get_by_base_domain(query)
1396            .unwrap()
1397            .into_iter()
1398            .map(|l| l.fields.origin)
1399            .collect::<Vec<String>>();
1400        results.sort_unstable();
1401        let mut sorted = expected.to_owned();
1402        sorted.sort_unstable();
1403        assert_eq!(sorted, results);
1404    }
1405
1406    fn check_good_bad(
1407        good: Vec<&str>,
1408        bad: Vec<&str>,
1409        good_queries: Vec<&str>,
1410        zero_queries: Vec<&str>,
1411    ) {
1412        let db = LoginDb::open_in_memory();
1413        for h in good.iter().chain(bad.iter()) {
1414            db.add(
1415                LoginEntry {
1416                    origin: (*h).into(),
1417                    http_realm: Some((*h).into()),
1418                    password: "test".into(),
1419                    ..Default::default()
1420                },
1421                &*TEST_ENCDEC,
1422            )
1423            .unwrap();
1424        }
1425        for query in good_queries {
1426            check_matches(&db, query, &good);
1427        }
1428        for query in zero_queries {
1429            check_matches(&db, query, &[]);
1430        }
1431    }
1432
1433    #[test]
1434    fn test_get_by_base_domain_invalid() {
1435        check_good_bad(
1436            vec!["https://example.com"],
1437            vec![],
1438            vec![],
1439            vec!["invalid query"],
1440        );
1441    }
1442
1443    #[test]
1444    fn test_get_by_base_domain() {
1445        check_good_bad(
1446            vec![
1447                "https://example.com",
1448                "https://www.example.com",
1449                "http://www.example.com",
1450                "http://www.example.com:8080",
1451                "http://sub.example.com:8080",
1452                "https://sub.example.com:8080",
1453                "https://sub.sub.example.com",
1454                "ftp://sub.example.com",
1455            ],
1456            vec![
1457                "https://badexample.com",
1458                "https://example.co",
1459                "https://example.com.au",
1460            ],
1461            vec!["example.com"],
1462            vec!["foo.com"],
1463        );
1464        // punycode! This is likely to need adjusting once we normalize
1465        // on insert.
1466        check_good_bad(
1467            vec![
1468                "http://xn--r28h.com", // punycoded version of "http://😍.com"
1469            ],
1470            vec!["http://💖.com"],
1471            vec!["😍.com", "xn--r28h.com"],
1472            vec![],
1473        );
1474    }
1475
1476    #[test]
1477    fn test_get_by_base_domain_ipv4() {
1478        check_good_bad(
1479            vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1480            vec!["https://127.0.0.0", "https://example.com"],
1481            vec!["127.0.0.1"],
1482            vec!["127.0.0.2"],
1483        );
1484    }
1485
1486    #[test]
1487    fn test_get_by_base_domain_ipv6() {
1488        check_good_bad(
1489            vec!["http://[::1]", "https://[::1]:8000"],
1490            vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1491            vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1492            vec!["[0:0:0:0:0:0:1:2]"],
1493        );
1494    }
1495
1496    #[test]
1497    fn test_add() {
1498        ensure_initialized();
1499        let db = LoginDb::open_in_memory();
1500        let to_add = LoginEntry {
1501            origin: "https://www.example.com".into(),
1502            http_realm: Some("https://www.example.com".into()),
1503            username: "test_user".into(),
1504            password: "test_password".into(),
1505            ..Default::default()
1506        };
1507        let login = db.add(to_add, &*TEST_ENCDEC).unwrap();
1508        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1509
1510        assert_eq!(login.fields.origin, login2.fields.origin);
1511        assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1512        assert_eq!(login.sec_fields, login2.sec_fields);
1513    }
1514
1515    #[test]
1516    fn test_update() {
1517        ensure_initialized();
1518        let db = LoginDb::open_in_memory();
1519        let login = db
1520            .add(
1521                LoginEntry {
1522                    origin: "https://www.example.com".into(),
1523                    http_realm: Some("https://www.example.com".into()),
1524                    username: "user1".into(),
1525                    password: "password1".into(),
1526                    ..Default::default()
1527                },
1528                &*TEST_ENCDEC,
1529            )
1530            .unwrap();
1531        db.update(
1532            &login.meta.id,
1533            LoginEntry {
1534                origin: "https://www.example2.com".into(),
1535                http_realm: Some("https://www.example2.com".into()),
1536                username: "user2".into(),
1537                password: "password2".into(),
1538                ..Default::default() // TODO: check and fix if needed
1539            },
1540            &*TEST_ENCDEC,
1541        )
1542        .unwrap();
1543
1544        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1545
1546        assert_eq!(login2.fields.origin, "https://www.example2.com");
1547        assert_eq!(
1548            login2.fields.http_realm,
1549            Some("https://www.example2.com".into())
1550        );
1551        let sec_fields = login2.decrypt_fields(&*TEST_ENCDEC).unwrap();
1552        assert_eq!(sec_fields.username, "user2");
1553        assert_eq!(sec_fields.password, "password2");
1554    }
1555
1556    #[test]
1557    fn test_touch() {
1558        ensure_initialized();
1559        let db = LoginDb::open_in_memory();
1560        let login = db
1561            .add(
1562                LoginEntry {
1563                    origin: "https://www.example.com".into(),
1564                    http_realm: Some("https://www.example.com".into()),
1565                    username: "user1".into(),
1566                    password: "password1".into(),
1567                    ..Default::default()
1568                },
1569                &*TEST_ENCDEC,
1570            )
1571            .unwrap();
1572        // Simulate touch happening at another "time"
1573        thread::sleep(time::Duration::from_millis(50));
1574        db.touch(&login.meta.id).unwrap();
1575        let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1576        assert!(login2.meta.time_last_used > login.meta.time_last_used);
1577        assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1578    }
1579
1580    #[test]
1581    fn test_delete() {
1582        ensure_initialized();
1583        let db = LoginDb::open_in_memory();
1584        let login = db
1585            .add(
1586                LoginEntry {
1587                    origin: "https://www.example.com".into(),
1588                    http_realm: Some("https://www.example.com".into()),
1589                    username: "test_user".into(),
1590                    password: "test_password".into(),
1591                    ..Default::default()
1592                },
1593                &*TEST_ENCDEC,
1594            )
1595            .unwrap();
1596
1597        assert!(db.delete(login.guid_str()).unwrap());
1598
1599        let local_login = db
1600            .query_row(
1601                "SELECT * FROM loginsL WHERE guid = :guid",
1602                named_params! { ":guid": login.guid_str() },
1603                |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
1604            )
1605            .unwrap();
1606        assert_eq!(local_login.fields.http_realm, None);
1607        assert_eq!(local_login.fields.form_action_origin, None);
1608
1609        assert!(!db.exists(login.guid_str()).unwrap());
1610    }
1611
1612    #[test]
1613    fn test_delete_many() {
1614        ensure_initialized();
1615        let db = LoginDb::open_in_memory();
1616
1617        let login_a = db
1618            .add(
1619                LoginEntry {
1620                    origin: "https://a.example.com".into(),
1621                    http_realm: Some("https://www.example.com".into()),
1622                    username: "test_user".into(),
1623                    password: "test_password".into(),
1624                    ..Default::default()
1625                },
1626                &*TEST_ENCDEC,
1627            )
1628            .unwrap();
1629
1630        let login_b = db
1631            .add(
1632                LoginEntry {
1633                    origin: "https://b.example.com".into(),
1634                    http_realm: Some("https://www.example.com".into()),
1635                    username: "test_user".into(),
1636                    password: "test_password".into(),
1637                    ..Default::default()
1638                },
1639                &*TEST_ENCDEC,
1640            )
1641            .unwrap();
1642
1643        let result = db
1644            .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
1645            .unwrap();
1646        assert!(result[0]);
1647        assert!(result[1]);
1648        assert!(!db.exists(login_a.guid_str()).unwrap());
1649        assert!(!db.exists(login_b.guid_str()).unwrap());
1650    }
1651
1652    #[test]
1653    fn test_subsequent_delete_many() {
1654        ensure_initialized();
1655        let db = LoginDb::open_in_memory();
1656
1657        let login = db
1658            .add(
1659                LoginEntry {
1660                    origin: "https://a.example.com".into(),
1661                    http_realm: Some("https://www.example.com".into()),
1662                    username: "test_user".into(),
1663                    password: "test_password".into(),
1664                    ..Default::default()
1665                },
1666                &*TEST_ENCDEC,
1667            )
1668            .unwrap();
1669
1670        let result = db.delete_many(vec![login.guid_str()]).unwrap();
1671        assert!(result[0]);
1672        assert!(!db.exists(login.guid_str()).unwrap());
1673
1674        let result = db.delete_many(vec![login.guid_str()]).unwrap();
1675        assert!(!result[0]);
1676    }
1677
1678    #[test]
1679    fn test_delete_many_with_non_existent_id() {
1680        ensure_initialized();
1681        let db = LoginDb::open_in_memory();
1682
1683        let result = db.delete_many(vec![&Guid::random()]).unwrap();
1684        assert!(!result[0]);
1685    }
1686
1687    #[test]
1688    fn test_delete_local_for_remote_replacement() {
1689        ensure_initialized();
1690        let db = LoginDb::open_in_memory();
1691        let login = db
1692            .add(
1693                LoginEntry {
1694                    origin: "https://www.example.com".into(),
1695                    http_realm: Some("https://www.example.com".into()),
1696                    username: "test_user".into(),
1697                    password: "test_password".into(),
1698                    ..Default::default()
1699                },
1700                &*TEST_ENCDEC,
1701            )
1702            .unwrap();
1703
1704        let result = db
1705            .delete_local_records_for_remote_replacement(vec![login.guid_str()])
1706            .unwrap();
1707
1708        let local_guids = get_local_guids(&db);
1709        assert_eq!(local_guids.len(), 0);
1710
1711        let mirror_guids = get_mirror_guids(&db);
1712        assert_eq!(mirror_guids.len(), 0);
1713
1714        assert_eq!(result.local_deleted, 1);
1715    }
1716
1717    mod test_find_login_to_update {
1718        use super::*;
1719
1720        fn make_entry(username: &str, password: &str) -> LoginEntry {
1721            LoginEntry {
1722                origin: "https://www.example.com".into(),
1723                http_realm: Some("the website".into()),
1724                username: username.into(),
1725                password: password.into(),
1726                ..Default::default()
1727            }
1728        }
1729
1730        fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
1731            db.add(make_entry(username, password), &*TEST_ENCDEC)
1732                .unwrap()
1733                .decrypt(&*TEST_ENCDEC)
1734                .unwrap()
1735        }
1736
1737        #[test]
1738        fn test_match() {
1739            ensure_initialized();
1740            let db = LoginDb::open_in_memory();
1741            let login = make_saved_login(&db, "user", "pass");
1742            assert_eq!(
1743                Some(login),
1744                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1745                    .unwrap(),
1746            );
1747        }
1748
1749        #[test]
1750        fn test_non_matches() {
1751            ensure_initialized();
1752            let db = LoginDb::open_in_memory();
1753            // Non-match because the username is different
1754            make_saved_login(&db, "other-user", "pass");
1755            // Non-match because the http_realm is different
1756            db.add(
1757                LoginEntry {
1758                    origin: "https://www.example.com".into(),
1759                    http_realm: Some("the other website".into()),
1760                    username: "user".into(),
1761                    password: "pass".into(),
1762                    ..Default::default()
1763                },
1764                &*TEST_ENCDEC,
1765            )
1766            .unwrap();
1767            // Non-match because it uses form_action_origin instead of http_realm
1768            db.add(
1769                LoginEntry {
1770                    origin: "https://www.example.com".into(),
1771                    form_action_origin: Some("https://www.example.com/".into()),
1772                    username: "user".into(),
1773                    password: "pass".into(),
1774                    ..Default::default()
1775                },
1776                &*TEST_ENCDEC,
1777            )
1778            .unwrap();
1779            assert_eq!(
1780                None,
1781                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1782                    .unwrap(),
1783            );
1784        }
1785
1786        #[test]
1787        fn test_match_blank_password() {
1788            ensure_initialized();
1789            let db = LoginDb::open_in_memory();
1790            let login = make_saved_login(&db, "", "pass");
1791            assert_eq!(
1792                Some(login),
1793                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1794                    .unwrap(),
1795            );
1796        }
1797
1798        #[test]
1799        fn test_username_match_takes_precedence_over_blank_username() {
1800            ensure_initialized();
1801            let db = LoginDb::open_in_memory();
1802            make_saved_login(&db, "", "pass");
1803            let username_match = make_saved_login(&db, "user", "pass");
1804            assert_eq!(
1805                Some(username_match),
1806                db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1807                    .unwrap(),
1808            );
1809        }
1810
1811        #[test]
1812        fn test_invalid_login() {
1813            ensure_initialized();
1814            let db = LoginDb::open_in_memory();
1815            assert!(db
1816                .find_login_to_update(
1817                    LoginEntry {
1818                        http_realm: None,
1819                        form_action_origin: None,
1820                        ..LoginEntry::default()
1821                    },
1822                    &*TEST_ENCDEC
1823                )
1824                .is_err());
1825        }
1826
1827        #[test]
1828        fn test_update_with_duplicate_login() {
1829            ensure_initialized();
1830            // If we have duplicate logins in the database, it should be possible to update them
1831            // without triggering a DuplicateLogin error
1832            let db = LoginDb::open_in_memory();
1833            let login = make_saved_login(&db, "user", "pass");
1834            let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
1835            dupe.meta.id = "different-guid".to_string();
1836            db.insert_new_login(&dupe).unwrap();
1837
1838            let mut entry = login.entry();
1839            entry.password = "pass2".to_string();
1840            db.update(&login.id, entry, &*TEST_ENCDEC).unwrap();
1841
1842            let mut entry = login.entry();
1843            entry.password = "pass3".to_string();
1844            db.add_or_update(entry, &*TEST_ENCDEC).unwrap();
1845        }
1846    }
1847}