diff options
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/keyRing.jsm')
-rw-r--r-- | comm/mail/extensions/openpgp/content/modules/keyRing.jsm | 2202 |
1 files changed, 2202 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/keyRing.jsm b/comm/mail/extensions/openpgp/content/modules/keyRing.jsm new file mode 100644 index 0000000000..07b5c36991 --- /dev/null +++ b/comm/mail/extensions/openpgp/content/modules/keyRing.jsm @@ -0,0 +1,2202 @@ +/* + * 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 = ["EnigmailKeyRing"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailStringUtils } = ChromeUtils.import( + "resource:///modules/MailStringUtils.jsm" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + CollectedKeysDB: "chrome://openpgp/content/modules/CollectedKeysDB.jsm", + OpenPGPAlias: "chrome://openpgp/content/modules/OpenPGPAlias.jsm", + EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm", + EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm", + EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm", + EnigmailLog: "chrome://openpgp/content/modules/log.jsm", + EnigmailTrust: "chrome://openpgp/content/modules/trust.jsm", + EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm", + EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm", + GPGME: "chrome://openpgp/content/modules/GPGME.jsm", + newEnigmailKeyObj: "chrome://openpgp/content/modules/keyObj.jsm", + PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm", + RNP: "chrome://openpgp/content/modules/RNP.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "l10n", () => { + return new Localization(["messenger/openpgp/openpgp.ftl"], true); +}); + +let gKeyListObj = null; +let gKeyIndex = []; +let gSubkeyIndex = []; +let gLoadingKeys = false; + +/* + + This module operates with a Key Store (array) containing objects with the following properties: + + * keyList [Array] of EnigmailKeyObj + + * keySortList [Array]: used for quickly sorting the keys + - userId (in lower case) + - keyId + - keyNum + * trustModel: [String]. One of: + - p: pgp/classical + - t: always trust + - a: auto (:0) (default, currently pgp/classical) + - T: TOFU + - TP: TOFU+PGP + +*/ + +var EnigmailKeyRing = { + _initialized: false, + + init() { + if (this._initialized) { + return; + } + this._initialized = true; + this.clearCache(); + }, + + /** + * Get the complete list of all public keys, optionally sorted by a column + * + * @param win - optional |object| holding the parent window for displaying error messages + * @param sortColumn - optional |string| containing the column name for sorting. One of: + * userid, keyid, keyidshort, fpr, keytype, validity, trust, created, expiry + * @param sortDirection - |number| 1 = ascending / -1 = descending + * + * @returns keyListObj - |object| { keyList, keySortList } (see above) + */ + getAllKeys(win, sortColumn, sortDirection) { + if (gKeyListObj.keySortList.length === 0) { + loadKeyList(win, sortColumn, sortDirection); + //EnigmailWindows.keyManReloadKeys(); + /* TODO: do we need something similar with TB's future trust behavior? + if (!gKeyCheckDone) { + gKeyCheckDone = true; + runKeyUsabilityCheck(); + } + */ + } else if (sortColumn) { + gKeyListObj.keySortList.sort( + getSortFunction(sortColumn.toLowerCase(), gKeyListObj, sortDirection) + ); + } + + return gKeyListObj; + }, + + /** + * get 1st key object that matches a given key ID or subkey ID + * + * @param keyId - String: key Id with 16 characters (preferred) or 8 characters), + * or fingerprint (40 or 32 characters). + * Optionally preceded with "0x" + * @param noLoadKeys - Boolean [optional]: do not try to load the key list first + * + * @returns Object - found KeyObject or null if key not found + */ + getKeyById(keyId, noLoadKeys) { + lazy.EnigmailLog.DEBUG("keyRing.jsm: getKeyById: " + keyId + "\n"); + + if (!keyId) { + return null; + } + + if (keyId.search(/^0x/) === 0) { + keyId = keyId.substr(2); + } + keyId = keyId.toUpperCase(); + + if (!noLoadKeys) { + this.getAllKeys(); // ensure keylist is loaded; + } + + let keyObj = gKeyIndex[keyId]; + + if (keyObj === undefined) { + keyObj = gSubkeyIndex[keyId]; + } + + return keyObj !== undefined ? keyObj : null; + }, + + isSubkeyId(keyId) { + if (!keyId) { + throw new Error("keyId parameter not set"); + } + + keyId = keyId.replace(/^0x/, "").toUpperCase(); + + let keyObj = gSubkeyIndex[keyId]; + + return keyObj !== undefined; + }, + + /** + * get all key objects that match a given email address + * + * @param searchTerm - String: an email address to match against all UIDs of the keys. + * An empty string will return no result + * @param onlyValidUid - Boolean: if true (default), invalid (e.g. revoked) UIDs are not matched + * + * @param allowExpired - Boolean: if true, expired keys are matched. + * + * @returns Array of KeyObjects with the found keys (array length is 0 if no key found) + */ + getKeysByEmail(email, onlyValidUid = true, allowExpired = false) { + lazy.EnigmailLog.DEBUG("keyRing.jsm: getKeysByEmail: '" + email + "'\n"); + + let res = []; + if (!email) { + return res; + } + + this.getAllKeys(); // ensure keylist is loaded; + email = email.toLowerCase(); + + for (let key of gKeyListObj.keyList) { + if (!allowExpired && key.keyTrust == "e") { + continue; + } + + for (let userId of key.userIds) { + if (userId.type !== "uid") { + continue; + } + + // Skip test if it's expired. If expired isn't allowed, we + // already skipped it above. + if ( + onlyValidUid && + userId.keyTrust != "e" && + lazy.EnigmailTrust.isInvalid(userId.keyTrust) + ) { + continue; + } + + if ( + lazy.EnigmailFuncs.getEmailFromUserID(userId.userId).toLowerCase() === + email + ) { + res.push(key); + break; + } + } + } + return res; + }, + + emailAddressesWithSecretKey: null, + + async _populateEmailHasSecretKeyCache() { + this.emailAddressesWithSecretKey = new Set(); + + this.getAllKeys(); // ensure keylist is loaded; + + for (let key of gKeyListObj.keyList) { + if (!key.secretAvailable) { + continue; + } + let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(key.fpr); + if (!isPersonal) { + continue; + } + for (let userId of key.userIds) { + if (userId.type !== "uid") { + continue; + } + if (lazy.EnigmailTrust.isInvalid(userId.keyTrust)) { + continue; + } + this.emailAddressesWithSecretKey.add( + lazy.EnigmailFuncs.getEmailFromUserID(userId.userId).toLowerCase() + ); + } + } + }, + + /** + * This API uses a cache. It helps when making lookups from multiple + * places, during a longer transaction. + * Currently, the cache isn't refreshed automatically. + * Set this.emailAddressesWithSecretKey to null when starting a new + * operation that needs fresh information. + */ + async hasSecretKeyForEmail(emailAddr) { + if (!this.emailAddressesWithSecretKey) { + await this._populateEmailHasSecretKeyCache(); + } + + return this.emailAddressesWithSecretKey.has(emailAddr); + }, + + /** + * Specialized function that takes into account + * the specifics of email addresses in UIDs. + * + * @param emailAddr: String - email address to search for without any angulars + * or names + * + * @returns KeyObject with the found key, or null if no key found + */ + async getSecretKeyByEmail(emailAddr) { + let result = {}; + await this.getAllSecretKeysByEmail(emailAddr, result, true); + return result.best; + }, + + async getAllSecretKeysByEmail(emailAddr, result, allowExpired) { + lazy.EnigmailLog.DEBUG( + "keyRing.jsm: getAllSecretKeysByEmail: '" + emailAddr + "'\n" + ); + let keyList = this.getKeysByEmail(emailAddr, true, true); + + result.all = []; + result.best = null; + + var nowDate = new Date(); + var nowSecondsSinceEpoch = nowDate.valueOf() / 1000; + let bestIsExpired = false; + + for (let key of keyList) { + if (!key.secretAvailable) { + continue; + } + let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(key.fpr); + if (!isPersonal) { + continue; + } + if ( + key.getEncryptionValidity(true, "ignoreExpired").keyValid && + key.getSigningValidity("ignoreExpired").keyValid + ) { + let thisIsExpired = + key.expiryTime != 0 && key.expiryTime < nowSecondsSinceEpoch; + if (!allowExpired && thisIsExpired) { + continue; + } + result.all.push(key); + if (!result.best) { + result.best = key; + bestIsExpired = thisIsExpired; + } else if ( + result.best.algoSym === key.algoSym && + result.best.keySize === key.keySize + ) { + if (!key.expiryTime || key.expiryTime > result.best.expiryTime) { + result.best = key; + } + } else if (bestIsExpired && !thisIsExpired) { + if ( + result.best.algoSym.search(/^(DSA|RSA)$/) < 0 && + key.algoSym.search(/^(DSA|RSA)$/) === 0 + ) { + // prefer RSA or DSA over ECC (long-term: change this once ECC keys are widely supported) + result.best = key; + bestIsExpired = thisIsExpired; + } else if ( + key.getVirtualKeySize() > result.best.getVirtualKeySize() + ) { + result.best = key; + bestIsExpired = thisIsExpired; + } + } + } + } + }, + + /** + * get a list of keys for a given set of (sub-) key IDs + * + * @param keyIdList: Array of key IDs + OR String, with space-separated list of key IDs + */ + getKeyListById(keyIdList) { + lazy.EnigmailLog.DEBUG( + "keyRing.jsm: getKeyListById: '" + keyIdList + "'\n" + ); + let keyArr; + if (typeof keyIdList === "string") { + keyArr = keyIdList.split(/ +/); + } else { + keyArr = keyIdList; + } + + let ret = []; + for (let i in keyArr) { + let r = this.getKeyById(keyArr[i]); + if (r) { + ret.push(r); + } + } + + return ret; + }, + + /** + * @param {nsIFile} file - ASCII armored file containing the revocation. + */ + async importRevFromFile(file) { + let contents = await IOUtils.readUTF8(file.path); + + const beginIndexObj = {}; + const endIndexObj = {}; + const blockType = lazy.EnigmailArmor.locateArmoredBlock( + contents, + 0, + "", + beginIndexObj, + endIndexObj, + {} + ); + if (!blockType) { + return; + } + + if (blockType.search(/^(PUBLIC|PRIVATE) KEY BLOCK$/) !== 0) { + return; + } + + let pgpBlock = contents.substr( + beginIndexObj.value, + endIndexObj.value - beginIndexObj.value + 1 + ); + + const cApi = lazy.EnigmailCryptoAPI(); + let res = await cApi.importRevBlockAPI(pgpBlock); + if (res.exitCode) { + return; + } + + EnigmailKeyRing.clearCache(); + lazy.EnigmailWindows.keyManReloadKeys(); + }, + + /** + * Import a secret key from the given file. + * + * @param {nsIFile} file - ASCII armored file containing the revocation. + * @param {nsIWindow} win - parent window + * @param {Function} passCB - a callback function that will be called if the user needs + * to enter a passphrase to unlock a secret key. See passphrasePromptCallback + * for the function signature. + * @param {object} errorMsgObj - errorMsgObj.value will contain an error + * message in case of failures + * @param {object} importedKeysObj - importedKeysObj.value will contain + * an array of the FPRs imported + */ + async importSecKeyFromFile( + win, + passCB, + keepPassphrases, + inputFile, + errorMsgObj, + importedKeysObj + ) { + lazy.EnigmailLog.DEBUG( + "keyRing.jsm: EnigmailKeyRing.importSecKeyFromFile: fileName=" + + inputFile.path + + "\n" + ); + + let data = await IOUtils.read(inputFile.path); + let contents = MailStringUtils.uint8ArrayToByteString(data); + let res; + let tryAgain; + let permissive = false; + do { + tryAgain = false; + let failed = true; + + try { + // strict on first attempt, permissive on optional second attempt + res = await lazy.RNP.importSecKeyBlockImpl( + win, + passCB, + keepPassphrases, + contents, + permissive + ); + failed = + !res || res.exitCode || !res.importedKeys || !res.importedKeys.length; + } catch (ex) { + lazy.EnigmailDialog.alert(win, ex); + } + + if (failed) { + if (!permissive) { + let agreed = lazy.EnigmailDialog.confirmDlg( + win, + lazy.l10n.formatValueSync("confirm-permissive-import") + ); + if (agreed) { + permissive = true; + tryAgain = true; + } + } else { + lazy.EnigmailDialog.alert( + win, + lazy.l10n.formatValueSync("import-keys-failed") + ); + } + } + } while (tryAgain); + + if (!res || !res.importedKeys) { + return 1; + } + + if (importedKeysObj) { + importedKeysObj.keys = res.importedKeys; + } + if (res.importedKeys.length > 0) { + EnigmailKeyRing.updateKeys(res.importedKeys); + } + EnigmailKeyRing.clearCache(); + + return res.exitCode; + }, + + /** + * empty the key cache, such that it will get loaded next time it is accessed + * + * no input or return values + */ + clearCache() { + lazy.EnigmailLog.DEBUG("keyRing.jsm: EnigmailKeyRing.clearCache\n"); + gKeyListObj = { + keyList: [], + keySortList: [], + }; + + gKeyIndex = []; + gSubkeyIndex = []; + }, + + /** + * Check if the cache is empty + * + * @returns Boolean: true: cache cleared + */ + getCacheEmpty() { + return gKeyIndex.length === 0; + }, + + /** + * Get a list of UserIds for a given key. + * Only the Only UIDs with highest trust level are returned. + * + * @param String keyId key, optionally preceded with 0x + * + * @returns Array of String: list of UserIds + */ + getValidUids(keyId) { + let keyObj = this.getKeyById(keyId); + if (keyObj) { + return this.getValidUidsFromKeyObj(keyObj); + } + return []; + }, + + getValidUidsFromKeyObj(keyObj) { + let r = []; + if (keyObj) { + const TRUSTLEVELS_SORTED = lazy.EnigmailTrust.trustLevelsSorted(); + let hideInvalidUid = true; + let maxTrustLevel = TRUSTLEVELS_SORTED.indexOf(keyObj.keyTrust); + + if (lazy.EnigmailTrust.isInvalid(keyObj.keyTrust)) { + // pub key not valid (anymore)-> display all UID's + hideInvalidUid = false; + } + + for (let i in keyObj.userIds) { + if (keyObj.userIds[i].type !== "uat") { + if (hideInvalidUid) { + let thisTrust = TRUSTLEVELS_SORTED.indexOf( + keyObj.userIds[i].keyTrust + ); + if (thisTrust > maxTrustLevel) { + r = [keyObj.userIds[i].userId]; + maxTrustLevel = thisTrust; + } else if (thisTrust === maxTrustLevel) { + r.push(keyObj.userIds[i].userId); + } + // else do not add uid + } else if ( + !lazy.EnigmailTrust.isInvalid(keyObj.userIds[i].keyTrust) || + !hideInvalidUid + ) { + // UID valid OR key not valid, but invalid keys allowed + r.push(keyObj.userIds[i].userId); + } + } + } + } + + return r; + }, + + /** + * Export public key(s) to a file + * + * @param {string[]} idArrayFull - array of key IDs or fingerprints + * to export (full keys). + * @param {string[]} idArrayReduced - array of key IDs or fingerprints + * to export (reduced keys, non-self signatures stripped). + * @param {String[]] idArrayMinimal - array of key IDs or fingerprints + * to export (minimal keys, user IDs and non-self signatures stripped). + * @param {String or nsIFile} outputFile - output file name or Object - or NULL + * @param {object} exitCodeObj - o.value will contain exit code + * @param {object} errorMsgObj - o.value will contain error message + * + * @returns String - if outputFile is NULL, the key block data; "" if a file is written + */ + async extractPublicKeys( + idArrayFull, + idArrayReduced, + idArrayMinimal, + outputFile, + exitCodeObj, + errorMsgObj + ) { + // At least one array must have valid input + if ( + (!idArrayFull || !Array.isArray(idArrayFull) || !idArrayFull.length) && + (!idArrayReduced || + !Array.isArray(idArrayReduced) || + !idArrayReduced.length) && + (!idArrayMinimal || + !Array.isArray(idArrayMinimal) || + !idArrayMinimal.length) + ) { + throw new Error("invalid parameter given to EnigmailKeyRing.extractKey"); + } + + exitCodeObj.value = -1; + + let keyBlock = lazy.RNP.getMultiplePublicKeys( + idArrayFull, + idArrayReduced, + idArrayMinimal + ); + if (!keyBlock) { + errorMsgObj.value = lazy.l10n.formatValueSync("fail-key-extract"); + return ""; + } + + exitCodeObj.value = 0; + if (outputFile) { + return IOUtils.writeUTF8(outputFile.path, keyBlock) + .then(() => { + return ""; + }) + .catch(async () => { + exitCodeObj.value = -1; + errorMsgObj.value = await lazy.l10n.formatValue("file-write-failed", { + output: outputFile.path, + }); + return null; + }); + } + return keyBlock; + }, + + promptKeyExport2AsciiFilename(window, label, defaultFilename) { + return lazy.EnigmailDialog.filePicker( + window, + label, + "", + true, + false, + "*.asc", + defaultFilename, + [lazy.l10n.formatValueSync("ascii-armor-file"), "*.asc"] + ); + }, + + async exportPublicKeysInteractive(window, defaultFileName, keyIdArray) { + let label = lazy.l10n.formatValueSync("export-to-file"); + let outFile = EnigmailKeyRing.promptKeyExport2AsciiFilename( + window, + label, + defaultFileName + ); + if (!outFile) { + return; + } + + var exitCodeObj = {}; + var errorMsgObj = {}; + + await EnigmailKeyRing.extractPublicKeys( + keyIdArray, // full + null, + null, + outFile, + exitCodeObj, + errorMsgObj + ); + if (exitCodeObj.value !== 0) { + lazy.EnigmailDialog.alert( + window, + lazy.l10n.formatValueSync("save-keys-failed") + ); + return; + } + lazy.EnigmailDialog.info(window, lazy.l10n.formatValueSync("save-keys-ok")); + }, + + backupSecretKeysInteractive(window, defaultFileName, fprArray) { + let label = lazy.l10n.formatValueSync("export-keypair-to-file"); + let outFile = EnigmailKeyRing.promptKeyExport2AsciiFilename( + window, + label, + defaultFileName + ); + + if (!outFile) { + return; + } + + window.openDialog( + "chrome://openpgp/content/ui/backupKeyPassword.xhtml", + "", + "dialog,modal,centerscreen,resizable", + { + okCallback: EnigmailKeyRing.exportSecretKey, + file: outFile, + fprArray, + } + ); + }, + + /** + * Export the secret key after a successful password setup. + * + * @param {string} password - The declared password to protect the keys. + * @param {Array} fprArray - The array of fingerprint of the selected keys. + * @param {object} file - The file where the keys should be saved. + * @param {boolean} confirmed - If the password was properly typed in the + * prompt. + */ + async exportSecretKey(password, fprArray, file, confirmed = false) { + // Interrupt in case this method has been called directly without confirming + // the input password through the password prompt. + if (!confirmed) { + return; + } + + let backupKeyBlock = await lazy.RNP.backupSecretKeys(fprArray, password); + if (!backupKeyBlock) { + Services.prompt.alert( + null, + lazy.l10n.formatValueSync("save-keys-failed") + ); + return; + } + + await IOUtils.writeUTF8(file.path, backupKeyBlock) + .then(async () => { + lazy.EnigmailDialog.info( + null, + await lazy.l10n.formatValue("save-keys-ok") + ); + }) + .catch(async () => { + Services.prompt.alert( + null, + await lazy.l10n.formatValue("file-write-failed", { + output: file.path, + }) + ); + }); + }, + + /** + * import key from provided key data (synchronous) + * + * @param parent nsIWindow + * @param askToConfirm Boolean - if true, display confirmation dialog + * @param keyBlock String - data containing key + * @param isBinary Boolean + * @param keyId String - key ID expected to import (no meaning) + * @param errorMsgObj Object - o.value will contain error message from GnuPG + * @param importedKeysObj Object - [OPTIONAL] o.value will contain an array of the FPRs imported + * @param minimizeKey Boolean - [OPTIONAL] minimize key for importing + * @param limitedUids Array<String> - [OPTIONAL] restrict importing the key(s) to a given set of UIDs + * @param allowPermissiveFallbackWithPrompt Boolean - If true, and regular import attempt fails, + * the user is asked to allow an optional + * permissive import attempt. + * @param {string} acceptance - Acceptance for the keys to import, + * which are new, or still have acceptance "undecided". + * + * @returns Integer - exit code: + * ExitCode == 0 => success + * ExitCode > 0 => error + * ExitCode == -1 => Cancelled by user + */ + importKey( + parent, + askToConfirm, + keyBlock, + isBinary, + keyId, + errorMsgObj, + importedKeysObj, + minimizeKey = false, + limitedUids = [], + allowPermissiveFallbackWithPrompt = true, + acceptance = null + ) { + const cApi = lazy.EnigmailCryptoAPI(); + return cApi.sync( + this.importKeyAsync( + parent, + askToConfirm, + keyBlock, + isBinary, + keyId, + errorMsgObj, + importedKeysObj, + minimizeKey, + limitedUids, + allowPermissiveFallbackWithPrompt, + acceptance + ) + ); + }, + + /** + * import key from provided key data + * + * @param parent nsIWindow + * @param askToConfirm Boolean - if true, display confirmation dialog + * @param keyBlock String - data containing key + * @param isBinary Boolean + * @param keyId String - key ID expected to import (no meaning) + * @param errorMsgObj Object - o.value will contain error message from GnuPG + * @param importedKeysObj Object - [OPTIONAL] o.value will contain an array of the FPRs imported + * @param minimizeKey Boolean - [OPTIONAL] minimize key for importing + * @param limitedUids Array<String> - [OPTIONAL] restrict importing the key(s) to a given set of UIDs + * @param allowPermissiveFallbackWithPrompt Boolean - If true, and regular import attempt fails, + * the user is asked to allow an optional + * permissive import attempt. + * @param acceptance String - The new acceptance value for the imported keys, + * which are new, or still have acceptance "undecided". + * + * @returns Integer - exit code: + * ExitCode == 0 => success + * ExitCode > 0 => error + * ExitCode == -1 => Cancelled by user + */ + async importKeyAsync( + parent, + askToConfirm, + keyBlock, + isBinary, + keyId, // ignored + errorMsgObj, + importedKeysObj, + minimizeKey = false, + limitedUids = [], + allowPermissiveFallbackWithPrompt = true, + acceptance = null + ) { + lazy.EnigmailLog.DEBUG( + `keyRing.jsm: EnigmailKeyRing.importKeyAsync('${keyId}', ${askToConfirm}, ${minimizeKey})\n` + ); + + var pgpBlock; + if (!isBinary) { + const beginIndexObj = {}; + const endIndexObj = {}; + const blockType = lazy.EnigmailArmor.locateArmoredBlock( + keyBlock, + 0, + "", + beginIndexObj, + endIndexObj, + {} + ); + if (!blockType) { + errorMsgObj.value = lazy.l10n.formatValueSync("no-pgp-block"); + return 1; + } + + if (blockType.search(/^(PUBLIC|PRIVATE) KEY BLOCK$/) !== 0) { + errorMsgObj.value = lazy.l10n.formatValueSync("not-first-block"); + return 1; + } + + pgpBlock = keyBlock.substr( + beginIndexObj.value, + endIndexObj.value - beginIndexObj.value + 1 + ); + } + + if (askToConfirm) { + if ( + !lazy.EnigmailDialog.confirmDlg( + parent, + lazy.l10n.formatValueSync("import-key-confirm"), + lazy.l10n.formatValueSync("key-man-button-import") + ) + ) { + errorMsgObj.value = lazy.l10n.formatValueSync("fail-cancel"); + return -1; + } + } + + if (minimizeKey) { + throw new Error("importKeyAsync with minimizeKey: not implemented"); + } + + const cApi = lazy.EnigmailCryptoAPI(); + let result = undefined; + let tryAgain; + let permissive = false; + do { + // strict on first attempt, permissive on optional second attempt + let blockParam = isBinary ? keyBlock : pgpBlock; + + result = await cApi.importPubkeyBlockAutoAcceptAPI( + parent, + blockParam, + acceptance, + permissive, + limitedUids + ); + + tryAgain = false; + let failed = + !result || + result.exitCode || + !result.importedKeys || + !result.importedKeys.length; + if (failed) { + if (allowPermissiveFallbackWithPrompt && !permissive) { + let agreed = lazy.EnigmailDialog.confirmDlg( + parent, + lazy.l10n.formatValueSync("confirm-permissive-import") + ); + if (agreed) { + permissive = true; + tryAgain = true; + } + } else if (askToConfirm) { + // if !askToConfirm the caller is responsible to handle the error + lazy.EnigmailDialog.alert( + parent, + lazy.l10n.formatValueSync("import-keys-failed") + ); + } + } + } while (tryAgain); + + if (!result) { + result = {}; + result.exitCode = -1; + } else if (result.importedKeys) { + if (importedKeysObj) { + importedKeysObj.value = result.importedKeys; + } + if (result.importedKeys.length > 0) { + EnigmailKeyRing.updateKeys(result.importedKeys); + } + } + + EnigmailKeyRing.clearCache(); + return result.exitCode; + }, + + async importKeyDataWithConfirmation( + window, + preview, + keyData, + isBinary, + limitedUids = [] + ) { + let somethingWasImported = false; + if (preview.length > 0) { + let outParam = {}; + if (lazy.EnigmailDialog.confirmPubkeyImport(window, preview, outParam)) { + let exitStatus; + let errorMsgObj = {}; + try { + exitStatus = await EnigmailKeyRing.importKeyAsync( + window, + false, + keyData, + isBinary, + "", + errorMsgObj, + null, + false, + limitedUids, + true, + outParam.acceptance + ); + } catch (ex) { + console.debug(ex); + } + + if (exitStatus === 0) { + let keyList = preview.map(a => a.id); + lazy.EnigmailDialog.keyImportDlg(window, keyList); + somethingWasImported = true; + } else { + lazy.l10n.formatValue("fail-key-import").then(value => { + lazy.EnigmailDialog.alert(window, value + "\n" + errorMsgObj.value); + }); + } + } + } else { + lazy.l10n.formatValue("no-key-found2").then(value => { + lazy.EnigmailDialog.alert(window, value); + }); + } + return somethingWasImported; + }, + + async importKeyArrayWithConfirmation( + window, + keyArray, + isBinary, + limitedUids = [] + ) { + let somethingWasImported = false; + if (keyArray.length > 0) { + let outParam = {}; + if (lazy.EnigmailDialog.confirmPubkeyImport(window, keyArray, outParam)) { + let importedKeys = []; + let allErrors = ""; + for (let key of keyArray) { + let exitStatus; + let errorMsgObj = {}; + try { + exitStatus = await EnigmailKeyRing.importKeyAsync( + window, + false, + key.pubKey, + isBinary, + "", + errorMsgObj, + null, + false, + limitedUids, + true, + outParam.acceptance + ); + } catch (ex) { + console.debug(ex); + } + + if (exitStatus === 0) { + importedKeys.push(key.id); + } else { + allErrors += "\n" + errorMsgObj.value; + } + } + + if (importedKeys.length) { + lazy.EnigmailDialog.keyImportDlg(window, importedKeys); + somethingWasImported = true; + } else { + lazy.l10n.formatValue("fail-key-import").then(value => { + lazy.EnigmailDialog.alert(window, value + allErrors); + }); + } + } + } else { + lazy.l10n.formatValue("no-key-found2").then(value => { + lazy.EnigmailDialog.alert(window, value); + }); + } + return somethingWasImported; + }, + + async importKeyDataSilent(window, keyData, isBinary, onlyFingerprint = "") { + let errorMsgObj = {}; + let exitStatus = -1; + try { + exitStatus = await EnigmailKeyRing.importKeyAsync( + window, + false, + keyData, + isBinary, + "", + errorMsgObj, + undefined, + false, + onlyFingerprint ? [onlyFingerprint] : [] + ); + this.clearCache(); + } catch (ex) { + console.debug(ex); + } + return exitStatus === 0; + }, + + /** + * Generate a new key pair with GnuPG + * + * @name: String - name part of UID + * @comment: String - comment part of UID (brackets are added) + * @comment: String - email part of UID (<> will be added) + * @expiryDate: Number - Unix timestamp of key expiry date; 0 if no expiry + * @keyLength: Number - size of key in bytes (e.g 4096) + * @keyType: String - RSA or ECC + * @passphrase: String - password; null if no password + * @listener: Object - { + * function onDataAvailable(data) {...}, + * function onStopRequest(exitCode) {...} + * } + * + * @return: handle to process + */ + generateKey( + name, + comment, + email, + expiryDate, + keyLength, + keyType, + passphrase, + listener + ) { + lazy.EnigmailLog.WRITE("keyRing.jsm: generateKey:\n"); + throw new Error("Not implemented"); + }, + + isValidForEncryption(keyObj) { + return this._getValidityLevelIgnoringAcceptance(keyObj, null, false) == 0; + }, + + // returns an acceptanceLevel from -1 to 3, + // or -2 for "doesn't match email" or "not usable" + async isValidKeyForRecipient(keyObj, emailAddr, allowExpired) { + if (!emailAddr) { + return -2; + } + + let level = this._getValidityLevelIgnoringAcceptance( + keyObj, + emailAddr, + allowExpired + ); + if (level < 0) { + return level; + } + return this._getAcceptanceLevelForEmail(keyObj, emailAddr); + }, + + /** + * This function checks that given key is not expired, not revoked, + * and that a (related) encryption (sub-)key is available. + * If an email address is provided by the caller, the function + * also requires that a matching user id is available. + * + * @param {object} keyObj - the key to check + * @param {string} [emailAddr] - optional email address + * @returns {Integer} - validity level, negative for invalid, + * 0 if no problem were found (neutral) + */ + _getValidityLevelIgnoringAcceptance(keyObj, emailAddr, allowExpired) { + if (keyObj.keyTrust == "r") { + return -2; + } + + if (keyObj.keyTrust == "e" && !allowExpired) { + return -2; + } + + if (emailAddr) { + let uidMatch = false; + for (let uid of keyObj.userIds) { + if (uid.type !== "uid") { + continue; + } + + if ( + lazy.EnigmailFuncs.getEmailFromUserID(uid.userId).toLowerCase() === + emailAddr + ) { + uidMatch = true; + break; + } + } + if (!uidMatch) { + return -2; + } + } + + // key valid for encryption? + if (!keyObj.keyUseFor.includes("E")) { + return -2; + } + + // Ensure we have at least one key usable for encryption + // that is not expired/revoked. + + // We already checked above, the primary key is not revoked/expired + let foundGoodEnc = keyObj.keyUseFor.match(/e/); + if (!foundGoodEnc) { + for (let aSub of keyObj.subKeys) { + if (aSub.keyTrust == "r") { + continue; + } + if (aSub.keyTrust == "e" && !allowExpired) { + continue; + } + if (aSub.keyUseFor.match(/e/)) { + foundGoodEnc = true; + break; + } + } + } + + if (!foundGoodEnc) { + return -2; + } + + return 0; // no problem found + }, + + async _getAcceptanceLevelForEmail(keyObj, emailAddr) { + let acceptanceLevel; + if (keyObj.secretAvailable) { + let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey( + keyObj.fpr + ); + if (isPersonal) { + acceptanceLevel = 3; + } else { + acceptanceLevel = -1; // rejected + } + } else { + acceptanceLevel = await this.getKeyAcceptanceLevelForEmail( + keyObj, + emailAddr + ); + } + + return acceptanceLevel; + }, + + /** + * try to find valid key for encryption to passed email address + * + * @param details if not null returns error in details.msg + * + * @return: found key ID (without leading "0x") or null + */ + async getValidKeyForRecipient(emailAddr, details) { + lazy.EnigmailLog.DEBUG( + 'keyRing.jsm: getValidKeyForRecipient(): emailAddr="' + emailAddr + '"\n' + ); + const FULLTRUSTLEVEL = 2; + + emailAddr = emailAddr.toLowerCase(); + + var foundKeyId = null; + var foundAcceptanceLevel = null; + + let k = this.getAllKeys(null, null); + let keyList = k.keyList; + + for (let keyObj of keyList) { + let acceptanceLevel = await this.isValidKeyForRecipient( + keyObj, + emailAddr, + false + ); + + // immediately return as best match, if a fully or ultimately + // trusted key is found + if (acceptanceLevel >= FULLTRUSTLEVEL) { + return keyObj.keyId; + } + + if (acceptanceLevel < 1) { + continue; + } + + if (foundKeyId != keyObj.keyId) { + // different matching key found + if ( + !foundKeyId || + (foundKeyId && acceptanceLevel > foundAcceptanceLevel) + ) { + foundKeyId = keyObj.keyId; + foundAcceptanceLevel = acceptanceLevel; + } + } + } + + if (!foundKeyId) { + if (details) { + details.msg = "ProblemNoKey"; + } + let msg = + "no valid encryption key with enough trust level for '" + + emailAddr + + "' found"; + lazy.EnigmailLog.DEBUG( + "keyRing.jsm: getValidKeyForRecipient(): " + msg + "\n" + ); + } else { + lazy.EnigmailLog.DEBUG( + "keyRing.jsm: getValidKeyForRecipient(): key=" + + foundKeyId + + '" found\n' + ); + } + return foundKeyId; + }, + + getAcceptanceStringFromAcceptanceLevel(level) { + switch (level) { + case 3: + return "personal"; + case 2: + return "verified"; + case 1: + return "unverified"; + case -1: + return "rejected"; + case 0: + default: + return "undecided"; + } + }, + + async getKeyAcceptanceLevelForEmail(keyObj, email) { + if (keyObj.secretAvailable) { + throw new Error( + `Unexpected private key parameter; keyObj.fpr=${keyObj.fpr}` + ); + } + + let acceptanceLevel = 0; + + let acceptanceResult = {}; + try { + await lazy.PgpSqliteDb2.getAcceptance( + keyObj.fpr, + email, + acceptanceResult + ); + } catch (ex) { + console.debug("getAcceptance failed: " + ex); + return null; + } + + if (acceptanceResult.fingerprintAcceptance == "rejected") { + // rejecting is always global for all email addresses + return -1; + } + + if (acceptanceResult.emailDecided) { + switch (acceptanceResult.fingerprintAcceptance) { + case "verified": + acceptanceLevel = 2; + break; + case "unverified": + acceptanceLevel = 1; + break; + default: + case "undecided": + acceptanceLevel = 0; + break; + } + } + return acceptanceLevel; + }, + + async getKeyAcceptanceForEmail(keyObj, email) { + let acceptanceResult = {}; + + try { + await lazy.PgpSqliteDb2.getAcceptance( + keyObj.fpr, + email, + acceptanceResult + ); + } catch (ex) { + console.debug("getAcceptance failed: " + ex); + return null; + } + + if (acceptanceResult.fingerprintAcceptance == "rejected") { + // rejecting is always global for all email addresses + return acceptanceResult.fingerprintAcceptance; + } + + if (acceptanceResult.emailDecided) { + switch (acceptanceResult.fingerprintAcceptance) { + case "verified": + case "unverified": + case "undecided": + return acceptanceResult.fingerprintAcceptance; + } + } + + return "undecided"; + }, + + /** + * Determine the key ID for a set of given addresses + * + * @param {Array<string>} addresses: email addresses + * @param {object} details: - holds details for invalid keys: + * - errArray: { + * addr {String}: email addresses + * msg {String}: related error + * } + * + * @returns {boolean}: true if at least one key missing; false otherwise + */ + async getValidKeysForAllRecipients(addresses, details) { + if (!addresses) { + return null; + } + // check whether each address is or has a key: + let keyMissing = false; + if (details) { + details.errArray = []; + } + for (let i = 0; i < addresses.length; i++) { + let addr = addresses[i]; + if (!addr) { + continue; + } + // try to find current address in key list: + var errMsg = null; + addr = addr.toLowerCase(); + if (!addr.includes("@")) { + throw new Error( + "getValidKeysForAllRecipients unexpected lookup for non-email addr: " + + addr + ); + } + + let aliasKeyList = this.getAliasKeyList(addr); + if (aliasKeyList) { + for (let entry of aliasKeyList) { + let foundError = true; + + let key; + if ("fingerprint" in entry) { + key = this.getKeyById(entry.fingerprint); + } else if ("id" in entry) { + key = this.getKeyById(entry.id); + } + if (key && this.isValidForEncryption(key)) { + let acceptanceResult = + await lazy.PgpSqliteDb2.getFingerprintAcceptance(null, key.fpr); + // If we don't have acceptance info for the key yet, + // or, we have it and it isn't rejected, + // then we accept the key for using it in alias definitions. + if (!acceptanceResult || acceptanceResult != "rejected") { + foundError = false; + } + } + + if (foundError) { + keyMissing = true; + if (details) { + let detEl = {}; + detEl.addr = addr; + detEl.msg = "alias problem"; + details.errArray.push(detEl); + } + console.debug( + 'keyRing.jsm: getValidKeysForAllRecipients(): alias key list for="' + + addr + + ' refers to missing or unusable key"\n' + ); + } + } + + // skip the lookup for direct matching keys by email + continue; + } + + // try email match: + var addrErrDetails = {}; + let foundKeyId = await this.getValidKeyForRecipient(addr, addrErrDetails); + if (details && addrErrDetails.msg) { + errMsg = addrErrDetails.msg; + } + if (!foundKeyId) { + // no key for this address found + keyMissing = true; + if (details) { + if (!errMsg) { + errMsg = "ProblemNoKey"; + } + var detailsElem = {}; + detailsElem.addr = addr; + detailsElem.msg = errMsg; + details.errArray.push(detailsElem); + } + lazy.EnigmailLog.DEBUG( + 'keyRing.jsm: getValidKeysForAllRecipients(): no single valid key found for="' + + addr + + '"\n' + ); + } + } + return keyMissing; + }, + + async getMultValidKeysForOneRecipient(emailAddr, allowExpired = false) { + lazy.EnigmailLog.DEBUG( + 'keyRing.jsm: getMultValidKeysForOneRecipient(): emailAddr="' + + emailAddr + + '"\n' + ); + emailAddr = emailAddr.toLowerCase(); + if (emailAddr.startsWith("<") && emailAddr.endsWith(">")) { + emailAddr = emailAddr.substr(1, emailAddr.length - 2); + } + + let found = []; + + let k = this.getAllKeys(null, null); + let keyList = k.keyList; + + for (let keyObj of keyList) { + let acceptanceLevel = await this.isValidKeyForRecipient( + keyObj, + emailAddr, + allowExpired + ); + if (acceptanceLevel < -1) { + continue; + } + if (!keyObj.secretAvailable) { + keyObj.acceptance = + this.getAcceptanceStringFromAcceptanceLevel(acceptanceLevel); + } + found.push(keyObj); + } + return found; + }, + + /** + * If the given email address has an alias definition, return its + * list of key identifiers. + * + * The function will prefer a match to an exact email alias. + * If no email alias could be found, the function will search for + * an alias rule that matches the domain. + * + * @param {string} email - The email address to look up + * @returns {[]} - An array with alias key identifiers found for the + * input, or null if no alias matches the address. + */ + getAliasKeyList(email) { + let ekl = lazy.OpenPGPAlias.getEmailAliasKeyList(email); + if (ekl) { + return ekl; + } + + return lazy.OpenPGPAlias.getDomainAliasKeyList(email); + }, + + /** + * Return the fingerprint of each usable alias key for the given + * email address. + * + * @param {string[]} keyList - Array of key identifiers + * @returns {string[]} An array with fingerprints of all alias keys, + * or an empty array on failure. + */ + getAliasKeys(keyList) { + let keys = []; + + for (let entry of keyList) { + let key; + let lookupId; + if ("fingerprint" in entry) { + lookupId = entry.fingerprint; + key = this.getKeyById(entry.fingerprint); + } else if ("id" in entry) { + lookupId = entry.id; + key = this.getKeyById(entry.id); + } + if (key && this.isValidForEncryption(key)) { + keys.push(key.fpr); + } else { + let reason = key ? "not usable" : "missing"; + console.debug( + "getAliasKeys: key for identifier: " + lookupId + " is " + reason + ); + return []; + } + } + + return keys; + }, + + /** + * Rebuild the quick access search indexes after the key list was loaded + */ + rebuildKeyIndex() { + gKeyIndex = []; + gSubkeyIndex = []; + + for (let i in gKeyListObj.keyList) { + let k = gKeyListObj.keyList[i]; + gKeyIndex[k.keyId] = k; + gKeyIndex[k.fpr] = k; + gKeyIndex[k.keyId.substr(-8, 8)] = k; + + // add subkeys + for (let j in k.subKeys) { + gSubkeyIndex[k.subKeys[j].keyId] = k; + } + } + }, + + /** + * Update specific keys in the key cache. If the key objects don't exist yet, + * they will be created + * + * @param keys: Array of String - key IDs or fingerprints + */ + updateKeys(keys) { + lazy.EnigmailLog.DEBUG("keyRing.jsm: updateKeys(" + keys.join(",") + ")\n"); + let uniqueKeys = [...new Set(keys)]; // make key IDs unique + + deleteKeysFromCache(uniqueKeys); + + if (gKeyListObj.keyList.length > 0) { + loadKeyList(null, null, 1, uniqueKeys); + } else { + loadKeyList(null, null, 1); + } + + lazy.EnigmailWindows.keyManReloadKeys(); + }, + + findRevokedPersonalKeysByEmail(email) { + let res = []; + if (email === "") { + return res; + } + email = email.toLowerCase(); + this.getAllKeys(); // ensure keylist is loaded; + for (let k of gKeyListObj.keyList) { + if (k.keyTrust != "r") { + continue; + } + let hasAdditionalEmail = false; + let isMatch = false; + + for (let userId of k.userIds) { + if (userId.type !== "uid") { + continue; + } + + let emailInUid = lazy.EnigmailFuncs.getEmailFromUserID( + userId.userId + ).toLowerCase(); + if (emailInUid == email) { + isMatch = true; + } else { + // For privacy reasons, exclude revoked keys that point to + // other email addresses. + hasAdditionalEmail = true; + break; + } + } + + if (isMatch && !hasAdditionalEmail) { + res.push("0x" + k.fpr); + } + } + return res; + }, + + // Forward to RNP, to avoid that other modules depend on RNP + async getRecipientAutocryptKeyForEmail(email) { + return lazy.RNP.getRecipientAutocryptKeyForEmail(email); + }, + + getAutocryptKey(keyId, email) { + let keyObj = this.getKeyById(keyId); + if ( + !keyObj || + !keyObj.subKeys.length || + !keyObj.userIds.length || + !keyObj.keyUseFor.includes("s") + ) { + return null; + } + let uid = keyObj.getUserIdWithEmail(email); + if (!uid) { + return null; + } + return lazy.RNP.getAutocryptKeyB64(keyId, null, uid.userId); + }, + + alreadyCheckedGnuPG: new Set(), + + /** + * @typedef {object} EncryptionKeyMeta + * @property {string} readiness - one of + * "accepted", "expiredAccepted", + * "otherAccepted", "expiredOtherAccepted", + * "undecided", "expiredUndecided", + * "rejected", "expiredRejected", + * "collected", "rejectedPersonal", "revoked", "alias" + * + * The meaning of "otherAccepted" is: the key is undecided for this + * email address, but accepted for at least on other address. + * + * @property {KeyObj} keyObj - + * undefined if an alias + * @property {CollectedKey} collectedKey - + * undefined if not a collected key or an alias + */ + + /** + * Obtain information on the availability of recipient keys + * for the given email address, and the status of the keys. + * + * No key details are returned for alias keys. + * + * If readiness is "collected" it's an unexpired key that hasn't + * been imported into permanent storage (keyring) yet. + * + * @param {string} email - email address + * + * @returns {EncryptionKeyMeta[]} - meta information for an encryption key + * + * Callers can filter it keys according to needs, like + * + * let meta = getEncryptionKeyMeta("foo@example.com"); + * let readyToUse = meta.filter(k => k.readiness == "accepted" || k.readiness == "alias"); + * let hasAlias = meta.filter(k => k.readiness == "alias"); + * let accepted = meta.filter(k => k.readiness == "accepted"); + * let expiredAccepted = meta.filter(k => k.readiness == "expiredAccepted"); + * let unaccepted = meta.filter(k => k.readiness == "undecided" || k.readiness == "rejected" ); + * let expiredUnaccepted = meta.filter(k => k.readiness == "expiredUndecided" || k.readiness == "expiredRejected"); + * let unacceptedNotYetImported = meta.filter(k => k.readiness == "collected"); + * let invalidKeys = meta.some(k => k.readiness == "revoked" || k.readiness == "rejectedPersonal" || ); + * + * let keyReadiness = meta.groupBy(({readiness}) => readiness); + */ + async getEncryptionKeyMeta(email) { + email = email.toLowerCase(); + + let result = []; + + result.hasAliasRule = lazy.OpenPGPAlias.hasAliasDefinition(email); + if (result.hasAliasRule) { + let keyMeta = {}; + keyMeta.readiness = "alias"; + result.push(keyMeta); + return result; + } + + let fingerprintsInKeyring = new Set(); + + for (let keyObj of this.getAllKeys(null, null).keyList) { + let keyMeta = {}; + keyMeta.keyObj = keyObj; + + let uidMatch = false; + for (let uid of keyObj.userIds) { + if (uid.type !== "uid") { + continue; + } + // key valid for encryption? + if (!keyObj.keyUseFor.includes("E")) { + continue; + } + + if ( + lazy.EnigmailFuncs.getEmailFromUserID(uid.userId).toLowerCase() === + email + ) { + uidMatch = true; + break; + } + } + if (!uidMatch) { + continue; + } + fingerprintsInKeyring.add(keyObj.fpr); + + if (keyObj.keyTrust == "r") { + keyMeta.readiness = "revoked"; + result.push(keyMeta); + continue; + } + let isExpired = keyObj.keyTrust == "e"; + + // Ensure we have at least one primary key or subkey usable for + // encryption that is not expired/revoked. + // We already checked above, the primary key is not revoked. + // If the primary key is good for encryption, we don't need to + // check subkeys. + if (!keyObj.keyUseFor.match(/e/)) { + let hasExpiredSubkey = false; + let hasRevokedSubkey = false; + let hasUsableSubkey = false; + + for (let aSub of keyObj.subKeys) { + if (!aSub.keyUseFor.match(/e/)) { + continue; + } + if (aSub.keyTrust == "e") { + hasExpiredSubkey = true; + } else if (aSub.keyTrust == "r") { + hasRevokedSubkey = true; + } else { + hasUsableSubkey = true; + } + } + + if (!hasUsableSubkey) { + if (hasExpiredSubkey) { + isExpired = true; + } else if (hasRevokedSubkey) { + keyMeta.readiness = "revoked"; + result.push(keyMeta); + continue; + } + } + } + + if (keyObj.secretAvailable) { + let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey( + keyObj.fpr + ); + if (isPersonal) { + keyMeta.readiness = "accepted"; + } else { + // We don't allow encrypting to rejected secret/personal keys. + keyMeta.readiness = "rejectedPersonal"; + result.push(keyMeta); + continue; + } + } else { + let acceptanceLevel = await this.getKeyAcceptanceLevelForEmail( + keyObj, + email + ); + switch (acceptanceLevel) { + case 1: + case 2: + keyMeta.readiness = isExpired ? "expiredAccepted" : "accepted"; + break; + case -1: + keyMeta.readiness = isExpired ? "expiredRejected" : "rejected"; + break; + case 0: + default: + let other = await lazy.PgpSqliteDb2.getFingerprintAcceptance( + null, + keyObj.fpr + ); + if (other == "verified" || other == "unverified") { + // If the check for the email returned undecided, but + // overall the key is marked as accepted, it means that + // the key is only accepted for another email address. + keyMeta.readiness = isExpired + ? "expiredOtherAccepted" + : "otherAccepted"; + } else { + keyMeta.readiness = isExpired ? "expiredUndecided" : "undecided"; + } + break; + } + } + result.push(keyMeta); + } + + if ( + Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg") && + Services.prefs.getBoolPref("mail.openpgp.fetch_pubkeys_from_gnupg") && + !this.alreadyCheckedGnuPG.has(email) + ) { + this.alreadyCheckedGnuPG.add(email); + let keysFromGnuPGMap = lazy.GPGME.getPublicKeysForEmail(email); + for (let aFpr of keysFromGnuPGMap.keys()) { + let oldKey = this.getKeyById(aFpr); + let gpgKeyData = keysFromGnuPGMap.get(aFpr); + if (oldKey) { + await this.importKeyDataSilent(null, gpgKeyData, false); + } else { + let k = await lazy.RNP.getKeyListFromKeyBlockImpl(gpgKeyData); + if (!k) { + continue; + } + if (k.length != 1) { + continue; + } + let db = await lazy.CollectedKeysDB.getInstance(); + // If key is known in the db: merge + update. + let key = await db.mergeExisting(k[0], gpgKeyData, { + uri: "", + type: "gnupg", + }); + await db.storeKey(key); + } + } + } + + let collDB = await lazy.CollectedKeysDB.getInstance(); + let coll = await collDB.findKeysForEmail(email); + for (let c of coll) { + let k = await lazy.RNP.getKeyListFromKeyBlockImpl(c.pubKey); + if (!k) { + continue; + } + if (k.length != 1) { + // Past code could have store key blocks that contained + // multiple entries. Ignore and delete. + collDB.deleteKey(k[0].fpr); + continue; + } + + let deleteFromCollected = false; + + if (fingerprintsInKeyring.has(k[0].fpr)) { + deleteFromCollected = true; + } else { + let trust = k[0].keyTrust; + if (trust == "r" || trust == "e") { + deleteFromCollected = true; + } + } + + if (!deleteFromCollected) { + // Ensure we have at least one primary key or subkey usable for + // encryption that is not expired/revoked. + // If the primary key is good for encryption, we don't need to + // check subkeys. + + if (!k[0].keyUseFor.match(/e/)) { + let hasUsableSubkey = false; + + for (let aSub of k[0].subKeys) { + if (!aSub.keyUseFor.match(/e/)) { + continue; + } + if (aSub.keyTrust != "e" && aSub.keyTrust != "r") { + hasUsableSubkey = true; + break; + } + } + + if (!hasUsableSubkey) { + deleteFromCollected = true; + } + } + } + + if (deleteFromCollected) { + collDB.deleteKey(k[0].fpr); + continue; + } + + let keyMeta = {}; + keyMeta.readiness = "collected"; + keyMeta.keyObj = k[0]; + keyMeta.collectedKey = c; + + result.push(keyMeta); + } + + return result; + }, +}; // EnigmailKeyRing + +/************************ INTERNAL FUNCTIONS ************************/ + +function sortByUserId(keyListObj, sortDirection) { + return function (a, b) { + return a.userId < b.userId ? -sortDirection : sortDirection; + }; +} + +const sortFunctions = { + keyid(keyListObj, sortDirection) { + return function (a, b) { + return a.keyId < b.keyId ? -sortDirection : sortDirection; + }; + }, + + keyidshort(keyListObj, sortDirection) { + return function (a, b) { + return a.keyId.substr(-8, 8) < b.keyId.substr(-8, 8) + ? -sortDirection + : sortDirection; + }; + }, + + fpr(keyListObj, sortDirection) { + return function (a, b) { + return keyListObj.keyList[a.keyNum].fpr < keyListObj.keyList[b.keyNum].fpr + ? -sortDirection + : sortDirection; + }; + }, + + keytype(keyListObj, sortDirection) { + return function (a, b) { + return keyListObj.keyList[a.keyNum].secretAvailable < + keyListObj.keyList[b.keyNum].secretAvailable + ? -sortDirection + : sortDirection; + }; + }, + + validity(keyListObj, sortDirection) { + return function (a, b) { + return lazy.EnigmailTrust.trustLevelsSorted().indexOf( + lazy.EnigmailTrust.getTrustCode(keyListObj.keyList[a.keyNum]) + ) < + lazy.EnigmailTrust.trustLevelsSorted().indexOf( + lazy.EnigmailTrust.getTrustCode(keyListObj.keyList[b.keyNum]) + ) + ? -sortDirection + : sortDirection; + }; + }, + + trust(keyListObj, sortDirection) { + return function (a, b) { + return lazy.EnigmailTrust.trustLevelsSorted().indexOf( + keyListObj.keyList[a.keyNum].ownerTrust + ) < + lazy.EnigmailTrust.trustLevelsSorted().indexOf( + keyListObj.keyList[b.keyNum].ownerTrust + ) + ? -sortDirection + : sortDirection; + }; + }, + + created(keyListObj, sortDirection) { + return function (a, b) { + return keyListObj.keyList[a.keyNum].keyCreated < + keyListObj.keyList[b.keyNum].keyCreated + ? -sortDirection + : sortDirection; + }; + }, + + expiry(keyListObj, sortDirection) { + return function (a, b) { + return keyListObj.keyList[a.keyNum].expiryTime < + keyListObj.keyList[b.keyNum].expiryTime + ? -sortDirection + : sortDirection; + }; + }, +}; + +function getSortFunction(type, keyListObj, sortDirection) { + return (sortFunctions[type] || sortByUserId)(keyListObj, sortDirection); +} + +/** + * Load the key list into memory and return it sorted by a specified column + * + * @param win - |object| holding the parent window for displaying error messages + * @param sortColumn - |string| containing the column name for sorting. One of: + * userid, keyid, keyidshort, fpr, keytype, validity, trust, created, expiry. + * Null will sort by userid. + * @param sortDirection - |number| 1 = ascending / -1 = descending + * @param onlyKeys - |array| of Strings: if defined, only (re-)load selected key IDs + * + * no return value + */ +function loadKeyList(win, sortColumn, sortDirection, onlyKeys = null) { + lazy.EnigmailLog.DEBUG("keyRing.jsm: loadKeyList( " + onlyKeys + ")\n"); + + if (gLoadingKeys) { + waitForKeyList(); + return; + } + gLoadingKeys = true; + + try { + const cApi = lazy.EnigmailCryptoAPI(); + cApi + .getKeys(onlyKeys) + .then(keyList => { + createAndSortKeyList( + keyList, + sortColumn, + sortDirection, + onlyKeys === null + ); + gLoadingKeys = false; + }) + .catch(e => { + lazy.EnigmailLog.ERROR(`keyRing.jsm: loadKeyList: error ${e} +`); + gLoadingKeys = false; + }); + waitForKeyList(); + } catch (ex) { + lazy.EnigmailLog.ERROR( + "keyRing.jsm: loadKeyList: exception: " + ex.toString() + ); + } +} + +/** + * Update the global key sort-list (quick index to keys) + * + * no return value + */ +function updateSortList() { + gKeyListObj.keySortList = []; + for (let i = 0; i < gKeyListObj.keyList.length; i++) { + let keyObj = gKeyListObj.keyList[i]; + gKeyListObj.keySortList.push({ + userId: keyObj.userId ? keyObj.userId.toLowerCase() : "", + keyId: keyObj.keyId, + fpr: keyObj.fpr, + keyNum: i, + }); + } +} + +/** + * Delete a set of keys from the key cache. Does not rebuild key indexes. + * Not found keys are skipped. + * + * @param keyList: Array of Strings: key IDs (or fpr) to delete + * + * @returns Array of deleted key objects + */ + +function deleteKeysFromCache(keyList) { + lazy.EnigmailLog.DEBUG( + "keyRing.jsm: deleteKeysFromCache(" + keyList.join(",") + ")\n" + ); + + let deleted = []; + let foundKeys = []; + for (let keyId of keyList) { + let k = EnigmailKeyRing.getKeyById(keyId, true); + if (k) { + foundKeys.push(k); + } + } + + for (let k of foundKeys) { + let foundIndex = -1; + for (let i = 0; i < gKeyListObj.keyList.length; i++) { + if (gKeyListObj.keyList[i].fpr == k.fpr) { + foundIndex = i; + break; + } + } + if (foundIndex >= 0) { + gKeyListObj.keyList.splice(foundIndex, 1); + deleted.push(k); + } + } + + return deleted; +} + +function createAndSortKeyList( + keyList, + sortColumn, + sortDirection, + resetKeyCache +) { + lazy.EnigmailLog.DEBUG("keyRing.jsm: createAndSortKeyList()\n"); + + if (typeof sortColumn !== "string") { + sortColumn = "userid"; + } + if (!sortDirection) { + sortDirection = 1; + } + + if (!("keyList" in gKeyListObj) || resetKeyCache) { + gKeyListObj.keyList = []; + gKeyListObj.keySortList = []; + gKeyListObj.trustModel = "?"; + } + + gKeyListObj.keyList = gKeyListObj.keyList.concat( + keyList.map(k => { + return lazy.newEnigmailKeyObj(k); + }) + ); + + // update the quick index for sorting keys + updateSortList(); + + // create a hash-index on key ID (8 and 16 characters and fingerprint) + // in a single array + + EnigmailKeyRing.rebuildKeyIndex(); + + gKeyListObj.keySortList.sort( + getSortFunction(sortColumn.toLowerCase(), gKeyListObj, sortDirection) + ); +} + +/* +function runKeyUsabilityCheck() { + EnigmailLog.DEBUG("keyRing.jsm: runKeyUsabilityCheck()\n"); + + setTimeout(function() { + try { + let msg = getKeyUsability().keyExpiryCheck(); + + if (msg && msg.length > 0) { + EnigmailDialog.info(null, msg); + } else { + getKeyUsability().checkOwnertrust(); + } + } catch (ex) { + EnigmailLog.DEBUG( + "keyRing.jsm: runKeyUsabilityCheck: exception " + + ex.message + + "\n" + + ex.stack + + "\n" + ); + } + }, 60 * 1000); // 1 minute +} +*/ + +function waitForKeyList() { + let mainThread = Services.tm.mainThread; + while (gLoadingKeys) { + mainThread.processNextEvent(true); + } +} |