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