mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-06 11:18:19 +02:00
Detailed description in the Bug 1807783. It's complicated to create a test because the issue is intermittent. I've discovered this issue while working on Bug 1807618. Changes there provoked intermittent failures more often and prompted investigation. With this patch tests are running smoothly for Bug 1807618. Differential Revision: https://phabricator.services.mozilla.com/D165602
315 lines
9 KiB
JavaScript
315 lines
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/. */
|
|
|
|
const { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
lazy,
|
|
"LoginHelper",
|
|
"resource://gre/modules/LoginHelper.jsm"
|
|
);
|
|
|
|
function LoginManagerCrypto_SDR() {
|
|
this.init();
|
|
}
|
|
|
|
LoginManagerCrypto_SDR.prototype = {
|
|
classID: Components.ID("{dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}"),
|
|
QueryInterface: ChromeUtils.generateQI(["nsILoginManagerCrypto"]),
|
|
|
|
__decoderRing: null, // nsSecretDecoderRing service
|
|
get _decoderRing() {
|
|
if (!this.__decoderRing) {
|
|
this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].getService(
|
|
Ci.nsISecretDecoderRing
|
|
);
|
|
}
|
|
return this.__decoderRing;
|
|
},
|
|
|
|
__utfConverter: null, // UCS2 <--> UTF8 string conversion
|
|
get _utfConverter() {
|
|
if (!this.__utfConverter) {
|
|
this.__utfConverter = Cc[
|
|
"@mozilla.org/intl/scriptableunicodeconverter"
|
|
].createInstance(Ci.nsIScriptableUnicodeConverter);
|
|
this.__utfConverter.charset = "UTF-8";
|
|
}
|
|
return this.__utfConverter;
|
|
},
|
|
|
|
_utfConverterReset() {
|
|
this.__utfConverter = null;
|
|
},
|
|
|
|
_uiBusy: false,
|
|
|
|
init() {
|
|
// Check to see if the internal PKCS#11 token has been initialized.
|
|
// If not, set a blank password.
|
|
let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
|
|
Ci.nsIPK11TokenDB
|
|
);
|
|
|
|
let token = tokenDB.getInternalKeyToken();
|
|
if (token.needsUserInit) {
|
|
this.log("Initializing key3.db with default blank password.");
|
|
token.initPassword("");
|
|
}
|
|
},
|
|
|
|
/*
|
|
* encrypt
|
|
*
|
|
* Encrypts the specified string, using the SecretDecoderRing.
|
|
*
|
|
* Returns the encrypted string, or throws an exception if there was a
|
|
* problem.
|
|
*/
|
|
encrypt(plainText) {
|
|
let cipherText = null;
|
|
|
|
let wasLoggedIn = this.isLoggedIn;
|
|
let canceledMP = false;
|
|
|
|
this._uiBusy = !wasLoggedIn;
|
|
try {
|
|
let plainOctet = this._utfConverter.ConvertFromUnicode(plainText);
|
|
plainOctet += this._utfConverter.Finish();
|
|
cipherText = this._decoderRing.encryptString(plainOctet);
|
|
} catch (e) {
|
|
this.log(`Failed to encrypt string with error ${e.name}.`);
|
|
// If the user clicks Cancel, we get NS_ERROR_FAILURE.
|
|
// (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
|
|
if (e.result == Cr.NS_ERROR_FAILURE) {
|
|
canceledMP = true;
|
|
throw Components.Exception(
|
|
"User canceled primary password entry",
|
|
Cr.NS_ERROR_ABORT
|
|
);
|
|
} else {
|
|
throw Components.Exception(
|
|
"Couldn't encrypt string",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
} finally {
|
|
this._uiBusy = false;
|
|
// If we triggered a primary password prompt, notify observers.
|
|
if (!wasLoggedIn && this.isLoggedIn) {
|
|
this._notifyObservers("passwordmgr-crypto-login");
|
|
} else if (canceledMP) {
|
|
this._notifyObservers("passwordmgr-crypto-loginCanceled");
|
|
}
|
|
}
|
|
return cipherText;
|
|
},
|
|
|
|
/*
|
|
* encryptMany
|
|
*
|
|
* Encrypts the specified strings, using the SecretDecoderRing.
|
|
*
|
|
* Returns a promise which resolves with the the encrypted strings,
|
|
* or throws/rejects with an error if there was a problem.
|
|
*/
|
|
async encryptMany(plaintexts) {
|
|
if (!Array.isArray(plaintexts) || !plaintexts.length) {
|
|
throw Components.Exception(
|
|
"Need at least one plaintext to encrypt",
|
|
Cr.NS_ERROR_INVALID_ARG
|
|
);
|
|
}
|
|
|
|
let cipherTexts;
|
|
|
|
let wasLoggedIn = this.isLoggedIn;
|
|
let canceledMP = false;
|
|
|
|
this._uiBusy = !wasLoggedIn;
|
|
try {
|
|
cipherTexts = await this._decoderRing.asyncEncryptStrings(plaintexts);
|
|
} catch (e) {
|
|
this.log(`Failed to encrypt strings with error ${e.name}.`);
|
|
// If the user clicks Cancel, we get NS_ERROR_FAILURE.
|
|
// (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
|
|
if (e.result == Cr.NS_ERROR_FAILURE) {
|
|
canceledMP = true;
|
|
throw Components.Exception(
|
|
"User canceled primary password entry",
|
|
Cr.NS_ERROR_ABORT
|
|
);
|
|
} else {
|
|
throw Components.Exception(
|
|
"Couldn't encrypt strings",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
} finally {
|
|
this._uiBusy = false;
|
|
// If we triggered a primary password prompt, notify observers.
|
|
if (!wasLoggedIn && this.isLoggedIn) {
|
|
this._notifyObservers("passwordmgr-crypto-login");
|
|
} else if (canceledMP) {
|
|
this._notifyObservers("passwordmgr-crypto-loginCanceled");
|
|
}
|
|
}
|
|
return cipherTexts;
|
|
},
|
|
|
|
/*
|
|
* decrypt
|
|
*
|
|
* Decrypts the specified string, using the SecretDecoderRing.
|
|
*
|
|
* Returns the decrypted string, or throws an exception if there was a
|
|
* problem.
|
|
*/
|
|
decrypt(cipherText) {
|
|
let plainText = null;
|
|
|
|
let wasLoggedIn = this.isLoggedIn;
|
|
let canceledMP = false;
|
|
|
|
this._uiBusy = !wasLoggedIn;
|
|
try {
|
|
let plainOctet;
|
|
plainOctet = this._decoderRing.decryptString(cipherText);
|
|
plainText = this._utfConverter.ConvertToUnicode(plainOctet);
|
|
} catch (e) {
|
|
this.log(
|
|
`Failed to decrypt cipher text of length ${cipherText.length} with error ${e.name}.`
|
|
);
|
|
|
|
// In the unlikely event the converter threw, reset it.
|
|
this._utfConverterReset();
|
|
|
|
// If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
|
|
// If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
|
|
// Wrong passwords are handled by the decoderRing reprompting;
|
|
// we get no notification.
|
|
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
|
|
canceledMP = true;
|
|
throw Components.Exception(
|
|
"User canceled primary password entry",
|
|
Cr.NS_ERROR_ABORT
|
|
);
|
|
} else {
|
|
throw Components.Exception(
|
|
"Couldn't decrypt string",
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
} finally {
|
|
this._uiBusy = false;
|
|
// If we triggered a primary password prompt, notify observers.
|
|
if (!wasLoggedIn && this.isLoggedIn) {
|
|
this._notifyObservers("passwordmgr-crypto-login");
|
|
} else if (canceledMP) {
|
|
this._notifyObservers("passwordmgr-crypto-loginCanceled");
|
|
}
|
|
}
|
|
|
|
return plainText;
|
|
},
|
|
|
|
/**
|
|
* Decrypts the specified strings, using the SecretDecoderRing.
|
|
*
|
|
* @resolve {string[]} The decrypted strings. If a string cannot
|
|
* be decrypted, the empty string is returned for that instance.
|
|
* Callers will need to use decrypt() to determine if the encrypted
|
|
* string is invalid or intentionally empty. Throws/reject with
|
|
* an error if there was a problem.
|
|
*/
|
|
async decryptMany(cipherTexts) {
|
|
if (!Array.isArray(cipherTexts) || !cipherTexts.length) {
|
|
throw Components.Exception(
|
|
"Need at least one ciphertext to decrypt",
|
|
Cr.NS_ERROR_INVALID_ARG
|
|
);
|
|
}
|
|
|
|
let plainTexts = [];
|
|
|
|
let wasLoggedIn = this.isLoggedIn;
|
|
let canceledMP = false;
|
|
|
|
this._uiBusy = !wasLoggedIn;
|
|
try {
|
|
plainTexts = await this._decoderRing.asyncDecryptStrings(cipherTexts);
|
|
} catch (e) {
|
|
this.log(`Failed to decrypt strings with error ${e.name}.`);
|
|
// If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
|
|
// If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
|
|
// Wrong passwords are handled by the decoderRing reprompting;
|
|
// we get no notification.
|
|
if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
|
|
canceledMP = true;
|
|
throw Components.Exception(
|
|
"User canceled primary password entry",
|
|
Cr.NS_ERROR_ABORT
|
|
);
|
|
} else {
|
|
throw Components.Exception(
|
|
"Couldn't decrypt strings: " + e.result,
|
|
Cr.NS_ERROR_FAILURE
|
|
);
|
|
}
|
|
} finally {
|
|
this._uiBusy = false;
|
|
// If we triggered a primary password prompt, notify observers.
|
|
if (!wasLoggedIn && this.isLoggedIn) {
|
|
this._notifyObservers("passwordmgr-crypto-login");
|
|
} else if (canceledMP) {
|
|
this._notifyObservers("passwordmgr-crypto-loginCanceled");
|
|
}
|
|
}
|
|
return plainTexts;
|
|
},
|
|
|
|
/*
|
|
* uiBusy
|
|
*/
|
|
get uiBusy() {
|
|
return this._uiBusy;
|
|
},
|
|
|
|
/*
|
|
* isLoggedIn
|
|
*/
|
|
get isLoggedIn() {
|
|
let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
|
|
Ci.nsIPK11TokenDB
|
|
);
|
|
let token = tokenDB.getInternalKeyToken();
|
|
return !token.hasPassword || token.isLoggedIn();
|
|
},
|
|
|
|
/*
|
|
* defaultEncType
|
|
*/
|
|
get defaultEncType() {
|
|
return Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
|
|
},
|
|
|
|
/*
|
|
* _notifyObservers
|
|
*/
|
|
_notifyObservers(topic) {
|
|
this.log(`Prompted for a primary password, notifying for ${topic}`);
|
|
Services.obs.notifyObservers(null, topic);
|
|
},
|
|
}; // end of nsLoginManagerCrypto_SDR implementation
|
|
|
|
XPCOMUtils.defineLazyGetter(LoginManagerCrypto_SDR.prototype, "log", () => {
|
|
let logger = lazy.LoginHelper.createLogger("Login crypto");
|
|
return logger.log.bind(logger);
|
|
});
|
|
|
|
const EXPORTED_SYMBOLS = ["LoginManagerCrypto_SDR"];
|