diff options
author | Patrick Brunschwig <[email protected]> | 2021-05-16 12:10:17 +0200 |
---|---|---|
committer | Patrick Brunschwig <[email protected]> | 2021-05-16 12:10:17 +0200 |
commit | 13b20185b469dd00f8d28e089360cb722d53e5dd (patch) | |
tree | 449328725756bbc6a7559b9e01e7919aebae3bb9 | |
parent | 960cd78b5b071b65bd58d2df709eda3847a3d9e5 (diff) | |
download | enigmail-13b20185b469dd00f8d28e089360cb722d53e5dd.tar.gz enigmail-13b20185b469dd00f8d28e089360cb722d53e5dd.tar.bz2 enigmail-13b20185b469dd00f8d28e089360cb722d53e5dd.zip |
implemented exporting of keys in gpgme.js
-rw-r--r-- | package/cryptoAPI/gnupg.js | 2 | ||||
-rw-r--r-- | package/cryptoAPI/gpgme.js | 149 | ||||
-rw-r--r-- | package/cryptoAPI/interface.js | 19 | ||||
-rw-r--r-- | package/cryptoAPI/openpgp-js.js | 5 | ||||
-rw-r--r-- | package/tests/gpgme-test.js | 61 |
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; +} |