aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPatrick Brunschwig <[email protected]>2021-05-16 12:10:17 +0200
committerPatrick Brunschwig <[email protected]>2021-05-16 12:10:17 +0200
commit13b20185b469dd00f8d28e089360cb722d53e5dd (patch)
tree449328725756bbc6a7559b9e01e7919aebae3bb9
parent960cd78b5b071b65bd58d2df709eda3847a3d9e5 (diff)
downloadenigmail-13b20185b469dd00f8d28e089360cb722d53e5dd.tar.gz
enigmail-13b20185b469dd00f8d28e089360cb722d53e5dd.tar.bz2
enigmail-13b20185b469dd00f8d28e089360cb722d53e5dd.zip
implemented exporting of keys in gpgme.js
-rw-r--r--package/cryptoAPI/gnupg.js2
-rw-r--r--package/cryptoAPI/gpgme.js149
-rw-r--r--package/cryptoAPI/interface.js19
-rw-r--r--package/cryptoAPI/openpgp-js.js5
-rw-r--r--package/tests/gpgme-test.js61
5 files changed, 175 insertions, 61 deletions
diff --git a/package/cryptoAPI/gnupg.js b/package/cryptoAPI/gnupg.js
index a47211c8..f00f37bf 100644
--- a/package/cryptoAPI/gnupg.js
+++ b/package/cryptoAPI/gnupg.js
@@ -166,7 +166,7 @@ class GnuPGCryptoAPI extends CryptoAPI {
* @param {String} fpr: a single FPR
* @param {String} email: [optional] the email address of the desired user ID.
* If the desired user ID cannot be found or is not valid, use the primary UID instead
- * @param {Array<Number>} subkeyDates: [optional] remove subkeys with sepcific creation Dates
+ * @param {Array<Number>} subkeyDates: [optional] remove subkeys that don't match sepcific creation Dates
*
* @return {Promise<Object>}:
* - exitCode (0 = success)
diff --git a/package/cryptoAPI/gpgme.js b/package/cryptoAPI/gpgme.js
index add5e547..904778ff 100644
--- a/package/cryptoAPI/gpgme.js
+++ b/package/cryptoAPI/gpgme.js
@@ -33,6 +33,7 @@ const EnigmailVersioning = ChromeUtils.import("chrome://enigmail/content/modules
//const pgpjs_keys = ChromeUtils.import("chrome://enigmail/content/modules/cryptoAPI/pgpjs-keys.jsm").pgpjs_keys;
const nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+const MINIMUM_GPG_VERSION = "2.2.10";
const VALIDITY_SYMBOL = {
ultimate: "u",
@@ -91,6 +92,13 @@ class GpgMECryptoAPI extends CryptoAPI {
if (!this._gpgPath) throw "GnuPG not available";
let r = this.sync(determineGpgVersion(this._gpgPath));
+
+ if (EnigmailVersioning.lessThan(r.gpgVersion, MINIMUM_GPG_VERSION)) {
+ EnigmailLog.ERROR(`gpgme.js: found GnuPG version ${r.gpgVersion} does not meet minimum required version ${MINIMUM_GPG_VERSION}\n`);
+
+ throw new Error("GnuPG does not fulfil minimum required version");
+ }
+
this._gpgPath = r.gpgPath;
this._gpgVersion = r.gpgVersion;
}
@@ -134,42 +142,6 @@ class GpgMECryptoAPI extends CryptoAPI {
}
/**
- * Export the minimum key for the public key object:
- * public key, user ID, newest encryption subkey
- *
- * @param {String} fpr : a single FPR
- * @param {String} email: [optional] the email address of the desired user ID.
- * If the desired user ID cannot be found or is not valid, use the primary UID instead
- *
- * @return {Promise<Object>}:
- * - exitCode (0 = success)
- * - errorMsg (if exitCode != 0)
- * - keyData: BASE64-encded string of key data
- */
- async getMinimalPubKey(fpr, email) {
- return {
- exitCode: -1,
- errorMsg: "",
- keyData: ""
- };
- }
-
- /**
- * Get a minimal stripped key containing only:
- * - The public key
- * - the primary UID + its self-signature
- * - the newest valild encryption key + its signature packet
- *
- * @param {String} armoredKey: Key data (in OpenPGP armored format)
- *
- * @return {Promise<Uint8Array, or null>}
- */
-
- async getStrippedKey(armoredKey) {
- return null;
- }
-
- /**
* Get the list of all konwn keys (including their secret keys)
* @param {Array of String} onlyKeys: [optional] only load data for specified key IDs
*
@@ -452,6 +424,24 @@ class GpgMECryptoAPI extends CryptoAPI {
}
/**
+ * Export the minimum key for the public key object:
+ * public key, user ID, newest encryption subkey
+ *
+ * @param {String} fpr : a single FPR
+ * @param {String} email: [optional] the email address of the desired user ID.
+ * If the desired user ID cannot be found or is not valid, use the primary UID instead
+ * @param {Array<Number>} subkeyDates: [optional] remove subkeys with sepcific creation Dates
+ *
+ * @return {Promise<Object>}:
+ * - exitCode (0 = success)
+ * - errorMsg (if exitCode != 0)
+ * - keyData: BASE64-encded string of key data
+ */
+ async getMinimalPubKey(fpr, email, subkeyDates) {
+ return exportKeyFromGnuPG(this._gpgPath, fpr, false, false, true, email, subkeyDates);
+ }
+
+ /**
* Export secret key(s) as ASCII armored data
*
* @param {String} keyId Specification by fingerprint or keyID, separate mutliple keys with spaces
@@ -463,15 +453,14 @@ class GpgMECryptoAPI extends CryptoAPI {
* - {String} errorMsg: error message in case exitCode !== 0
*/
- async extractSecretKey(keyId, minimalKey) {
- return null;
+ async extractSecretKey(keyId, minimalKey = false) {
+ return exportKeyFromGnuPG(this._gpgPath, keyId, true, true, minimalKey);
}
/**
* Export public key(s) as ASCII armored data
*
* @param {String} keyId Specification by fingerprint or keyID, separate mutliple keys with spaces
- * @param {Boolean} minimalKey if true, reduce key to minimum required
*
* @return {Object}:
* - {Number} exitCode: result code (0: OK)
@@ -480,7 +469,7 @@ class GpgMECryptoAPI extends CryptoAPI {
*/
async extractPublicKey(keyId) {
- return null;
+ return exportKeyFromGnuPG(this._gpgPath, keyId, false, true, false);
}
/**
@@ -1455,3 +1444,83 @@ function gpgUnescape(str) {
}
return str;
}
+
+
+/**
+ * Export Keys from GnuPG
+ * @param {Object<nsIFile>} gpgPath: path to gpg executable
+ * @param {String} keyId: list of keys separated by space
+ * @param {Boolean} secretKey: if true, export secret key; if false export public key
+ * @param {Boolean} minimalKey: if true, export a minimal key
+ * @param {Boolean} asciiArmor: if true, export as ASCII armored data, otherwise as BASE64 binary data
+ * @param {String} email: [optional] if set, only consider UIDs that match the given email address
+ * @param {Array<Number>} subkeyDates: [optional] remove subkeys that don't match sepcific creation Dates
+ *
+ * @returns {Object}:
+ * - {Number} exitCode: result code (0: OK)
+ * - {String} keyData: ASCII armored key data material
+ * - {String} errorMsg: error message in case exitCode !== 0
+ */
+async function exportKeyFromGnuPG(gpgPath, keyId, secretKey = false, asciiArmor = true, minimalKey = false, email, subkeyDates) {
+ EnigmailLog.DEBUG(`gpgme.js: exportKeyFromGnuPG(${gpgPath}, ${keyId}, ${secretKey}, ${minimalKey})\n`);
+
+ let args = ["--no-verbose", "--status-fd", "2", "--batch", "--yes"],
+ exitCode = -1,
+ errorMsg = "";
+
+ if (asciiArmor) {
+ args.push("-a");
+ }
+
+ if (minimalKey) {
+ args.push("--export-options");
+ args.push("export-minimal,no-export-attributes");
+ args.push("--export-filter");
+ args.push("keep-uid=" + (email ? "mbox=" + email : "primary=1"));
+
+ // filter for specific subkeys
+ let dropSubkeyFilter = "usage!~e && usage!~s";
+
+ if (subkeyDates && subkeyDates.length > 0) {
+ dropSubkeyFilter = subkeyDates.map(x => `key_created!=${x}`).join(" && ");
+ }
+ args = args.concat([
+ "--export-filter", "drop-subkey=" + dropSubkeyFilter
+ ]);
+ }
+
+ if (secretKey) {
+ args.push("--export-secret-keys");
+ }
+ else {
+ args.push("--export");
+ }
+
+ if (keyId) {
+ args = args.concat(keyId.split(/[ ,\t]+/));
+ }
+
+ let res = await EnigmailExecution.execAsync(gpgPath, args, "");
+ exitCode = res.exitCode;
+
+ if (res.stdoutData) {
+ exitCode = 0;
+ }
+
+ if (exitCode !== 0) {
+ if (res.errorMsg) {
+ errorMsg = EnigmailFiles.formatCmdLine(gpgPath, args);
+ errorMsg += "\n" + res.errorMsg;
+ }
+ }
+
+ if (!asciiArmor) {
+ res.stdoutData = btoa(res.stdoutData);
+ }
+
+ return {
+ keyData: res.stdoutData,
+ exitCode: exitCode,
+ errorMsg: errorMsg
+ };
+}
diff --git a/package/cryptoAPI/interface.js b/package/cryptoAPI/interface.js
index a0664341..a4bfbb43 100644
--- a/package/cryptoAPI/interface.js
+++ b/package/cryptoAPI/interface.js
@@ -103,13 +103,14 @@ class CryptoAPI {
* @param {String} fpr : a single FPR
* @param {String} email: [optional] the email address of the desired user ID.
* If the desired user ID cannot be found or is not valid, use the primary UID instead
+ * @param {Array<Number>} subkeyDates: [optional] remove subkeys that don't match sepcific creation Dates
*
* @return {Promise<Object>}:
* - exitCode (0 = success)
* - errorMsg (if exitCode != 0)
* - keyData: BASE64-encded string of key data
*/
- async getMinimalPubKey(fpr, email) {
+ async getMinimalPubKey(fpr, email, subkeyDates) {
return {
exitCode: -1,
errorMsg: "",
@@ -118,21 +119,6 @@ class CryptoAPI {
}
/**
- * Get a minimal stripped key containing only:
- * - The public key
- * - the primary UID + its self-signature
- * - the newest valild encryption key + its signature packet
- *
- * @param {String} armoredKey: Key data (in OpenPGP armored format)
- *
- * @return {Promise<Uint8Array, or null>}
- */
-
- async getStrippedKey(armoredKey) {
- return null;
- }
-
- /**
* Get the list of all konwn keys (including their secret keys)
* @param {Array of String} onlyKeys: [optional] only load data for specified key IDs
*
@@ -233,7 +219,6 @@ class CryptoAPI {
* Export public key(s) as ASCII armored data
*
* @param {String} keyId Specification by fingerprint or keyID, separate mutliple keys with spaces
- * @param {Boolean} minimalKey if true, reduce key to minimum required
*
* @return {Object}:
* - {Number} exitCode: result code (0: OK)
diff --git a/package/cryptoAPI/openpgp-js.js b/package/cryptoAPI/openpgp-js.js
index b57bde69..7e6c2ed9 100644
--- a/package/cryptoAPI/openpgp-js.js
+++ b/package/cryptoAPI/openpgp-js.js
@@ -163,7 +163,6 @@ class OpenPGPjsCryptoAPI extends CryptoAPI {
* Export public key(s) as ASCII armored data
*
* @param {String} fpr Fingerprint(s), separate mutliple keys with spaces
- * @param {Boolean} minimalKey if true, reduce key(s) to minimum required
*
* @return {Object}:
* - {Number} exitCode: result code (0: OK)
@@ -227,13 +226,15 @@ class OpenPGPjsCryptoAPI extends CryptoAPI {
* @param {String} fpr : a single FPR
* @param {String} email: [optional] the email address of the desired user ID.
* If the desired user ID cannot be found or is not valid, use the primary UID instead
+ * @param {Array<Number>} subkeyDates: [optional] remove subkeys that don't match sepcific creation Dates
*
* @return {Promise<Object>}:
* - exitCode (0 = success)
* - errorMsg (if exitCode != 0)
* - keyData: BASE64-encded string of key data
*/
- async getMinimalPubKey(fpr, email) {
+ async getMinimalPubKey(fpr, email, subkeyDates) {
+ // subkeyDates are not needed/supported for OpenPGP.js
return pgpjs_keyStore.readMinimalPubKey(fpr, email);
}
diff --git a/package/tests/gpgme-test.js b/package/tests/gpgme-test.js
index b99cfffc..e0d5472a 100644
--- a/package/tests/gpgme-test.js
+++ b/package/tests/gpgme-test.js
@@ -12,7 +12,7 @@
do_load_module("file://" + do_get_cwd().path + "/testHelper.js");
/* global TestHelper: false */
-testing("cryptoAPI/gpgme.js"); /*global getGpgMEApi: false, EnigmailFiles: false, EnigmailConstants: false */
+testing("cryptoAPI/gpgme.js"); /*global getGpgMEApi: false, EnigmailFiles: false, EnigmailConstants: false, EnigmailExecution: false */
const EnigmailArmor = ChromeUtils.import("chrome://enigmail/content/modules/armor.jsm").EnigmailArmor;
test(function testGroups() {
@@ -339,3 +339,62 @@ test(withTestGpgHome(withEnigmail(asyncTest(async function testDeleteKey(esvc, w
r = await gpgmeApi.deleteKeys(["0x65537E212DC19025AD38EDB2781617319CE311C4"], true);
Assert.equal(r.exitCode, 0, "deletion of secret key");
}))));
+
+
+test(withTestGpgHome(withEnigmail(asyncTest(async function testExportKey(esvc, window) {
+ const gpgmeApi = getGpgMEApi();
+ gpgmeApi.initialize(null, esvc, null);
+
+ const secKeyFile = do_get_file("resources/multi-uid.sec", false);
+ let r = await gpgmeApi.importKeyFromFile(secKeyFile, false, null);
+ Assert.equal(r.importSum, 1);
+
+ let keyData = (await gpgmeApi.extractPublicKey("0xADC49530CB6B132412D856107F1568CB8997F7BA")).keyData;
+ Assert.equal(keyData.substr(0, 36), "-----BEGIN PGP PUBLIC KEY BLOCK-----");
+
+ r = await testGpgKeyData(gpgmeApi, keyData);
+ Assert.equal(r.split(/[\r\n]+/).length, 18);
+ Assert.ok(r.includes("uid:-::::1536940615::680F6B5FD4CA9FDAB29407FAFBFA15339AB8A5A6::Unit Test <[email protected]>"));
+ Assert.ok(r.includes("uid:-::::1536939111::A692D45B4B173E4E7E05BA8E17A2D7EDBD85DB76::[email protected]"));
+ Assert.ok(r.includes("uid:r::::::D707F090C6B85B86AE9A5168732CAEF3CA7D27FA::Error <[email protected]>"));
+ Assert.ok(r.includes("uat:-::::1536939071::A3549B6F0E55083DCC5B5E5890E2CD2A4D4143EB"));
+ Assert.ok(r.includes("sub:-:3072:1:BDB2B2394A9DDBFF:1536938954::::::e"));
+ Assert.ok(r.includes("sub:r:3072:1:8B20932A70419EA6:1536939152::::::s"));
+ Assert.ok(r.includes("sub:r:3072:1:0B24E9A73D088034:1536939191::::::e"));
+ Assert.ok(r.includes("sub:-:3072:1:2462FC183074D416:1537000928::::::s"));
+ Assert.ok(r.includes("sub:-:3072:1:BF99A9839B499171:1537000944::::::e"));
+
+ keyData = (await gpgmeApi.extractSecretKey("0xADC49530CB6B132412D856107F1568CB8997F7BA", true)).keyData;
+ Assert.equal(keyData.substr(0, 37), "-----BEGIN PGP PRIVATE KEY BLOCK-----");
+
+ r = await testGpgKeyData(gpgmeApi, keyData);
+ Assert.equal(r.split(/[\r\n]+/).length, 20);
+
+ Assert.ok(r.includes("sec:-:3072:1:7F1568CB8997F7BA:1536938954:::-:::scESC"));
+ Assert.ok(r.includes("uid:-::::1536940615::680F6B5FD4CA9FDAB29407FAFBFA15339AB8A5A6::Unit Test <[email protected]>"));
+ Assert.ok(r.includes("uid:r::::::D707F090C6B85B86AE9A5168732CAEF3CA7D27FA::Error <[email protected]>") === false);
+ Assert.ok(r.includes("uat:-::::1536939071::A3549B6F0E55083DCC5B5E5890E2CD2A4D4143EB") === false);
+ Assert.ok(r.includes("ssb:-:3072:1:BDB2B2394A9DDBFF:1536938954::::::e"));
+ Assert.ok(r.includes("ssb:r:3072:1:8B20932A70419EA6:1536939152::::::s"));
+ Assert.ok(r.includes("ssb:r:3072:1:0B24E9A73D088034:1536939191::::::e"));
+ Assert.ok(r.includes("ssb:-:3072:1:2462FC183074D416:1537000928::::::s"));
+ Assert.ok(r.includes("ssb:-:3072:1:BF99A9839B499171:1537000944::::::e"));
+
+ keyData = (await gpgmeApi.getMinimalPubKey("0xADC49530CB6B132412D856107F1568CB8997F7BA", "[email protected]", [1536939152, 1536939191])).keyData;
+ Assert.equal(keyData.substr(0, 36), "mQGNBFub08oBDACmb04i4u8xUV1ADbnbN5l8");
+
+ r = await testGpgKeyData(gpgmeApi, atob(keyData));
+ Assert.equal(r.split(/[\r\n]+/).length, 8);
+ Assert.ok(r.includes("uid:-::::1536939111::A692D45B4B173E4E7E05BA8E17A2D7EDBD85DB76::[email protected]"));
+ Assert.ok(r.includes("uid:-::::1536940615::680F6B5FD4CA9FDAB29407FAFBFA15339AB8A5A6::Unit Test <[email protected]>") === false);
+ Assert.ok(r.includes("sub:r:3072:1:8B20932A70419EA6:1536939152::::::s"));
+ Assert.ok(r.includes("sub:-:3072:1:2462FC183074D416:1537000928::::::s") === false);
+}))));
+
+
+async function testGpgKeyData(gpgmeApi, keyData) {
+ const importArgs = ["--no-default-keyring", "--no-tty", "--batch", "--no-verbose", "--with-fingerprint", "--with-colons", "--import-options", "import-show", "--dry-run", "--import"];
+ let r = await EnigmailExecution.execAsync(gpgmeApi._gpgPath, importArgs, keyData);
+
+ return r.stdoutData;
+}