mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-10 05:08:36 +02:00
***
Bug 1514594: Part 3a - Change ChromeUtils.import to return an exports object; not pollute global. r=mccr8
This changes the behavior of ChromeUtils.import() to return an exports object,
rather than a module global, in all cases except when `null` is passed as a
second argument, and changes the default behavior not to pollute the global
scope with the module's exports. Thus, the following code written for the old
model:
ChromeUtils.import("resource://gre/modules/Services.jsm");
is approximately the same as the following, in the new model:
var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
Since the two behaviors are mutually incompatible, this patch will land with a
scripted rewrite to update all existing callers to use the new model rather
than the old.
***
Bug 1514594: Part 3b - Mass rewrite all JS code to use the new ChromeUtils.import API. rs=Gijs
This was done using the followng script:
https://bitbucket.org/kmaglione/m-c-rewrites/src/tip/processors/cu-import-exports.jsm
***
Bug 1514594: Part 3c - Update ESLint plugin for ChromeUtils.import API changes. r=Standard8
Differential Revision: https://phabricator.services.mozilla.com/D16747
***
Bug 1514594: Part 3d - Remove/fix hundreds of duplicate imports from sync tests. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D16748
***
Bug 1514594: Part 3e - Remove no-op ChromeUtils.import() calls. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D16749
***
Bug 1514594: Part 3f.1 - Cleanup various test corner cases after mass rewrite. r=Gijs
***
Bug 1514594: Part 3f.2 - Cleanup various non-test corner cases after mass rewrite. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D16750
--HG--
extra : rebase_source : 359574ee3064c90f33bf36c2ebe3159a24cc8895
extra : histedit_source : b93c8f42808b1599f9122d7842d2c0b3e656a594%2C64a3a4e3359dc889e2ab2b49461bab9e27fc10a7
256 lines
9.9 KiB
JavaScript
256 lines
9.9 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* Helpers for using OS Key Store.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"OSKeyStore",
|
|
];
|
|
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
|
|
XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
|
|
"@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
|
|
XPCOMUtils.defineLazyServiceGetter(this, "osReauthenticator",
|
|
"@mozilla.org/security/osreauthenticator;1", Ci.nsIOSReauthenticator);
|
|
|
|
// Skip reauth during tests, only works in non-official builds.
|
|
const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";
|
|
|
|
var OSKeyStore = {
|
|
/**
|
|
* On macOS this becomes part of the name label visible on Keychain Acesss as
|
|
* "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
|
|
*/
|
|
STORE_LABEL: AppConstants.MOZ_APP_NAME,
|
|
|
|
/**
|
|
* Consider the module is initialized as locked. OS might unlock without a
|
|
* prompt.
|
|
* @type {Boolean}
|
|
*/
|
|
_isLocked: true,
|
|
|
|
_pendingUnlockPromise: null,
|
|
|
|
/**
|
|
* @returns {boolean} True if logged in (i.e. decrypt(reauth = false) will
|
|
* not retrigger a dialog) and false if not.
|
|
* User might log out elsewhere in the OS, so even if this
|
|
* is true a prompt might still pop up.
|
|
*/
|
|
get isLoggedIn() {
|
|
return !this._isLocked;
|
|
},
|
|
|
|
/**
|
|
* @returns {boolean} True if there is another login dialog existing and false
|
|
* otherwise.
|
|
*/
|
|
get isUIBusy() {
|
|
return !!this._pendingUnlockPromise;
|
|
},
|
|
|
|
/**
|
|
* If the test pref exists, this method will dispatch a observer message and
|
|
* resolves to simulate successful reauth, or rejects to simulate failed reauth.
|
|
*
|
|
* @returns {Promise<undefined>} Resolves when sucessful login, rejects when
|
|
* login fails.
|
|
*/
|
|
async _reauthInTests() {
|
|
// Skip this reauth because there is no way to mock the
|
|
// native dialog in the testing environment, for now.
|
|
log.debug("_ensureReauth: _testReauth: ", this._testReauth);
|
|
switch (this._testReauth) {
|
|
case "pass":
|
|
Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
|
|
break;
|
|
case "cancel":
|
|
Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
|
|
throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
|
|
default:
|
|
throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Ensure the store in use is logged in. It will display the OS login
|
|
* login prompt or do nothing if it's logged in already. If an existing login
|
|
* prompt is already prompted, the result from it will be used instead.
|
|
*
|
|
* Note: This method must set _pendingUnlockPromise before returning the
|
|
* promise (i.e. the first |await|), otherwise we'll risk re-entry.
|
|
* This is why there aren't an |await| in the method. The method is marked as
|
|
* |async| to communicate that it's async.
|
|
*
|
|
* @param {boolean|string} reauth If it's set to true or a string, prompt
|
|
* the reauth login dialog.
|
|
* The string will be shown on the native OS
|
|
* login dialog.
|
|
* @returns {Promise<boolean>} True if it's logged in or no password is set
|
|
* and false if it's still not logged in (prompt
|
|
* canceled or other error).
|
|
*/
|
|
async ensureLoggedIn(reauth = false) {
|
|
if (this._pendingUnlockPromise) {
|
|
log.debug("ensureLoggedIn: Has a pending unlock operation");
|
|
return this._pendingUnlockPromise;
|
|
}
|
|
log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);
|
|
|
|
let unlockPromise;
|
|
|
|
// Decides who should handle reauth
|
|
if (!this._reauthEnabledByUser || (typeof reauth == "boolean" && !reauth)) {
|
|
unlockPromise = Promise.resolve();
|
|
} else if (!AppConstants.MOZILLA_OFFICIAL && this._testReauth) {
|
|
unlockPromise = this._reauthInTests();
|
|
} else if (AppConstants.platform == "win" ||
|
|
AppConstants.platform == "macosx") {
|
|
let reauthLabel = typeof reauth == "string" ? reauth : "";
|
|
// On Windows, this promise rejects when the user cancels login dialog, see bug 1502121.
|
|
// On macOS this resolves to false, so we would need to check it.
|
|
unlockPromise = osReauthenticator.asyncReauthenticateUser(reauthLabel)
|
|
.then(reauthResult => {
|
|
if (typeof reauthResult == "boolean" && !reauthResult) {
|
|
throw new Components.Exception("User canceled OS reauth entry", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
});
|
|
} else {
|
|
log.debug("ensureLoggedIn: Skipping reauth on unsupported platforms");
|
|
unlockPromise = Promise.resolve();
|
|
}
|
|
|
|
unlockPromise = unlockPromise.then(async () => {
|
|
if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
|
|
log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
|
|
let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
|
|
// TODO We should somehow have a dialog to ask the user to write this down,
|
|
// and another dialog somewhere for the user to restore the secret with it.
|
|
// (Intentionally not printing it out in the console)
|
|
log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
|
|
}
|
|
});
|
|
|
|
if (nativeOSKeyStore.isNSSKeyStore) {
|
|
// Workaround bug 1492305: NSS-implemented methods don't reject when user cancels.
|
|
unlockPromise = unlockPromise.then(() => {
|
|
log.debug("ensureLoggedIn: isNSSKeyStore: ", reauth, Services.logins.isLoggedIn);
|
|
// User has hit the cancel button on the master password prompt.
|
|
// We must reject the promise chain here.
|
|
if (!Services.logins.isLoggedIn) {
|
|
throw Components.Exception("User canceled OS unlock entry (Workaround)", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
});
|
|
}
|
|
|
|
unlockPromise = unlockPromise.then(() => {
|
|
log.debug("ensureLoggedIn: Logged in");
|
|
this._pendingUnlockPromise = null;
|
|
this._isLocked = false;
|
|
|
|
return true;
|
|
}, (err) => {
|
|
log.debug("ensureLoggedIn: Not logged in", err);
|
|
this._pendingUnlockPromise = null;
|
|
this._isLocked = true;
|
|
|
|
return false;
|
|
});
|
|
|
|
this._pendingUnlockPromise = unlockPromise;
|
|
|
|
return this._pendingUnlockPromise;
|
|
},
|
|
|
|
/**
|
|
* Decrypts cipherText.
|
|
*
|
|
* Note: In the event of an rejection, check the result property of the Exception
|
|
* object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
|
|
* don't show that dialog), apart from other errors (e.g., gracefully
|
|
* recover from that and still shows the dialog.)
|
|
*
|
|
* @param {string} cipherText Encrypted string including the algorithm details.
|
|
* @param {boolean|string} reauth If it's set to true or a string, prompt
|
|
* the reauth login dialog.
|
|
* The string may be shown on the native OS
|
|
* login dialog.
|
|
* @returns {Promise<string>} resolves to the decrypted string, or rejects otherwise.
|
|
*/
|
|
async decrypt(cipherText, reauth = false) {
|
|
if (!await this.ensureLoggedIn(reauth)) {
|
|
throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
|
|
}
|
|
let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
|
|
return String.fromCharCode.apply(String, bytes);
|
|
},
|
|
|
|
/**
|
|
* Encrypts a string and returns cipher text containing algorithm information used for decryption.
|
|
*
|
|
* @param {string} plainText Original string without encryption.
|
|
* @returns {Promise<string>} resolves to the encrypted string (with algorithm), otherwise rejects.
|
|
*/
|
|
async encrypt(plainText) {
|
|
if (!await this.ensureLoggedIn()) {
|
|
throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
|
|
}
|
|
|
|
// Convert plain text into a UTF-8 binary string
|
|
plainText = unescape(encodeURIComponent(plainText));
|
|
|
|
// Convert it to an array
|
|
let textArr = [];
|
|
for (let char of plainText) {
|
|
textArr.push(char.charCodeAt(0));
|
|
}
|
|
|
|
let rawEncryptedText = await nativeOSKeyStore.asyncEncryptBytes(this.STORE_LABEL, textArr.length, textArr);
|
|
|
|
// Mark the output with a version number.
|
|
return rawEncryptedText;
|
|
},
|
|
|
|
/**
|
|
* Resolve when the login dialogs are closed, immediately if none are open.
|
|
*
|
|
* An existing MP dialog will be focused and will request attention.
|
|
*
|
|
* @returns {Promise<boolean>}
|
|
* Resolves with whether the user is logged in to MP.
|
|
*/
|
|
async waitForExistingDialog() {
|
|
if (this.isUIBusy) {
|
|
return this._pendingUnlockPromise;
|
|
}
|
|
return this.isLoggedIn;
|
|
},
|
|
|
|
/**
|
|
* Remove the store. For tests.
|
|
*/
|
|
async cleanup() {
|
|
return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
|
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
|
|
return new ConsoleAPI({
|
|
maxLogLevelPref: "extensions.formautofill.loglevel",
|
|
prefix: "OSKeyStore",
|
|
});
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_testReauth", TEST_ONLY_REAUTH, "");
|
|
XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_reauthEnabledByUser",
|
|
"extensions.formautofill.reauth.enabled", false);
|