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