diff --git a/package/Makefile b/package/Makefile index c2d6e8b1..8979df58 100644 --- a/package/Makefile +++ b/package/Makefile @@ -1,110 +1,111 @@ # 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/. DEPTH = .. include $(DEPTH)/config/autoconf.mk PREF_JS_EXPORTS = $(srcdir)/prefs/enigmail.js COMPFILES = \ enigmail.js \ pgpmimeHandler.js \ mimeEncrypt.js \ prefs-service.js \ msgCompFields.js PREFFILES = prefs/enigmail.js MODFILES = \ addrbook.jsm \ app.jsm \ armor.jsm \ attachment.jsm \ autocrypt.jsm \ + autoKeyLocate.jsm \ card.jsm \ clipboard.jsm \ commandLine.jsm \ configBackup.jsm \ configure.jsm \ constants.jsm \ core.jsm \ data.jsm \ decryption.jsm \ decryptPermanently.jsm \ dialog.jsm \ encryption.jsm \ errorHandling.jsm \ events.jsm \ execution.jsm \ funcs.jsm \ files.jsm \ filters.jsm \ fixExchangeMsg.jsm \ gpgAgent.jsm \ glodaMime.jsm \ glodaUtils.jsm \ gpg.jsm \ hash.jsm \ httpProxy.jsm \ installGnuPG.jsm \ installPep.jsm \ key.jsm \ keyEditor.jsm \ keyRing.jsm \ keyUsability.jsm \ keyRefreshService.jsm \ keyserver.jsm \ keyserverUris.jsm \ lazy.jsm \ locale.jsm \ log.jsm \ mime.jsm \ mimeDecrypt.jsm \ mimeVerify.jsm \ os.jsm \ passwordCheck.jsm \ passwords.jsm \ pEp.jsm \ pEpAdapter.jsm \ pEpDecrypt.jsm \ pEpFilter.jsm \ pEpListener.jsm \ pEpKeySync.jsm \ pEpMessageHist.jsm \ pipeConsole.jsm \ prefs.jsm \ protocolHandler.jsm \ rng.jsm \ rules.jsm \ send.jsm \ socks5Proxy.jsm \ stdlib.jsm \ streams.jsm \ system.jsm \ time.jsm \ timer.jsm \ tor.jsm \ trust.jsm \ uris.jsm \ verify.jsm \ versioning.jsm \ webKey.jsm \ windows.jsm \ wksMimeHandler.jsm \ zbase32.jsm all: deploy deploy: $(PREFFILES) $(COMPFILES) $(MODFILES) $(DEPTH)/util/install -m 644 $(DIST)/components $(COMPFILES) $(DEPTH)/util/install -m 644 $(DIST)/defaults/preferences $(PREFFILES) $(DEPTH)/util/install -m 644 $(DIST)/modules $(MODFILES) clean: $(DEPTH)/util/install -u $(DIST)/components $(COMPFILES) $(DEPTH)/util/install -u $(DIST)/defaults/preferences $(PREFFILES) $(DEPTH)/util/install -u $(DIST)/modules $(MODFILES) diff --git a/package/autoKeyLocate.jsm b/package/autoKeyLocate.jsm new file mode 100644 index 00000000..5a55b2c9 --- /dev/null +++ b/package/autoKeyLocate.jsm @@ -0,0 +1,139 @@ +/*global Components: false*/ +/* 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/. */ + +"use strict"; + +/** + * Module implements rate limiting of --auto-key-locate. + */ + +var EXPORTED_SYMBOLS = ["EnigmailAutoKeyLocate"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Sqlite.jsm"); /* global Sqlite: false */ +Cu.import("resource://enigmail/log.jsm"); /* global EnigmailLog: false*/ +Cu.import("resource://enigmail/funcs.jsm"); /* global EnigmailFuncs: false*/ +Cu.import("resource://gre/modules/PromiseUtils.jsm"); /* global PromiseUtils: false */ +Cu.import("resource://enigmail/gpg.jsm"); /* global EnigmailGpg: false*/ + +var EnigmailAutoKeyLocate = { + /** + * Determine for an email address when we last attempted to + * obtain a key via wkd + */ + checkUser: function(email) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: processAutoKeyLocateHeader: from=" + email + "\n"); + + let conn; + + return Sqlite.openConnection({ + path: "enigmail.sqlite", + sharedMemoryCache: false + }).then(function onConnection(connection) { + conn = connection; + return checkDatabaseStructure(conn); + }, function onError(error) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: processAutoKeyLocateHeader: could not open database\n"); + }).then(function _f() { + return timeForRecheck(conn, email); + }).then(function _done(val) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: OK - closing connection\n"); + conn.close(); + return Promise.resolve(val); + }).catch(function _err(reason) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: error - closing connection: " + reason + "\n"); + conn.close(); + // in case something goes wrong we recheck anyway + return Promise.resolve(true); + }); + }, + + /** + * determine if WKD support is available in GnuPG + */ + isAvailable: function() { + return EnigmailGpg.getGpgFeature("supports-wkd"); + } +}; + + +/** + * Ensure that the database has the auto_key_locate_timestamps table. + * + * @param connection: Object - SQLite connection + * + * @return Promise + */ +function checkDatabaseStructure(connection) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: checkDatabaseStructure\n"); + + return connection.tableExists("auto_key_locate_timestamps").then( + function onSuccess(exists) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: checkDatabaseStructure - success\n"); + if (!exists) { + return createAutoKeyLocateTable(connection); + } + else { + return PromiseUtils.defer(); + } + }, + function onError(error) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: checkDatabaseStructure - error\n"); + Promise.reject(error); + }); +} + +/** + * Create the "auto_key_locate_timestamps" table. + * + * @param connection: Object - SQLite connection + * + * @return Promise + */ +function createAutoKeyLocateTable(connection) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: createAutoKeyLocateTable\n"); + + return connection.execute( + "create table auto_key_locate_timestamps (" + + "email text not null primary key, " + // email address of correspondent + "last_seen integer);"); // timestamp of last mail received for the email/type combination +} + +/** + * Check if enough time has passed since we looked-up the key for "email". + * + * @param connection: Object - SQLite connection + * @param email: String - Email address to search (in lowercase) + * + * @return Promise + */ +function timeForRecheck(connection, email) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: timeForRecheck\n"); + + let obj = { + email: email, + now: Date.now() + }; + + return connection.execute( + "select count(*) from auto_key_locate_timestamps where email = :email and :now - last_seen < 60*60*24", obj + ).then(function(val) { + return connection.execute( + "insert or replace into auto_key_locate_timestamps values (:email, :now)", obj + ).then(function() { + return Promise.resolve(val); + }); + }).then(function(rows) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: timeForRecheck: " + rows.toString() + "\n"); + + return rows.length === 1 && rows[0].getResultByIndex(0) === 0; + }, function(error) { + EnigmailLog.DEBUG("autoKeyLocate.jsm: timeForRecheck - error" + error + "\n"); + Promise.reject(error); + }); +} diff --git a/package/gpg.jsm b/package/gpg.jsm index 1c9b4e86..104370ed 100644 --- a/package/gpg.jsm +++ b/package/gpg.jsm @@ -1,380 +1,383 @@ /*global Components: false */ /*jshint -W097 */ /* * 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/. */ "use strict"; const EXPORTED_SYMBOLS = ["EnigmailGpg"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://enigmail/files.jsm"); /*global EnigmailFiles: false */ Cu.import("resource://enigmail/log.jsm"); /*global EnigmailLog: false */ Cu.import("resource://enigmail/locale.jsm"); /*global EnigmailLocale: false */ Cu.import("resource://enigmail/dialog.jsm"); /*global EnigmailDialog: false */ Cu.import("resource://enigmail/prefs.jsm"); /*global EnigmailPrefs: false */ Cu.import("resource://enigmail/execution.jsm"); /*global EnigmailExecution: false */ Cu.import("resource://enigmail/subprocess.jsm"); /*global subprocess: false */ Cu.import("resource://enigmail/core.jsm"); /*global EnigmailCore: false */ Cu.import("resource://enigmail/os.jsm"); /*global EnigmailOS: false */ Cu.import("resource://enigmail/versioning.jsm"); /*global EnigmailVersioning: false */ Cu.import("resource://enigmail/lazy.jsm"); /*global EnigmailLazy: false */ const getGpgAgent = EnigmailLazy.loader("enigmail/gpgAgent.jsm", "EnigmailGpgAgent"); const MINIMUM_GPG_VERSION = "2.0.10"; const GPG_BATCH_OPT_LIST = ["--batch", "--no-tty", "--status-fd", "2"]; function pushTrimmedStr(arr, str, splitStr) { // Helper function for pushing a string without leading/trailing spaces // to an array str = str.replace(/^ */, "").replace(/ *$/, ""); if (str.length > 0) { if (splitStr) { const tmpArr = str.split(/[\t ]+/); for (let i = 0; i < tmpArr.length; i++) { arr.push(tmpArr[i]); } } else { arr.push(str); } } return (str.length > 0); } function getDirmngrTorStatus(exitCodeObj) { const command = getGpgAgent().resolveToolPath("gpg-connect-agent"); if (command === null) { return null; } const args = ["--dirmngr"]; EnigmailLog.CONSOLE("enigmail> " + EnigmailFiles.formatCmdLine(command, args) + "\n"); let stdout = ""; try { exitCodeObj.value = subprocess.call({ command: command, arguments: args, environment: EnigmailCore.getEnvList(), stdin: function(stdin) { stdin.write("GETINFO tor\r\n"); stdin.write("bye\r\n"); stdin.write("\r\n"); stdin.close(); }, stdout: function(data) { stdout += data; } }).wait(); } catch (ex) { exitCodeObj.value = -1; EnigmailLog.DEBUG("enigmail> DONE with FAILURE\n"); } return stdout; } function dirmngrConfiguredWithTor() { if (!EnigmailGpg.getGpgFeature("supports-dirmngr")) return false; const exitCodeObj = { value: null }; const output = getDirmngrTorStatus(exitCodeObj); if (output === null || exitCodeObj.value < 0) { return false; } return output.match(/Tor mode is enabled/) !== null; } const EnigmailGpg = { agentVersion: "", _agentPath: null, get agentPath() { return this._agentPath; }, setAgentPath: function(path) { this._agentPath = path; }, /** * return the minimum version of GnuPG that is supported by Enigmail */ getMinimumGpgVersion: function() { return MINIMUM_GPG_VERSION; }, /*** determine if a specific feature is available in the GnuPG version used @featureName: String; one of the following values: version-supported - is the gpg version supported at all (true for gpg >= 2.0.10) supports-gpg-agent - is gpg-agent is usually provided (true for gpg >= 2.0) autostart-gpg-agent - is gpg-agent started automatically by gpg (true for gpg >= 2.0.16) keygen-passphrase - can the passphrase be specified when generating keys (false for gpg 2.1 and 2.1.1) windows-photoid-bug - is there a bug in gpg with the output of photoid on Windows (true for gpg < 2.0.16) genkey-no-protection - is "%no-protection" supported for generting keys (true for gpg >= 2.1) search-keys-cmd - what command to use to terminate the --search-key operation. ("save" for gpg > 2.1; "quit" otherwise) socks-on-windows - is SOCKS proxy supported on Windows (true for gpg >= 2.0.20) supports-dirmngr - is dirmngr supported (true for gpg >= 2.1) supports-ecc-keys - are ECC (elliptic curve) keys supported (true for gpg >= 2.1) supports-sender - does gnupg understand the --sender argument + supports-wkd - does gpg support wkd (web key directory) (true for gpg >= 2.1.19) @return: depending on featureName - Boolean unless specified differently: (true if feature is available / false otherwise) If the feature cannot be found, undefined is returned */ getGpgFeature: function(featureName) { let gpgVersion = EnigmailGpg.agentVersion; if (!gpgVersion || typeof(gpgVersion) != "string" || gpgVersion.length === 0) { return undefined; } gpgVersion = gpgVersion.replace(/\-.*$/, ""); if (gpgVersion.search(/^\d+\.\d+/) < 0) { // not a valid version number return undefined; } switch (featureName) { case "version-supported": return EnigmailVersioning.greaterThanOrEqual(gpgVersion, MINIMUM_GPG_VERSION); case "supports-gpg-agent": return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.0"); case "autostart-gpg-agent": return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.0.16"); case "keygen-passphrase": return EnigmailVersioning.lessThan(gpgVersion, "2.1") || EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.1.2"); case "genkey-no-protection": return EnigmailVersioning.greaterThan(gpgVersion, "2.1"); case "windows-photoid-bug": return EnigmailVersioning.lessThan(gpgVersion, "2.0.16"); case "supports-dirmngr": return EnigmailVersioning.greaterThan(gpgVersion, "2.1"); case "supports-ecc-keys": return EnigmailVersioning.greaterThan(gpgVersion, "2.1"); case "socks-on-windows": return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.0.20"); case "search-keys-cmd": // returns a string if (EnigmailVersioning.greaterThan(gpgVersion, "2.1")) { return "save"; } else return "quit"; case "supports-sender": return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.1.15"); + case "supports-wkd": + return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.1.19"); } return undefined; }, /** * get the standard arguments to pass to every GnuPG subprocess * * @withBatchOpts: Boolean - true: use --batch and some more options * false: don't use --batch and co. * * @return: Array of String - the list of arguments */ getStandardArgs: function(withBatchOpts) { // return the arguments to pass to every GnuPG subprocess let r = ["--charset", "utf-8", "--display-charset", "utf-8", "--use-agent"]; // mandatory parameter to add in all cases try { let p = EnigmailPrefs.getPref("agentAdditionalParam").replace(/\\\\/g, "\\"); let i = 0; let last = 0; let foundSign = ""; let startQuote = -1; while ((i = p.substr(last).search(/['"]/)) >= 0) { if (startQuote == -1) { startQuote = i; foundSign = p.substr(last).charAt(i); last = i + 1; } else if (p.substr(last).charAt(i) == foundSign) { // found enquoted part if (startQuote > 1) pushTrimmedStr(r, p.substr(0, startQuote), true); pushTrimmedStr(r, p.substr(startQuote + 1, last + i - startQuote - 1), false); p = p.substr(last + i + 1); last = 0; startQuote = -1; foundSign = ""; } else { last = last + i + 1; } } pushTrimmedStr(r, p, true); } catch (ex) {} if (withBatchOpts) { r = r.concat(GPG_BATCH_OPT_LIST); } return r; }, // returns the output of --with-colons --list-config getGnupgConfig: function(exitCodeObj, errorMsgObj) { const args = EnigmailGpg.getStandardArgs(true). concat(["--fixed-list-mode", "--with-colons", "--list-config"]); const statusMsgObj = {}; const cmdErrorMsgObj = {}; const statusFlagsObj = {}; const listText = EnigmailExecution.execCmd(EnigmailGpg.agentPath, args, "", exitCodeObj, statusFlagsObj, statusMsgObj, cmdErrorMsgObj); if (exitCodeObj.value !== 0) { errorMsgObj.value = EnigmailLocale.getString("badCommand"); if (cmdErrorMsgObj.value) { errorMsgObj.value += "\n" + EnigmailFiles.formatCmdLine(EnigmailGpg.agentPath, args); errorMsgObj.value += "\n" + cmdErrorMsgObj.value; } return ""; } return listText.replace(/(\r\n|\r)/g, "\n"); }, /** * return an array containing the aliases and the email addresses * of groups defined in gpg.conf * * @return: array of objects with the following properties: * - alias: group name as used by GnuPG * - keylist: list of keys (any form that GnuPG accepts), separated by ";" * * (see docu for gnupg parameter --group) */ getGpgGroups: function() { const exitCodeObj = {}; const errorMsgObj = {}; const cfgStr = EnigmailGpg.getGnupgConfig(exitCodeObj, errorMsgObj); if (exitCodeObj.value !== 0) { EnigmailDialog.alert(errorMsgObj.value); return null; } const groups = []; const cfg = cfgStr.split(/\n/); for (let i = 0; i < cfg.length; i++) { if (cfg[i].indexOf("cfg:group") === 0) { const groupArr = cfg[i].split(/:/); groups.push({ alias: groupArr[2], keylist: groupArr[3] }); } } return groups; }, /** * Force GnuPG to recalculate the trust db. This is sometimes required after importing keys. * * no return value */ recalcTrustDb: function() { EnigmailLog.DEBUG("enigmailCommon.jsm: recalcTrustDb:\n"); const command = EnigmailGpg.agentPath; const args = EnigmailGpg.getStandardArgs(false). concat(["--check-trustdb"]); try { const proc = subprocess.call({ command: EnigmailGpg.agentPath, arguments: args, environment: EnigmailCore.getEnvList(), charset: null, mergeStderr: false }); proc.wait(); } catch (ex) { EnigmailLog.ERROR("enigmailCommon.jsm: recalcTrustDb: subprocess.call failed with '" + ex.toString() + "'\n"); throw ex; } }, signingAlgIdToString: function(id) { // RFC 4880 Sec. 9.1, RFC 6637 Sec. 5 and draft-koch-eddsa-for-openpgp-03 Sec. 8 switch (parseInt(id, 10)) { case 1: case 2: case 3: return "RSA"; case 16: return "Elgamal"; case 17: return "DSA"; case 18: return "ECDH"; case 19: return "ECDSA"; case 20: return "ELG"; case 22: return "EDDSA"; default: return EnigmailLocale.getString("unknownSigningAlg", [parseInt(id, 10)]); } }, hashAlgIdToString: function(id) { // RFC 4880 Sec. 9.4 switch (parseInt(id, 10)) { case 1: return "MD5"; case 2: return "SHA-1"; case 3: return "RIPE-MD/160"; case 8: return "SHA256"; case 9: return "SHA384"; case 10: return "SHA512"; case 11: return "SHA224"; default: return EnigmailLocale.getString("unknownHashAlg", [parseInt(id, 10)]); } }, /** * For versions of GPG 2.1 and higher, checks to see if the dirmngr is configured to use Tor * * @return Boolean - True if dirmngr is configured with Tor. False otherwise */ dirmngrConfiguredWithTor: dirmngrConfiguredWithTor }; diff --git a/ui/content/enigmailLocateKeys.js b/ui/content/enigmailLocateKeys.js new file mode 100644 index 00000000..aa2e8695 --- /dev/null +++ b/ui/content/enigmailLocateKeys.js @@ -0,0 +1,90 @@ +/*global Components: false*/ +/* + * 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/. + */ + +/* eslint no-invalid-this: 0 */ + +"use strict"; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cc = Components.classes; + +Cu.import("resource://enigmail/core.jsm"); /*global EnigmailCore: false */ +Cu.import("resource://enigmail/log.jsm"); /*global EnigmailLog: false */ +Cu.import("resource://enigmail/locale.jsm"); /*global EnigmailLocale: false */ +Cu.import("resource://enigmail/errorHandling.jsm"); /*global EnigmailErrorHandling: false */ +Cu.import("resource://enigmail/dialog.jsm"); /*global EnigmailDialog: false */ +Cu.import("resource://enigmail/gpgAgent.jsm"); /*global EnigmailGpgAgent: false */ +Cu.import("resource://enigmail/execution.jsm"); /*global EnigmailExecution: false */ +Cu.import("resource://enigmail/keyRing.jsm"); /*global EnigmailKeyRing: false */ +Cu.import("resource://enigmail/autoKeyLocate.jsm"); /*global EnigmailAutoKeyLocate: false */ + +function onLoad() { + document.getElementById("dialog.status2").value = "Locating Keys..."; + let progressDlg = document.getElementById("dialog.progress"); + progressDlg.setAttribute("mode", "undetermined"); + + let inArg = window.arguments[0].toAddr.toString(); + EnigmailLog.DEBUG("enigmailLocateKeys.js: to: " + inArg + "\n"); + + if (inArg.trim() !== "") { + let listener = EnigmailExecution.newSimpleListener(null, function(ret) { + EnigmailLog.DEBUG(listener.stdoutData); + EnigmailLog.DEBUG(listener.stderrData); + let imported = listener.stdoutData.includes("IMPORT_OK"); + if (ret === 0 && imported) { + EnigmailKeyRing.clearCache(); + window.arguments[1] = { + repeatEvaluation: true, + foundKeys: true + }; + } + progressDlg.setAttribute("value", 100); + progressDlg.setAttribute("mode", "normal"); + window.close(); + }); + + Promise.all(inArg.split(",").map(function(x) { + return EnigmailAutoKeyLocate.checkUser(x.trim()); + })).then(function(checks) { + let toCheck = []; + let emails = inArg.split(","); + + EnigmailLog.DEBUG("enigmailLocateKeys.js: checks " + checks.toString() + "\n"); + + for (let i = 0; i < checks.length; i++) { + if (checks[i]) { + EnigmailLog.DEBUG("enigmailLocateKeys.js: recheck " + emails[i] + "\n"); + toCheck.push(emails[i]); + } + else { + EnigmailLog.DEBUG("enigmailLocateKeys.js: skip check " + emails[i] + "\n"); + } + } + + if (emails.length > 0) { + let proc = EnigmailExecution.execStart(EnigmailGpgAgent.agentPath, [ + "--verbose", + "--status-fd", "1", + "--auto-key-locate", "wkd", + "--locate-keys" + ].concat(toCheck), false, window, listener, { + value: null + }); + } + }); + } + else { + progressDlg.setAttribute("value", 100); + progressDlg.setAttribute("mode", "normal"); + window.close(); + } +} + +function onAccept() {} + +function onUnload() {} diff --git a/ui/content/enigmailLocateKeys.xul b/ui/content/enigmailLocateKeys.xul new file mode 100644 index 00000000..027745e0 --- /dev/null +++ b/ui/content/enigmailLocateKeys.xul @@ -0,0 +1,65 @@ + + + + + + + + + + + + + +