aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPatrick Brunschwig <[email protected]>2021-05-09 18:49:29 +0200
committerPatrick Brunschwig <[email protected]>2021-05-09 18:49:29 +0200
commit81ff4241b4a7294d6f6ae195d06bfd842a9fbb67 (patch)
tree6ec487c21be279c8a1a4f1c6703d00584a6f853c
parent09dd169bf7b58e17ed5e2aa8bfea8e2038e63be7 (diff)
downloadenigmail-81ff4241b4a7294d6f6ae195d06bfd842a9fbb67.tar.gz
enigmail-81ff4241b4a7294d6f6ae195d06bfd842a9fbb67.tar.bz2
enigmail-81ff4241b4a7294d6f6ae195d06bfd842a9fbb67.zip
- implemented decryption and signature verification
- fixed importing of keys
-rw-r--r--package/cryptoAPI/gpgme.js475
-rw-r--r--package/openpgp.jsm8
-rw-r--r--package/tests/gpgme-test.js259
-rw-r--r--ui/locale/en-US/enigmail.properties2
4 files changed, 656 insertions, 88 deletions
diff --git a/package/cryptoAPI/gpgme.js b/package/cryptoAPI/gpgme.js
index 45aac387..7b73926b 100644
--- a/package/cryptoAPI/gpgme.js
+++ b/package/cryptoAPI/gpgme.js
@@ -10,12 +10,16 @@
var EXPORTED_SYMBOLS = ["getGpgMEApi"];
var Services = Components.utils.import("resource://gre/modules/Services.jsm").Services;
+const XPCOMUtils = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm").XPCOMUtils;
if (typeof CryptoAPI === "undefined") {
Services.scriptloader.loadSubScript("chrome://enigmail/content/modules/cryptoAPI/interface.js",
null, "UTF-8"); /* global CryptoAPI */
}
+/* eslint no-invalid-this: 0 */
+XPCOMUtils.defineLazyModuleGetter(this, "EnigmailKeyRing", "chrome://enigmail/content/modules/keyRing.jsm", "EnigmailKeyRing"); /* global EnigmailKeyRing: false */
+
const EnigmailLog = ChromeUtils.import("chrome://enigmail/content/modules/log.jsm").EnigmailLog;
const EnigmailExecution = ChromeUtils.import("chrome://enigmail/content/modules/execution.jsm").EnigmailExecution;
const EnigmailFiles = ChromeUtils.import("chrome://enigmail/content/modules/files.jsm").EnigmailFiles;
@@ -23,9 +27,8 @@ const EnigmailConstants = ChromeUtils.import("chrome://enigmail/content/modules/
const EnigmailTime = ChromeUtils.import("chrome://enigmail/content/modules/time.jsm").EnigmailTime;
const EnigmailData = ChromeUtils.import("chrome://enigmail/content/modules/data.jsm").EnigmailData;
const EnigmailLocale = ChromeUtils.import("chrome://enigmail/content/modules/locale.jsm").EnigmailLocale;
-const EnigmailPassword = ChromeUtils.import("chrome://enigmail/content/modules/passwords.jsm").EnigmailPassword;
const EnigmailOS = ChromeUtils.import("chrome://enigmail/content/modules/os.jsm").EnigmailOS;
-const EnigmailCore = ChromeUtils.import("chrome://enigmail/content/modules/core.jsm").EnigmailCore;
+const EnigmailVersioning = ChromeUtils.import("chrome://enigmail/content/modules/versioning.jsm").EnigmailVersioning;
//const pgpjs_keys = ChromeUtils.import("chrome://enigmail/content/modules/cryptoAPI/pgpjs-keys.jsm").pgpjs_keys;
@@ -64,6 +67,7 @@ class GpgMECryptoAPI extends CryptoAPI {
* @param {String } preferredPath: try to use specific path to locate tool (gpg)
*/
initialize(parentWindow, esvc, preferredPath) {
+ EnigmailLog.DEBUG(`gpgme.js: initialize()\n`);
if (!esvc) {
esvc = {
environment: Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment)
@@ -71,12 +75,37 @@ class GpgMECryptoAPI extends CryptoAPI {
}
this._gpgmePath = resolvePath(esvc.environment);
+ this._gpgPath = null;
+ try {
+ let opts = this.sync(this.execJsonCmd({
+ op: "config"
+ }));
+
+ for (let o of opts.components) {
+ if (o.name === "gpg") {
+ this._gpgPath = o.program_name;
+ break;
+ }
+ }
+
+ if (! this._gpgPath) throw "GnuPG not available";
+
+ let r = this.sync(determineGpgVersion(this._gpgPath));
+ this._gpgPath = r.gpgPath;
+ this._gpgVersion = r.gpgVersion;
+ }
+ catch(ex) {
+ EnigmailLog.DEBUG(`gpgme.js: initialize: error: ${ex.toString()}\n`);
+ this._gpgmePath = null;
+ throw ex;
+ }
}
/**
* Close/shutdown anything related to the functionality
*/
finalize() {
+ // TODO: terminate running gpgme-json instance
return null;
}
@@ -147,10 +176,16 @@ class GpgMECryptoAPI extends CryptoAPI {
* @return {Promise<Array of Object>}
*/
async getKeys(onlyKeys = null) {
- let keysObj = await this.execJsonCmd({
+ EnigmailLog.DEBUG(`gpgme.js: getKeys(${onlyKeys})\n`);
+ let cmdObj = {
"op": "keylist",
"with-secret": true
- });
+ };
+
+ if (onlyKeys) {
+ cmdObj.keys = onlyKeys.split(/[, ]+/);
+ }
+ let keysObj = await this.execJsonCmd(cmdObj);
let keyArr = [];
if ("keys" in keysObj) {
@@ -168,6 +203,7 @@ class GpgMECryptoAPI extends CryptoAPI {
* @return {Array<{alias,keylist}>} <{String,String}>
*/
getGroups() {
+ EnigmailLog.DEBUG(`gpgme.js: getGroups()\n`);
let cfg = this.sync(this.execJsonCmd({
op: "config_opt",
component: "gpg",
@@ -211,6 +247,8 @@ class GpgMECryptoAPI extends CryptoAPI {
* @return {Array of KeyObject} with type = "grp"
*/
getGroupList() {
+ EnigmailLog.DEBUG(`gpgme.js: getGroupList()\n`);
+
let groupList = this.getGroups();
let retList = [];
for (let grp of groupList) {
@@ -264,10 +302,12 @@ class GpgMECryptoAPI extends CryptoAPI {
*/
async importKeyFromFile(inputFile) {
+ EnigmailLog.DEBUG(`gpgme.js: importKeyFromFile(${inputFile.path})\n`);
+
const EnigmailFiles = ChromeUtils.import("chrome://enigmail/content/modules/files.jsm").EnigmailFiles;
let fileData = EnigmailFiles.readBinaryFile(inputFile);
- return this.importKeyData(fileData, false, null);
+ return this.importKeyData(fileData, false);
}
/**
@@ -284,31 +324,58 @@ class GpgMECryptoAPI extends CryptoAPI {
* - {Number} importUnchanged: number of unchanged keys
*/
- async importKeyData(keyData, minimizeKey, limitedUids) {
- let res = await this.execJsonCmd({
- op: "import",
- data: keyData,
- protocol: "openpgp",
- base64: false
- });
+ async importKeyData(keyData, minimizeKey = false, limitedUids = []) {
+ let args = ["--no-verbose", "--status-fd", "2"];
+ if (minimizeKey) {
+ args = args.concat(["--import-options", "import-minimal"]);
+ }
- if ("result" in res) {
- EnigmailLog.DEBUG(`gpgme.js: importKeys: ${JSON.stringify(res)}`);
- let r = {
- exitCode: 0,
- importSum: res.result.considered,
- importedKeys: [],
- importUnchanged: res.result.unchanged
- };
+ if (limitedUids && limitedUids.length > 0 && this.supportsFeature("export-specific-uid")) {
+ let filter = limitedUids.map(i => {
+ return `mbox =~ ${i}`;
+ }).join(" || ");
- for (let k of res.result.imports) {
- r.importedKeys.push(k.fingerprint);
+ args.push("--import-filter");
+ args.push(`keep-uid=${filter}`);
+ }
+ args = args.concat(["--no-auto-check-trustdb", "--import"]);
+
+ const res = await EnigmailExecution.execAsync(this._gpgPath, args, keyData);
+
+ let importedKeys = [];
+ let importSum = 0;
+ let importUnchanged = 0;
+ let secCount = 0;
+ let secImported = 0;
+ let secDups = 0;
+
+ if (res.statusMsg) {
+ let r = parseImportResult(res.statusMsg);
+ if (r.exitCode !== -1) {
+ res.exitCode = r.exitCode;
+ }
+ if (r.errorMsg !== "") {
+ res.errorMsg = r.errorMsg;
}
- return r;
+ importedKeys = r.importedKeys;
+ importSum = r.importSum;
+ importUnchanged = r.importUnchanged;
+ secCount = r.secCount;
+ secImported = r.secImported;
+ secDups = r.secDups;
}
- return null;
+ return {
+ exitCode: res.exitCode,
+ errorMsg: res.errorMsg,
+ importedKeys: importedKeys,
+ importSum: importSum,
+ importUnchanged: importUnchanged,
+ secCount: secCount,
+ secImported: secImported,
+ secDups: secDups
+ };
}
/**
@@ -430,22 +497,6 @@ class GpgMECryptoAPI extends CryptoAPI {
* Generic function to decrypt and/or verify an OpenPGP message.
*
* @param {String} encrypted The encrypted data
- * @param {Object} options Decryption options
- *
- * @return {Promise<Object>} - Return object with decryptedData and
- * status information
- *
- * Use Promise.catch to handle failed decryption.
- * retObj.errorMsg will be an error message in this case.
- */
-
- async decrypt(encrypted, options) {
- return null;
- }
-
- /**
- * Decrypt a PGP/MIME-encrypted message
- *
* @param {String} encrypted The encrypted data
* @param {Object} options Decryption options
* - logFile (the actual file)
@@ -467,15 +518,103 @@ class GpgMECryptoAPI extends CryptoAPI {
* - {String} userId: signature user Id
* - {String} keyId: signature key ID
* - {String} sigDetails: as printed by GnuPG for VALIDSIG pattern
- retStatusObj.encToDetails = encToDetails;
- *
+ * - {String} encToDetails: \n keyId1 (userId1),\n keyId1 (userId2),\n ...
+ * - {String} encryptedFileName
+ *
+ * Use Promise.catch to handle failed decryption.
+ * retObj.errorMsg will be an error message in this case.
+ */
+
+ async decrypt(encrypted, options) {
+ EnigmailLog.DEBUG(`gpgme.js: decrypt()\n`);
+
+ let result = await this.execJsonCmd({
+ op: options.verifyOnly ? "verify" : "decrypt",
+ data: btoa(encrypted),
+ base64: true
+ });
+
+ EnigmailLog.DEBUG(`gpgme.js: decrypt: result: ${JSON.stringify(result)}\n`);
+ let ret = {
+ decryptedData: "",
+ exitCode: 1,
+ statusFlags: 0,
+ errorMsg: "err",
+ blockSeparation: "",
+ userId: "",
+ keyId: "",
+ sigDetails: "",
+ encToDetails: ""
+ };
+
+ if (result.type === "plaintext") {
+ ret.errorMsg = "";
+ ret.decryptedData = result.base64 ? atob(result.data) : result.data;
+ if (options.uiFlags & EnigmailConstants.UI_PGP_MIME) {
+ ret.statusFlags |= EnigmailConstants.PGP_MIME_ENCRYPTED;
+ }
+
+ if ("dec_info" in result) {
+ if (!result.dec_info.wrong_key_usage && result.dec_info.symkey_algo.length > 0) {
+ ret.statusFlags += EnigmailConstants.DECRYPTION_OKAY;
+ if (result.dec_info.legacy_cipher_nomdc) ret.statusFlags += EnigmailConstants.MISSING_MDC;
+ }
+
+ if (result.dec_info.file_name) ret.encryptedFileName = result.dec_info.file_name;
+
+ if ("recipients" in result.dec_info) {
+ let encToArr = [];
+ for (let r of result.dec_info.recipients) {
+ // except for ID 00000000, which signals hidden keys
+ if (r.keyid.search(/^0+$/) < 0) {
+ let localKey = EnigmailKeyRing.getKeyById(`0x${r.keyid}`);
+ encToArr.push(r.keyid + (localKey ? ` (${localKey.userId})` : ""));
+ }
+ else {
+ encToArr.push(EnigmailLocale.getString("hiddenKey"));
+ }
+ }
+ ret.encToDetails = "\n " + encToArr.join(",\n ") + "\n";
+ }
+ }
+
+ if ("info" in result) {
+ await this._interpetSignatureData(result, ret);
+ }
+ ret.exitCode = 0;
+ }
+ else {
+ EnigmailLog.DEBUG(`gpgme.js: decrypt: result= ${JSON.stringify(result)}\n`);
+
+ ret.errorMsg = result.msg;
+ ret.statusFlags = EnigmailConstants.DECRYPTION_FAILED;
+ if (result.code === 117440529) {
+ ret.statusFlags |= EnigmailConstants.NO_SECKEY;
+ }
+ }
+
+ return ret;
+ }
+
+ /**
+ * Decrypt a PGP/MIME-encrypted message
+ *
+ * @param {String} encrypted The encrypted data
+ * @param {Object} options Decryption options (see decrypt() for details)
+ *
+ * @return {Promise<Object>} - Return object with decryptedData and status information
+ * (see decrypt() for details)
*
* Use Promise.catch to handle failed decryption.
* retObj.errorMsg will be an error message in this case.
*/
async decryptMime(encrypted, options) {
- return null;
+ options.noOutput = false;
+ options.verifyOnly = false;
+ options.uiFlags = EnigmailConstants.UI_PGP_MIME;
+
+ return this.decrypt(encrypted, options);
}
/**
@@ -486,14 +625,113 @@ class GpgMECryptoAPI extends CryptoAPI {
* @param {Object} options Decryption options
*
* @return {Promise<Object>} - Return object with decryptedData and
- * status information
+ * status information
*
* Use Promise.catch to handle failed decryption.
* retObj.errorMsg will be an error message in this case.
*/
async verifyMime(signedData, signature, options) {
- return null;
+ EnigmailLog.DEBUG(`gpgme.js: verifyMime()\n`);
+ let result = await this.execJsonCmd({
+ op: "verify",
+ data: btoa(signedData),
+ signature: btoa(signature),
+ base64: true
+ });
+
+ let ret = {};
+
+ if ("info" in result) {
+ await this._interpetSignatureData(result, ret);
+ }
+ else {
+ EnigmailLog.DEBUG(`gpgme.js: verifyMime: result= ${JSON.stringify(result)}\n`);
+ ret.errorMsg = result.msg;
+ ret.statusFlags = EnigmailConstants.DECRYPTION_FAILED;
+ }
+
+ return ret;
+ }
+
+ /**
+ * private function to resd/intperet the signature data returned by gpgme
+ */
+ async _interpetSignatureData(resultData, retObj) {
+ if (!retObj) return;
+ if (!("info" in resultData)) return;
+ if (resultData.info.signatures.length < 1) return;
+
+ const
+ undetermined = 0,
+ none = 1,
+ keyMissing = 2,
+ red = 3,
+ green = 4,
+ valid = 99;
+
+ let overallSigStatus = undetermined,
+ iSig = null;
+
+ // determine "best" signature
+ for (let sig of resultData.info.signatures) {
+ let s = sig.summary;
+ let sigStatus = s.valid ? valid : s.green ? green : s.red ? red : s["key-misssing"] ? keyMissing : none;
+ if (sigStatus > overallSigStatus) {
+ overallSigStatus = sigStatus;
+ iSig = sig;
+ }
+ }
+
+ // interpret the "best" signature
+ if (iSig) {
+ if (iSig.summary.red) {
+ retObj.statusFlags |= EnigmailConstants.BAD_SIGNATURE;
+ }
+ else if (iSig.summary["key-missing"]) {
+ retObj.statusFlags |= (EnigmailConstants.UNVERIFIED_SIGNATURE | EnigmailConstants.NO_PUBKEY);
+ }
+ else {
+ retObj.statusFlags |= EnigmailConstants.GOOD_SIGNATURE;
+ if (iSig.summary.valid) retObj.statusFlags |= EnigmailConstants.TRUSTED_IDENTITY;
+ if (iSig.summary.revoked) retObj.statusFlags |= EnigmailConstants.REVOKED_KEY;
+ if (iSig.summary["key-expired"]) retObj.statusFlags |= (EnigmailConstants.EXPIRED_KEY | EnigmailConstants.EXPIRED_KEY_SIGNATURE);
+ if (iSig.summary["sig-expired"]) retObj.statusFlags |= EnigmailConstants.EXPIRED_SIGNATURE;
+ }
+
+ if (iSig.fingerprint) {
+ retObj.keyId = iSig.fingerprint;
+ // use gpgme to find key, as fpr may not be available via API
+ let keys = await this.getKeys(`0x${iSig.fingerprint}`);
+ if (keys && keys.length > 0) {
+ if (retObj.statusFlags & EnigmailConstants.GOOD_SIGNATURE) {
+ retObj.errorMsg = EnigmailLocale.getString("prefGood", [keys[0].userId]);
+ }
+ else if (retObj.statusFlags & EnigmailConstants.BAD_SIGNATURE) {
+ retObj.errorMsg = EnigmailLocale.getString("prefBad", [keys[0].userId]);
+ }
+ retObj.userId = keys[0].userId;
+ }
+ else {
+ keys = null;
+ }
+
+ let sigDate = new Date(iSig.timestamp * 1000).toISOString().substr(0, 10);
+ /* VALIDSIG args are (separated by space):
+ - <fingerprint_in_hex> 4F9F89F5505AC1D1A260631CDB1187B9DD5F693B
+ - <sig_creation_date> 2020-03-21
+ - <sig-timestamp> 1584805187
+ - <expire-timestamp> 0
+ - <sig-version> 4
+ - <reserved> 0
+ - <pubkey-algo> 1
+ - <hash-algo> 8
+ - <sig-class> 00
+ - [ <primary-key-fpr> ] 4F9F89F5505AC1D1A260631CDB1187B9DD5F693B
+ */
+ retObj.sigDetails = `${iSig.fingerprint} ${sigDate} ${iSig.timestamp} ${iSig.exp_timestamp} 4 0 ${iSig.pubkey_algo_name} ${iSig.hash_algo_name} 00 ${keys ? keys[0].fpr : ""}`;
+ }
+ }
}
/**
@@ -626,7 +864,60 @@ class GpgMECryptoAPI extends CryptoAPI {
* If the feature cannot be found, undefined is returned
*/
supportsFeature(featureName) {
- return false;
+ let gpgVersion = this._gpgVersion;
+
+ 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 "supports-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 "export-result":
+ return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.1.10");
+ case "decryption-info":
+ return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.0.19");
+ case "supports-wkd":
+ return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.1.19");
+ case "export-specific-uid":
+ return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.2.9");
+ case "supports-show-only":
+ return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.1.14");
+ case "handles-huge-keys":
+ return EnigmailVersioning.greaterThanOrEqual(gpgVersion, "2.2.17");
+ case "smartcard":
+ case "uid-management":
+ case "ownertrust":
+ return true;
+ }
+
+ return undefined;
}
@@ -845,8 +1136,8 @@ class GpgMECryptoAPI extends CryptoAPI {
// TODO: use gpgme-json as a daemon running as long as the mail app.
async execJsonCmd(paramsObj) {
let jsonStr = JSON.stringify(paramsObj);
+ EnigmailLog.DEBUG(`gpgme.js: execJsonCmd(${jsonStr.substr(0, 40)})\n`);
let n = jsonStr.length;
- EnigmailLog.DEBUG(`gpgHome: ${EnigmailCore.getEnvList().join(", ")}\n`);
let result = await EnigmailExecution.execAsync(this._gpgmePath, [], convertNativeNumber(n) + jsonStr);
try {
@@ -1005,3 +1296,93 @@ function convertNativeNumber(num) {
let s = String.fromCharCode(num & 0xFF) + String.fromCharCode((num >> 8) & 0xFF) + String.fromCharCode((num >> 16) & 0xFF) + String.fromCharCode((num >> 24) & 0xFF);
return s;
}
+
+/**
+ * Parse GnuPG status output
+ *
+ * @param statusMsg
+ */
+ function parseImportResult(statusMsg) {
+ // IMPORT_RES <count> <no_user_id> <imported> 0 <unchanged>
+ // <n_uids> <n_subk> <n_sigs> <n_revoc> <sec_read> <sec_imported> <sec_dups> <not_imported>
+
+ let import_res = statusMsg.match(/^IMPORT_RES ([0-9]+) ([0-9]+) ([0-9]+) 0 ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)/m);
+
+ let keyList = [];
+ let res = {
+ errorMsg: "",
+ exitCode: -1,
+ importedKeys: [],
+ importSum: 0,
+ importUnchanged: 0
+ };
+
+ if (import_res !== null) {
+ let secCount = parseInt(import_res[9], 10); // number of secret keys found
+ let secImported = parseInt(import_res[10], 10); // number of secret keys imported
+ let secDups = parseInt(import_res[11], 10); // number of secret keys already on the keyring
+
+ if (secCount !== secImported + secDups) {
+ res.errorMsg = EnigmailLocale.getString("import.secretKeyImportError");
+ res.exitCode = 1;
+ }
+ else {
+ res.importSum = parseInt(import_res[1], 10);
+ res.importUnchanged = parseInt(import_res[4], 10);
+ res.secCount = parseInt(import_res[9], 10); // number of secret keys found
+ res.secImported = parseInt(import_res[10], 10); // number of secret keys imported
+ res.secDups = parseInt(import_res[11], 10); // number of secret keys already on the keyring
+
+ res.exitCode = 0;
+ var statusLines = statusMsg.split(/\r?\n/);
+
+ for (let j = 0; j < statusLines.length; j++) {
+ var matches = statusLines[j].match(/IMPORT_OK ([0-9]+) (\w+)/);
+ if (matches && (matches.length > 2)) {
+ if (typeof(keyList[matches[2]]) !== "undefined") {
+ keyList[matches[2]] |= Number(matches[1]);
+ }
+ else
+ keyList[matches[2]] = Number(matches[1]);
+
+ res.importedKeys.push(matches[2]);
+ EnigmailLog.DEBUG("gpgme.js: parseImportResult: imported " + matches[2] + ":" + matches[1] + "\n");
+ }
+ }
+ }
+ }
+
+ return res;
+}
+
+async function determineGpgVersion(gpgPath) {
+ const args = ["--batch", "--no-tty", "--charset", "utf-8", "--display-charset", "utf-8", "--version", "--version"];
+
+ const res = await EnigmailExecution.execAsync(gpgPath, args);
+
+ if (res.exitCode !== 0) {
+ EnigmailLog.ERROR(`gpgme.js: setAgentPath: gpg failed with exitCode ${res.exitCode} msg='${res.stdoutData} ${res.stderrData}'\n`);
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+
+ // detection for Gpg4Win wrapper
+ if (res.stdoutData.search(/^gpgwrap.*;/) === 0) {
+ const outLines = res.stdoutData.split(/[\n\r]+/);
+ const firstLine = outLines[0];
+ outLines.splice(0, 1);
+ res.stdoutData = outLines.join("\n");
+ gpgPath = firstLine.replace(/^.*;[ \t]*/, "");
+
+ EnigmailLog.CONSOLE(`gpg4win-gpgwrapper detected; GnuPG path=${gpgPath}\n\n`);
+ }
+
+ const versionParts = res.stdoutData.replace(/[\r\n].*/g, "").replace(/ *\(gpg4win.*\)/i, "").split(/ /);
+ const gpgVersion = versionParts[versionParts.length - 1];
+
+ EnigmailLog.DEBUG(`gpgme.js: detected GnuPG version '${gpgVersion}'\n`);
+
+ return {
+ gpgVersion: gpgVersion,
+ gpgPath: gpgPath
+ };
+}
diff --git a/package/openpgp.jsm b/package/openpgp.jsm
index 0d58cb6f..3f41a122 100644
--- a/package/openpgp.jsm
+++ b/package/openpgp.jsm
@@ -141,6 +141,10 @@ var EnigmailOpenPGP = {
signingAlgIdToString: function(id) {
// RFC 4880 Sec. 9.1, RFC 6637 Sec. 5 and draft-koch-eddsa-for-openpgp-03 Sec. 8
+ if (typeof id === "string") {
+ return id;
+ }
+
let EnigmailLocale;
switch (parseInt(id, 10)) {
case 1:
@@ -167,6 +171,10 @@ var EnigmailOpenPGP = {
hashAlgIdToString: function(id) {
// RFC 4880 Sec. 9.4
+ if (typeof id === "string") {
+ return id;
+ }
+
let EnigmailLocale;
switch (parseInt(id, 10)) {
case 1:
diff --git a/package/tests/gpgme-test.js b/package/tests/gpgme-test.js
index 1150a523..2cfe864a 100644
--- a/package/tests/gpgme-test.js
+++ b/package/tests/gpgme-test.js
@@ -12,49 +12,48 @@
do_load_module("file://" + do_get_cwd().path + "/testHelper.js");
/* global TestHelper: false */
-testing("cryptoAPI/gpgme.js"); /*global getGpgMEApi: false */
-
-
-test(
- function testGroups() {
- const cApi = getGpgMEApi();
-
- cApi.execJsonCmd = async function(input) {
- Assert.equal(input.op, "config_opt");
- Assert.equal(input.component, "gpg");
- Assert.equal(input.option, "group");
-
- return {
- "option": {
- "name": "group",
- "description": "set up email aliases",
- "argname": "SPEC",
- "flags": 4,
- "level": 1,
- "type": 37,
- "alt_type": 1,
- "value": [{
- "is_none": false
- }, {
- "string": "[email protected]",
- "is_none": false
- }, {
- "string": "[email protected]",
- "is_none": false
- }]
- }
- };
+testing("cryptoAPI/gpgme.js"); /*global getGpgMEApi: false, EnigmailFiles: false, EnigmailConstants: false */
+const EnigmailArmor = ChromeUtils.import("chrome://enigmail/content/modules/armor.jsm").EnigmailArmor;
+
+test(function testGroups() {
+ const gpgmeApi = getGpgMEApi();
+
+ gpgmeApi.execJsonCmd = async function(input) {
+ Assert.equal(input.op, "config_opt");
+ Assert.equal(input.component, "gpg");
+ Assert.equal(input.option, "group");
+
+ return {
+ "option": {
+ "name": "group",
+ "description": "set up email aliases",
+ "argname": "SPEC",
+ "flags": 4,
+ "level": 1,
+ "type": 37,
+ "alt_type": 1,
+ "value": [{
+ "is_none": false
+ }, {
+ "string": "[email protected]",
+ "is_none": false
+ }, {
+ "string": "[email protected]",
+ "is_none": false
+ }]
+ }
};
+ };
- let res = cApi.getGroups();
- Assert.equal(res.length, 2);
- });
+ let res = gpgmeApi.getGroups();
+ Assert.equal(res.length, 2);
+});
test(withTestGpgHome(withEnigmail(asyncTest(async (esvc, window) => {
// Test key importing and key listing
- const cApi = getGpgMEApi();
- cApi.initialize(null, esvc, null);
+ const gpgmeApi = getGpgMEApi();
+ gpgmeApi.initialize(null, esvc, null);
const pubKey = "-----BEGIN PGP PUBLIC KEY BLOCK-----" +
"\n" +
@@ -109,13 +108,13 @@ test(withTestGpgHome(withEnigmail(asyncTest(async (esvc, window) => {
"\n=I9l9" +
"\n-----END PGP PUBLIC KEY BLOCK-----";
- let impKeys = await cApi.importKeyData(pubKey, false, null);
+ let impKeys = await gpgmeApi.importKeyData(pubKey, false, null);
Assert.equal(impKeys.importSum, 1);
Assert.equal(impKeys.importedKeys.length, 1);
Assert.equal(impKeys.importedKeys[0], "65537E212DC19025AD38EDB2781617319CE311C4");
Assert.equal(impKeys.importUnchanged, 0);
- let keyList = await cApi.getKeys();
+ let keyList = await gpgmeApi.getKeys();
Assert.equal(keyList.length, 1);
Assert.equal(keyList[0].fpr, "65537E212DC19025AD38EDB2781617319CE311C4");
@@ -142,3 +141,183 @@ test(withTestGpgHome(withEnigmail(asyncTest(async (esvc, window) => {
Assert.equal(keyList[0].userIds[0].uidFpr, "0");
Assert.equal(keyList[0].userIds[0].type, "uid");
}))));
+
+
+test(withTestGpgHome(withEnigmail(asyncTest(async function testDecrypt(esvc, window) {
+ const gpgmeApi = getGpgMEApi();
+ gpgmeApi.initialize(null, esvc, null);
+
+ Assert.ok(gpgmeApi.supportsFeature("supports-gpg-agent"));
+ const encFile = do_get_file("resources/pgpMime-msg.eml", false);
+ let fileData = EnigmailFiles.readFile(encFile);
+ let pgpMsg = EnigmailArmor.splitArmoredBlocks(fileData)[0];
+
+ let result = await gpgmeApi.decrypt(pgpMsg, {});
+ Assert.equal(result.statusFlags, EnigmailConstants.DECRYPTION_FAILED | EnigmailConstants.NO_SECKEY);
+
+ const secKeyFile = do_get_file("resources/dev-strike.sec", false);
+ let r = await gpgmeApi.importKeyFromFile(secKeyFile, false, null);
+ Assert.equal(r.importSum, 1);
+
+ let keyList = await gpgmeApi.getKeys();
+ Assert.equal(keyList.length, 1);
+ Assert.equal(keyList[0].keyId, "781617319CE311C4");
+
+ result = await gpgmeApi.decrypt(pgpMsg, {});
+ Assert.ok(result.statusFlags & EnigmailConstants.DECRYPTION_OKAY);
+
+ Assert.equal(result.exitCode, 0);
+ Assert.equal(result.sigDetails, "65537E212DC19025AD38EDB2781617319CE311C4 2020-03-21 1584808355 0 4 0 RSA SHA256 00 65537E212DC19025AD38EDB2781617319CE311C4");
+ Assert.equal(result.decryptedData, "This is a test\n");
+ Assert.equal(result.keyId, "65537E212DC19025AD38EDB2781617319CE311C4");
+ Assert.equal(result.userId, "anonymous strike <[email protected]>");
+ Assert.equal(result.encryptedFileName, "message.txt");
+
+ // this is a simple plain text wrapped such that it looks like a OpenPGP message
+ const storedMsg = `-----BEGIN PGP MESSAGE-----
+
+owE7rZbEEOfZI+yRmpOTr1CeX5SToscVkpFZrABEiQolqcUlCla6mlwA
+=aAp2
+-----END PGP MESSAGE-----`;
+
+ result = await gpgmeApi.decrypt(storedMsg, {});
+ Assert.equal(result.statusFlags, 65536);
+ Assert.equal(result.exitCode, 1);
+ Assert.equal(result.sigDetails, "");
+ Assert.equal(result.decryptedData, "");
+
+ // this is a signed-only message
+ const signedMsg = `-----BEGIN PGP MESSAGE-----
+
+owEBbQKS/ZANAwAIAXgWFzGc4xHEAcsmYgBeSY3kSGVsbG8gd29ybGQuClRoaXMg
+aXMgYSB0ZXN0IDotKQqJAjMEAAEIAB0WIQRlU34hLcGQJa047bJ4FhcxnOMRxAUC
+XkmN5AAKCRB4FhcxnOMRxAicD/4jPSNH27H+G83beIyZW5UU0vzr51fHQgz+keXH
+XYDpS0j7upZ2c4m5GAkc1hpdU2FMgUeksjCYhEXsIgh5RkCcacW01dr7Jw4ZgVCl
+eCzuMXNWYANVE2dVt3EZb/E6G6by+T1gYn2SBfZSLnBrN6r552J9Ae65MgOKWOAV
+nZ9679ys3N5BLX6cctfTc0+nE3sOmuNK3/C6cn5+FVvnTZXKBXU37Zxrs/BL46Jy
+JKoxpx3yVobKw7Esef4GeAYxIEDn02mNIVXJsnr+g6YtP+gWdcuuHLyGjd/Oakuj
+gbx4JmlUjUTZvoH/c40LEMtWCJ0qUUIeqEQLGYGWrfdpimc48Eli/nxxgTNrPbB3
+KADtiAito4JNWoovqJA+F4MkV3qB5A6xLk5cCmZhw92PfwABoIEJY7WeoRSU8aws
+2G/2QTFtOgKqwYdc15OhMP/+E4EB8sIHheaRAfhsyFq1mCMEMFFTqutu3XwaFFpp
+ifNuTRIruL7nup/f5pmDD8afC2xNUe9as7L26IqRDBHuU8hwAq2t0hoo3eSLN1Ow
+Av8j88klWOEH6vUiOi5gIlCNQH5CNsgBynMfz9IC8p35ExQLVnA3KxTGL3Uxmgvh
+YjlIbvu0YmTlsQvYFW4JBIPNeNy+1r+7PCsSbnOtsSU32/4SnnQVN4khCM+gKTHD
+j8bbyQ==
+=fwVK
+-----END PGP MESSAGE-----`;
+
+ result = await gpgmeApi.decrypt(signedMsg, {
+ verifyOnly: true
+ });
+ Assert.equal(result.statusFlags, EnigmailConstants.GOOD_SIGNATURE);
+ Assert.equal(result.exitCode, 0);
+ Assert.equal(result.sigDetails, "65537E212DC19025AD38EDB2781617319CE311C4 2020-02-16 1581878756 0 4 0 RSA SHA256 00 65537E212DC19025AD38EDB2781617319CE311C4");
+ Assert.equal(result.keyId, "65537E212DC19025AD38EDB2781617319CE311C4");
+ Assert.equal(result.userId, "anonymous strike <[email protected]>");
+ Assert.equal(result.decryptedData, "Hello world.\nThis is a test :-)\n");
+
+ const clearSigned = `-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA256
+
+Hello world.
+This is a test :-)
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCAAdFiEEZVN+IS3BkCWtOO2yeBYXMZzjEcQFAl5JkHwACgkQeBYXMZzj
+EcQbbA/9Eu9DhB+3rkRIswp+tOmyjyytlL3V++Lfla1hUpfeC3VNC9iY3Y8TwuBW
+yvPzloXD1YvVoG8QZZEqGxWEli9PcLtGLUZecSmMSD5+sKm9Muj2ahxnDSM1qN0V
+yOQ+IXBtfhjDM7Zz7Xxvtv8mcRkBgQ4DAkxHTXhnBjYxo0TpffDwpmqQ0YkZCjt2
+PzzYy050uL7cJZU94P5ENHtKNUkjnKgfTWqefjCG15WSj19tfxG6EHOpEnPy8fwF
+gW+RnnGFRP41GctKDJh3tJ2ydiGr+1XaGmOx/M5PM0RT69mmlSnjSVQC7+xnZdX4
+9AL/jOyDwRK2AFOo+mCoMFTM9WhScQC5zcoDKn1uKb1WPsjH8nrWro43yJPq2w4I
+Nlqhia3bNPadPzfK3/ec++pea8byjjAuLYVe7QskNUIsfdew+m8rlhy2zDzTPSlr
+uB+5+AtxV8yXxh5ATmjXlU/RiK1YIKu7QzueGKNjKrAqLTX+YwVX0HH0v2SAQZoL
+JGWYnspuCiOYm19OkQoZ6NQYbqadqqakvHzrbjODHrnmlq9XJzEZ5cbhf6oeO4FT
+wJEP0gfdDZA1bEV78SRcjJDlfo5vuWX4W/ZlAlA9hy5OVq69DOdlcVdgj18+gy94
++SPb1y4WzJ3KARDpgsruHZ6FnAEU1clxc5pEjll/MIeYP5NW0JM=
+=WyPS
+-----END PGP SIGNATURE-----`;
+
+ result = await gpgmeApi.decrypt(clearSigned, {
+ verifyOnly: true
+ });
+ Assert.equal(result.statusFlags, EnigmailConstants.GOOD_SIGNATURE);
+ Assert.equal(result.exitCode, 0);
+ Assert.equal(result.sigDetails, "65537E212DC19025AD38EDB2781617319CE311C4 2020-02-16 1581879420 0 4 0 RSA SHA256 00 65537E212DC19025AD38EDB2781617319CE311C4");
+ Assert.equal(result.keyId, "65537E212DC19025AD38EDB2781617319CE311C4");
+ Assert.equal(result.userId, "anonymous strike <[email protected]>");
+ Assert.equal(result.decryptedData, "Hello world.\nThis is a test :-)\n");
+
+ const noMdcProtection = `-----BEGIN PGP MESSAGE-----
+
+hQIMA9U1Yju2Dp5xARAAtAZjLVld1nOStJXw18BBbvLXZU5GydCKXKn7XvY8oT6O
+tWAqrEu+rLfx0IhNKLwo5lQE5kTBoydoXBCh0wRX9P6ZCmlZKmGo0AijzhDfQ6sy
+G7hQZbf0D0nxTGbSfuGwna7h/zhluYu8Zf9mqkITkbvPNRKIcouDedcZfmc1JELu
+jK5+sNt1eDqL/Sic1SOwAxhFyVr2s397z74ZQbH/Xs0bhnQaYPOnHAh8SB4WJCjb
+boDJbisJa87VEr9xgw/25YdqGkRFACZ47QIUKJ9jQywqaDwxfcJy8t3sNkE+SBZB
+dbJLRH78923eP/0djDPsfKq3/egUf4sfumv4Rx4hhrM+ZZRMio/2NPjVA3NKmM9z
+Tl/hp/825mL4mNheH/nHW1rBb58DI0VFJ9WeMq5X3mKc7Nqllv2P5KP+JfotifTG
+A89ZArE26tuPLSDtmb7yOWAkIIM7hJnIenWT8Vf/mD76FxBwd/lz0iAjwRtGhlkr
+7ARbWRqdaOwlExllfFJ3DmmBTDdKp3gbRmGTZMA3/zEjxuy/PU7OqSn4KX44MNVb
+I881kAj7rbAprjDC5lNAsCrXibS/yoQd5z2cDOM6WOFpq+65a7Mfp9ByvxdVJ8n1
+lWv9BzKlk2Lo3vhG3zSobOPZ0b4/kgXFuuoRY85sW7S94YahAPfyNFdKDvStF4jJ
+MBv+amSxwYvdlpRMJcBa48saFHionNWfjGlulsbLAnBGQNi4qYye5ZzNi+A/ERnz
+TA==
+=6sUw
+-----END PGP MESSAGE-----`;
+
+ result = await gpgmeApi.decrypt(noMdcProtection, {});
+ Assert.equal(result.statusFlags, EnigmailConstants.DECRYPTION_FAILED);
+ Assert.equal(result.exitCode, 1);
+ Assert.equal(result.decryptedData, "");
+
+ const badMdc = `-----BEGIN PGP MESSAGE-----
+
+hQIMA9U1Yju2Dp5xARAAnIdEzfwCFOHmBpd5FLoldI0sh6b1J5z6h3SlWyb6D07T
+ObvOt3REynBpF1bEb6XfAhqUx5FpKwcoeTlGuxUSMlurwRmDpUOr913+1wxnY0q5
+J1fclLIsFiDCcWYdwYsDTZoAmHbaoGXT+v+/we+hPpm81z9YxsxuwdcXesRxqImq
+Da3IKG+fW8HvD7BmcOZPYoyfZnMUSld9lQ5cZ7DD/BQblEDZQX2rVYSNIoi9YPIa
+puGr8PU1+CJeyJgk+RnMD7vItk3+BqyS7ybbzOFfhyQcBTlVgURMA88Pk42EUleP
+oT3TwXvvB410l/FQAb21QQmmXjc+RObuEaAZoJmXmv+2e+LBZTniTYBK16WaHYNU
+/N+5Hknf2193WAVbxea3Tw8tCpHj8IjbTde+zgMwmoRoLvB3RSpyL6NyBPYTxCXW
+b0aLsu5J74v5UD3SjzEu5kXaQJCvaBy7n6oqccyMe66ZVDHqaS7PYAyjbt3O2Fni
+LxmaM9NkpH7lQAiErRwrN+8yD/ypY5+532naB+QcN7iYsV5o6p78SvGNchvF9x/8
+H0lcXBzL3PfEQBeMy6bWkXjE060lxz7hWi6dA24zHXDImJp9Se8DEg3LXf2h4Gpa
+r45rGHjoRO/oNb6Ccw/dt1dqEISs1LooPTbtBAcy4F33PDvM3LuKZByZIFJTb8PS
+RwFT30I1c1xcp7D4Jgv3cWCks39z0jxfmSPLYyPjI1tT7Zi9m9vZN0ZfVC/XvJiT
+fsTKNmsBfsUHg/qzu+yD0e4bTuEKVsDcCg==
+=WPhs
+-----END PGP MESSAGE-----`;
+
+ result = await gpgmeApi.decrypt(badMdc, {});
+ Assert.equal(result.statusFlags, EnigmailConstants.DECRYPTION_FAILED);
+ Assert.equal(result.exitCode, 1);
+ Assert.equal(result.decryptedData, "");
+
+ const unknownPubkey = `-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+Hello world.
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3
+
+iD8DBQE+yUcu4mZch0nhy8kRAuh/AKDM1Xc49BKVfJIFg/btWGfbF/pgcwCgw0Zk
+3bVpfUMtjVsz6ChXUG35fMY=
+=n4PF
+-----END PGP SIGNATURE-----`;
+
+ result = await gpgmeApi.decrypt(unknownPubkey, {
+ verifyOnly: true
+ });
+ Assert.equal(result.statusFlags, EnigmailConstants.UNVERIFIED_SIGNATURE | EnigmailConstants.NO_PUBKEY);
+ Assert.equal(result.exitCode, 0);
+ Assert.equal(result.decryptedData, "Hello world.\n");
+
+
+ const attachmentFile = do_get_file("resources/attachment.txt", false);
+ fileData = EnigmailFiles.readFile(attachmentFile);
+ const attachmentSig = do_get_file("resources/attachment.txt.asc", false);
+ let sigData = EnigmailFiles.readFile(attachmentSig);
+ result = await gpgmeApi.verifyMime(fileData, sigData);
+ Assert.equal(result.statusFlags, EnigmailConstants.GOOD_SIGNATURE);
+}))));
diff --git a/ui/locale/en-US/enigmail.properties b/ui/locale/en-US/enigmail.properties
index 510f62a1..95b12e6a 100644
--- a/ui/locale/en-US/enigmail.properties
+++ b/ui/locale/en-US/enigmail.properties
@@ -341,7 +341,7 @@ gpgNotInPath=Unable to locate GnuPG executable in the PATH.\nMake sure you have
enigmailNotAvailable=Enigmail core Service not available
prefGood=Good signature from %S
-prefBad=BAD signature from %S
+prefBad=Unverified signature from %S
failCancel=Error - Key receive cancelled by user
failKeyExtract=Error - key extraction command failed