summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/storage-geckoview.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/storage-geckoview.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/storage-geckoview.sys.mjs245
1 files changed, 245 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/storage-geckoview.sys.mjs b/toolkit/components/passwordmgr/storage-geckoview.sys.mjs
new file mode 100644
index 0000000000..db2ef6d61b
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-geckoview.sys.mjs
@@ -0,0 +1,245 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * LoginManagerStorage implementation for GeckoView
+ */
+
+import { LoginManagerStorage_json } from "resource://gre/modules/storage-json.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.sys.mjs",
+ LoginEntry: "resource://gre/modules/GeckoViewAutocomplete.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+});
+
+export class LoginManagerStorage extends LoginManagerStorage_json {
+ static #storage = null;
+
+ static create(callback) {
+ if (!LoginManagerStorage.#storage) {
+ LoginManagerStorage.#storage = new LoginManagerStorage();
+ LoginManagerStorage.#storage.initialize().then(callback);
+ } else if (callback) {
+ callback();
+ }
+
+ return LoginManagerStorage.#storage;
+ }
+
+ get _crypto() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ initialize() {
+ try {
+ return Promise.resolve();
+ } catch (e) {
+ this.log("Initialization failed:", e);
+ throw new Error("Initialization failed");
+ }
+ }
+
+ /**
+ * Internal method used by regression tests only. It is called before
+ * replacing this storage module with a new instance.
+ */
+ terminate() {}
+
+ async addLoginsAsync(logins, continueOnDuplicates = false) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ removeLogin(login) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ modifyLogin(oldLogin, newLoginData) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ recordPasswordUse(login) {
+ lazy.GeckoViewAutocomplete.onLoginPasswordUsed(
+ lazy.LoginEntry.fromLoginInfo(login)
+ );
+ }
+
+ /**
+ * Returns a promise resolving to an array of all saved logins that can be decrypted.
+ *
+ * @resolve {nsILoginInfo[]}
+ */
+ getAllLogins(includeDeleted) {
+ return this._getLoginsAsync({}, includeDeleted);
+ }
+
+ async searchLoginsAsync(matchData, includeDeleted) {
+ this.log(
+ `Searching for matching saved logins for origin: ${matchData.origin}`
+ );
+ return this._getLoginsAsync(matchData, includeDeleted);
+ }
+
+ _baseHostnameFromOrigin(origin) {
+ if (!origin) {
+ return null;
+ }
+
+ let originURI = Services.io.newURI(origin);
+ try {
+ return Services.eTLD.getBaseDomain(originURI);
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+ // `getBaseDomain` cannot handle IP addresses and `nsIURI` cannot return
+ // IPv6 hostnames with the square brackets so use `URL.hostname`.
+ return new URL(origin).hostname;
+ } else if (ex.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ return originURI.asciiHost;
+ }
+ throw ex;
+ }
+ }
+
+ async _getLoginsAsync(matchData, includeDeleted) {
+ let baseHostname = this._baseHostnameFromOrigin(matchData.origin);
+
+ // Query all logins for the eTLD+1 and then filter the logins in _searchLogins
+ // so that we can handle the logic for scheme upgrades, subdomains, etc.
+ // Convert from the new shape to one which supports the legacy getters used
+ // by _searchLogins.
+ let candidateLogins = await lazy.GeckoViewAutocomplete.fetchLogins(
+ baseHostname
+ ).catch(_ => {
+ // No GV delegate is attached.
+ });
+
+ if (!candidateLogins) {
+ // May be undefined if there is no delegate attached to handle the request.
+ // Ignore the request.
+ return [];
+ }
+
+ let realMatchData = {};
+ let options = {};
+
+ if (matchData.guid) {
+ // Enforce GUID-based filtering when available, since the origin of the
+ // login may not match the origin of the form in the case of scheme
+ // upgrades.
+ realMatchData = { guid: matchData.guid };
+ } else {
+ for (let [name, value] of Object.entries(matchData)) {
+ switch (name) {
+ // Some property names aren't field names but are special options to
+ // affect the search.
+ case "acceptDifferentSubdomains":
+ case "schemeUpgrades": {
+ options[name] = value;
+ break;
+ }
+ default: {
+ realMatchData[name] = value;
+ break;
+ }
+ }
+ }
+ }
+
+ const [logins] = this._searchLogins(
+ realMatchData,
+ includeDeleted,
+ options,
+ candidateLogins.map(this._vanillaLoginToStorageLogin)
+ );
+ return logins;
+ }
+
+ /**
+ * Convert a modern decrypted vanilla login object to one expected from logins.json.
+ *
+ * The storage login is usually encrypted but not in this case, this aligns
+ * with the `_decryptLogins` method being a no-op.
+ *
+ * @param {object} vanillaLogin using `origin`/`formActionOrigin`/`username` properties.
+ * @returns {object} a vanilla login for logins.json using
+ * `hostname`/`formSubmitURL`/`encryptedUsername`.
+ */
+ _vanillaLoginToStorageLogin(vanillaLogin) {
+ return {
+ ...vanillaLogin,
+ hostname: vanillaLogin.origin,
+ formSubmitURL: vanillaLogin.formActionOrigin,
+ encryptedUsername: vanillaLogin.username,
+ encryptedPassword: vanillaLogin.password,
+ };
+ }
+
+ /**
+ * Use `searchLoginsAsync` instead.
+ */
+ searchLogins(matchData) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ /**
+ * Removes all logins from storage.
+ */
+ removeAllLogins() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ countLogins(origin, formActionOrigin, httpRealm) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ get uiBusy() {
+ return false;
+ }
+
+ get isLoggedIn() {
+ return true;
+ }
+
+ /**
+ * GeckoView will encrypt the login itself.
+ */
+ _encryptLogin(login) {
+ return login;
+ }
+
+ /**
+ * GeckoView logins are already decrypted before this component receives them
+ * so this method is a no-op for this backend.
+ * @see _vanillaLoginToStorageLogin
+ */
+ _decryptLogins(logins) {
+ return logins;
+ }
+
+ /**
+ * Sync metadata, which isn't supported by GeckoView.
+ */
+ async getSyncID() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ async setSyncID(syncID) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ async getLastSync() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ async setLastSync(timestamp) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+}
+
+ChromeUtils.defineLazyGetter(LoginManagerStorage.prototype, "log", () => {
+ let logger = lazy.LoginHelper.createLogger("Login storage");
+ return logger.log.bind(logger);
+});