/* * 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 https://mozilla.org/MPL/2.0/. */ "use strict"; var EXPORTED_SYMBOLS = ["OpenPGPMasterpass"]; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; XPCOMUtils.defineLazyModuleGetters(lazy, { EnigmailLog: "chrome://openpgp/content/modules/log.jsm", MailUtils: "resource:///modules/MailUtils.jsm", RNP: "chrome://openpgp/content/modules/RNP.jsm", }); var OpenPGPMasterpass = { _initDone: false, _sdr: null, getSDR() { if (!this._sdr) { try { this._sdr = Cc["@mozilla.org/security/sdr;1"].getService( Ci.nsISecretDecoderRing ); } catch (ex) { lazy.EnigmailLog.writeException("masterpass.jsm", ex); } } return this._sdr; }, filename: "encrypted-openpgp-passphrase.txt", secringFilename: "secring.gpg", getPassPath() { let path = Services.dirsvc.get("ProfD", Ci.nsIFile); path.append(this.filename); return path; }, getSecretKeyRingFile() { let path = Services.dirsvc.get("ProfD", Ci.nsIFile); path.append(this.secringFilename); return path; }, getOpenPGPSecretRingAlreadyExists() { return this.getSecretKeyRingFile().exists(); }, async _repairOrWarn() { let [prot, unprot] = lazy.RNP.getProtectedKeysCount(); let haveAtLeastOneSecretKey = prot || unprot; if ( !(await IOUtils.exists(this.getPassPath().path)) && haveAtLeastOneSecretKey ) { // We couldn't read the OpenPGP password from file. // This could either mean the file doesn't exist, which indicates // either a corruption, or the condition after a failed migration // from early Enigmail migrator versions (bug 1656287). // Or it could mean the user has a primary password set, // but the user failed to enter it correctly, // or we are facing the consequences of multiple password prompts. let secFileName = this.getSecretKeyRingFile().path; let title = "OpenPGP corruption detected"; if (prot) { let info; if (!unprot) { info = "Your Thunderbird Profile contains inconsistent or corrupted OpenPGP data. You have secret keys that were previously protected with an automatic passphrase, " + "but file encrypted-openpgp-passphrase.txt is missing. File " + secFileName + " that contains your secret keys cannot be accessed. " + "You must manually repair this corruption by moving the file to a different folder. Then restart, then import your secret keys from a backup. " + "The OpenPGP functionality will be disabled until repaired. "; } else { info = "Your Thunderbird Profile contains inconsistent or corrupted OpenPGP data. You have secret keys that were previously protected with an automatic passphrase, " + "but file encrypted-openpgp-passphrase.txt is missing. File " + secFileName + " contains secret keys cannot be accessed. However, it also contains unprotected keys, which you may continue to access. " + "You must manually repair this corruption by moving the file to a different folder. Then restart, then import your secret keys from a backup. You may also try to import the corrupted file, to import the unprotected keys. " + "The OpenPGP functionality will be disabled until repaired. "; } Services.prompt.alert(null, title, info); throw new Error( "Error, secring.gpg exists, but cannot obtain password from encrypted-openpgp-passphrase.txt" ); } else { // only unprotected keys // maybe https://bugzilla.mozilla.org/show_bug.cgi?id=1656287 let info = "Your Thunderbird Profile contains inconsistent or corrupted OpenPGP data. You have secret keys, " + "but file encrypted-openpgp-passphrase.txt is missing. " + "If you have recently used Enigmail version 2.2 to migrate your old keys, an incomplete migration is probably the cause of the corruption. " + "An automatic repair can be attempted. " + "The OpenPGP functionality will be disabled until repaired. " + "Before repairing, you should make a backup of file " + secFileName + " that contains your secret keys. " + "After repairing, you may run the Enigmail migration again, or use OpenPGP Key Manager to accept your keys as personal keys."; let button = "I confirm I created a backup. Perform automatic repair."; let promptFlags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + Services.prompt.BUTTON_POS_1_DEFAULT; let confirm = Services.prompt.confirmEx( null, // window title, info, promptFlags, button, null, null, null, {} ); if (confirm != 0) { throw new Error( "Error, secring.gpg exists, but cannot obtain password from encrypted-openpgp-passphrase.txt" ); } await this._ensurePasswordCreatedAndCached(); await lazy.RNP.protectUnprotectedKeys(); await lazy.RNP.saveKeyRings(); } } }, async _ensurePasswordCreatedAndCached() { if (this.cachedPassword) { return; } let sdr = this.getSDR(); if (!sdr) { throw new Error("Failed to obtain the SDR service."); } if (await IOUtils.exists(this.getPassPath().path)) { let encryptedPass = await IOUtils.readUTF8(this.getPassPath().path); encryptedPass = encryptedPass.trim(); if (!encryptedPass) { throw new Error( "Failed to obtain encrypted password data from file " + this.getPassPath().path ); } try { this.cachedPassword = sdr.decryptString(encryptedPass); // This is the success scenario, in which we return early. return; } catch (e) { // This code handles the corruption described in bug 1790610. // Failure to decrypt should be the only scenario that // reaches this code path. // Is a primary password set? let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService( Ci.nsIPK11TokenDB ); let token = tokenDB.getInternalKeyToken(); if (token.hasPassword && !token.isLoggedIn()) { // Yes, primary password is set, but user is not logged in. // Let's throw now, a future action will result in trying again. throw e; } // No. We have profile corruption: key4.db doesn't contain the // key to decrypt file encrypted-openpgp-passphrase.txt // Move to backup file and create a fresh file to fix the situation. let backup = await IOUtils.createUniqueFile( Services.dirsvc.get("ProfD", Ci.nsIFile).path, this.filename + ".corrupt" ); try { await IOUtils.move(this.getPassPath().path, backup); console.warn( `${this.filename} corruption fixed. Corrupted file moved to ${backup}` ); } catch (e2) { console.warn( `Cannot move corrupted file ${this.filename} to backup name ${backup}` ); // We cannot repair, so restarting doesn't help, keep running, // and hope the user notices this error in console. throw e2; } let secRingFile = this.getSecretKeyRingFile(); if (secRingFile.exists() && secRingFile.fileSize > 0) { // We have secret keys that can no longer be accessed. try { let backupOld = await IOUtils.createUniqueFile( Services.dirsvc.get("ProfD", Ci.nsIFile).path, this.secringFilename + ".old.corrupt" ); await IOUtils.move(secRingFile.path + ".old", backupOld); } catch (eOld) {} let backup2 = await IOUtils.createUniqueFile( Services.dirsvc.get("ProfD", Ci.nsIFile).path, this.secringFilename + ".corrupt" ); try { await IOUtils.move(secRingFile.path, backup2); console.warn( `secring.gpg corruption fixed. Corrupted file moved to ${backup}` ); await IOUtils.write(secRingFile.path, new Uint8Array()); } catch (e3) { console.warn( `Cannot move corrupted file ${this.filename} to backup name ${backup}` ); // We cannot repair, so restarting doesn't help, keep running, // and hope the user notices this error in console. throw e3; } // RNP might have already read the old file, we cannot easily // trigger rereading of the file, so let's restart. lazy.MailUtils.restartApplication(); return; } // If we arrive here, we have successfully repaired, and // can proceed with the code below to create a fresh file. } } if (await IOUtils.exists(this.getPassPath().path)) { // This check is an additional precaution, to prevent against // logic errors, or unexpected filesystem behavior. // If this file already exists, we MUST NOT create it again. // The code below is executed if the file does not exist yet, // or if the file was deleted or moved, after automatic repairing. throw new Error("File " + this.getPassPath().path + " already exists"); } // Make sure we don't use the new password unless we're successful // in encrypting and storing it to disk. // (This may fail if the user has a primary password set, // but refuses to enter it.) let newPass = this.generatePassword(); let encryptedPass = sdr.encryptString(newPass); if (!encryptedPass) { throw new Error("cannot create OpenPGP password"); } await IOUtils.writeUTF8(this.getPassPath().path, encryptedPass); this.cachedPassword = newPass; }, generatePassword() { // TODO: Patrick suggested to replace with // EnigmailRNG.getRandomString(numChars) const random_bytes = new Uint8Array(32); crypto.getRandomValues(random_bytes); let result = ""; for (let i = 0; i < 32; i++) { result += (random_bytes[i] % 16).toString(16); } return result; }, cachedPassword: null, // This function requires the password to already exist and be cached. retrieveCachedPassword() { if (!this.cachedPassword) { // Obviously some functionality requires the password, but we // don't have it yet. // The best we can do is spawn reading and caching asynchronously, // this will cause the password to be available once the user // retries the current operation. this.ensurePasswordIsCached(); throw new Error("no cached password"); } return this.cachedPassword; }, async ensurePasswordIsCached() { if (this.cachedPassword) { return; } if (!this._initDone) { // set flag immediately, to avoid any potential recursion // causing us to repair twice in parallel. this._initDone = true; await this._repairOrWarn(); } if (this.cachedPassword) { return; } await this._ensurePasswordCreatedAndCached(); }, // This function may trigger password creation, if necessary async retrieveOpenPGPPassword() { lazy.EnigmailLog.DEBUG("masterpass.jsm: retrieveMasterPassword()\n"); await this.ensurePasswordIsCached(); return this.cachedPassword; }, };