diff options
Diffstat (limited to 'toolkit/components/passwordmgr/crypto-SDR.sys.mjs')
-rw-r--r-- | toolkit/components/passwordmgr/crypto-SDR.sys.mjs | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/crypto-SDR.sys.mjs b/toolkit/components/passwordmgr/crypto-SDR.sys.mjs new file mode 100644 index 0000000000..da3ffe9b58 --- /dev/null +++ b/toolkit/components/passwordmgr/crypto-SDR.sys.mjs @@ -0,0 +1,309 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", +}); + +export 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); +}); |