summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/modules
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules')
-rw-r--r--comm/mail/extensions/openpgp/content/modules/CollectedKeysDB.jsm355
-rw-r--r--comm/mail/extensions/openpgp/content/modules/GPGME.jsm338
-rw-r--r--comm/mail/extensions/openpgp/content/modules/GPGMELib.jsm584
-rw-r--r--comm/mail/extensions/openpgp/content/modules/OpenPGPAlias.jsm173
-rw-r--r--comm/mail/extensions/openpgp/content/modules/RNP.jsm4787
-rw-r--r--comm/mail/extensions/openpgp/content/modules/RNPLib.jsm2109
-rw-r--r--comm/mail/extensions/openpgp/content/modules/armor.jsm367
-rw-r--r--comm/mail/extensions/openpgp/content/modules/constants.jsm183
-rw-r--r--comm/mail/extensions/openpgp/content/modules/core.jsm189
-rw-r--r--comm/mail/extensions/openpgp/content/modules/cryptoAPI.jsm32
-rw-r--r--comm/mail/extensions/openpgp/content/modules/cryptoAPI/GnuPGCryptoAPI.jsm238
-rw-r--r--comm/mail/extensions/openpgp/content/modules/cryptoAPI/RNPCryptoAPI.jsm282
-rw-r--r--comm/mail/extensions/openpgp/content/modules/cryptoAPI/interface.js288
-rw-r--r--comm/mail/extensions/openpgp/content/modules/data.jsm156
-rw-r--r--comm/mail/extensions/openpgp/content/modules/decryption.jsm639
-rw-r--r--comm/mail/extensions/openpgp/content/modules/dialog.jsm481
-rw-r--r--comm/mail/extensions/openpgp/content/modules/encryption.jsm564
-rw-r--r--comm/mail/extensions/openpgp/content/modules/filters.jsm598
-rw-r--r--comm/mail/extensions/openpgp/content/modules/filtersWrapper.jsm186
-rw-r--r--comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm433
-rw-r--r--comm/mail/extensions/openpgp/content/modules/funcs.jsm561
-rw-r--r--comm/mail/extensions/openpgp/content/modules/key.jsm285
-rw-r--r--comm/mail/extensions/openpgp/content/modules/keyLookupHelper.jsm380
-rw-r--r--comm/mail/extensions/openpgp/content/modules/keyObj.jsm679
-rw-r--r--comm/mail/extensions/openpgp/content/modules/keyRing.jsm2202
-rw-r--r--comm/mail/extensions/openpgp/content/modules/keyserver.jsm1549
-rw-r--r--comm/mail/extensions/openpgp/content/modules/keyserverUris.jsm43
-rw-r--r--comm/mail/extensions/openpgp/content/modules/log.jsm151
-rw-r--r--comm/mail/extensions/openpgp/content/modules/masterpass.jsm332
-rw-r--r--comm/mail/extensions/openpgp/content/modules/mime.jsm571
-rw-r--r--comm/mail/extensions/openpgp/content/modules/mimeDecrypt.jsm933
-rw-r--r--comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm760
-rw-r--r--comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm716
-rw-r--r--comm/mail/extensions/openpgp/content/modules/msgRead.jsm289
-rw-r--r--comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm1338
-rw-r--r--comm/mail/extensions/openpgp/content/modules/pgpmimeHandler.jsm299
-rw-r--r--comm/mail/extensions/openpgp/content/modules/singletons.jsm54
-rw-r--r--comm/mail/extensions/openpgp/content/modules/sqliteDb.jsm477
-rw-r--r--comm/mail/extensions/openpgp/content/modules/streams.jsm155
-rw-r--r--comm/mail/extensions/openpgp/content/modules/trust.jsm94
-rw-r--r--comm/mail/extensions/openpgp/content/modules/uris.jsm124
-rw-r--r--comm/mail/extensions/openpgp/content/modules/webKey.jsm293
-rw-r--r--comm/mail/extensions/openpgp/content/modules/windows.jsm518
-rw-r--r--comm/mail/extensions/openpgp/content/modules/wkdLookup.jsm363
-rw-r--r--comm/mail/extensions/openpgp/content/modules/wksMimeHandler.jsm262
-rw-r--r--comm/mail/extensions/openpgp/content/modules/zbase32.jsm108
46 files changed, 26518 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/CollectedKeysDB.jsm b/comm/mail/extensions/openpgp/content/modules/CollectedKeysDB.jsm
new file mode 100644
index 0000000000..0c4581e9f1
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/CollectedKeysDB.jsm
@@ -0,0 +1,355 @@
+/* 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/. */
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ RNP: "chrome://openpgp/content/modules/RNP.jsm",
+});
+
+/**
+ * Database of collected OpenPGP keys.
+ */
+const EXPORTED_SYMBOLS = ["CollectedKeysDB"];
+
+var log = console.createInstance({
+ prefix: "openpgp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "openpgp.loglevel",
+});
+
+/**
+ * Class that handles storage of OpenPGP keys that were found through various
+ * sources.
+ */
+class CollectedKeysDB {
+ /**
+ * @param {IDBDatabase} database
+ */
+ constructor(db) {
+ this.db = db;
+ this.db.onclose = () => {
+ log.debug("DB closed!");
+ };
+ this.db.onabort = () => {
+ log.debug("DB operation aborted!");
+ };
+ }
+
+ /**
+ * Get a database instance.
+ *
+ * @returns {CollectedKeysDB} a instance.
+ */
+ static async getInstance() {
+ return new Promise((resolve, reject) => {
+ const VERSION = 1;
+ let DBOpenRequest = indexedDB.open("openpgp_cache", VERSION);
+ DBOpenRequest.onupgradeneeded = event => {
+ let db = event.target.result;
+ if (event.oldVersion < 1) {
+ // Create an objectStore for this database
+ let objectStore = db.createObjectStore("seen_keys", {
+ keyPath: "fingerprint",
+ });
+ objectStore.createIndex("emails", "emails", {
+ unique: false,
+ multiEntry: true,
+ });
+ objectStore.createIndex("created", "created");
+ objectStore.createIndex("expires", "expires");
+ objectStore.createIndex("timestamp", "timestamp");
+ }
+ log.debug(`Database ready at version ${VERSION}`);
+ };
+ DBOpenRequest.onerror = event => {
+ log.debug(`Error loading database: ${DBOpenRequest.error.message}`);
+ reject(DBOpenRequest.error);
+ };
+ DBOpenRequest.onsuccess = event => {
+ let keyDb = new CollectedKeysDB(DBOpenRequest.result);
+ resolve(keyDb);
+ };
+ });
+ }
+
+ /**
+ * @typedef {object} CollectedKey - Key details.
+ * @property {string[]} emails - Lowercase email addresses associated with this key
+ * @property {string} fingerprint - Key fingerprint.
+ * @property {string[]} userIds - UserIds for this key.
+ * @property {string} id - Key ID with a 0x prefix.
+ * @property {string} pubKey - The public key data.
+ * @property {Date} created - Key creation date.
+ * @property {Date} expires - Key expiry date.
+ * @property {Date} timestamp - Timestamp of last time this key was saved/updated.
+ * @property {object[]} sources - List of sources we saw this key.
+ * @property {string} sources.uri - URI of the source.
+ * @property {string} sources.type - Type of source (e.g. attachment, wkd, keyserver)
+ * @property {string} sources.description - Description of the source, if any. E.g. the attachment name.
+ */
+
+ /**
+ * Store a key.
+ *
+ * @param {CollectedKey} key - the key to store.
+ */
+ async storeKey(key) {
+ if (key.fingerprint?.length != 40) {
+ throw new Error(`Invalid fingerprint: ${key.fingerprint}`);
+ }
+ return new Promise((resolve, reject) => {
+ let transaction = this.db.transaction(["seen_keys"], "readwrite");
+ transaction.oncomplete = () => {
+ log.debug(`Stored key 0x${key.id} for ${key.emails}`);
+ let window = Services.wm.getMostRecentWindow("mail:3pane");
+ window.dispatchEvent(
+ new CustomEvent("keycollected", { detail: { key } })
+ );
+ resolve();
+ };
+ transaction.onerror = () => {
+ reject(transaction.error);
+ };
+ // log.debug(`Storing key: ${JSON.stringify(key, null, 2)}`);
+ key.timestamp = new Date();
+ transaction.objectStore("seen_keys").put(key);
+ transaction.commit();
+ });
+ }
+
+ /**
+ * Find key for fingerprint.
+ *
+ * @param {string} fingerprint - Fingerprint to find key for.
+ * @returns {CollectedKey} the key found, or null.
+ */
+ async findKeyForFingerprint(fingerprint) {
+ if (fingerprint?.length != 40) {
+ throw new Error(`Invalid fingerprint: ${fingerprint}`);
+ }
+ return new Promise((resolve, reject) => {
+ let request = this.db
+ .transaction("seen_keys")
+ .objectStore("seen_keys")
+ .get(fingerprint);
+
+ request.onsuccess = event => {
+ // If we didn't find anything, result is undefined. If so return null
+ // so that we make it clear we found "something", but it was nothing.
+ resolve(request.result || null);
+ };
+ request.onerror = event => {
+ log.debug(`Find key failed: ${request.error.message}`);
+ reject(request.error);
+ };
+ });
+ }
+
+ /**
+ * Find keys for email.
+ *
+ * @param {string} email - Email to find keys for.
+ * @returns {CollectedKey[]} the keys found.
+ */
+ async findKeysForEmail(email) {
+ email = email.toLowerCase();
+ return new Promise((resolve, reject) => {
+ let keys = [];
+ let index = this.db
+ .transaction("seen_keys")
+ .objectStore("seen_keys")
+ .index("emails");
+ index.openCursor(IDBKeyRange.only(email)).onsuccess = function (event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ // All results done.
+ resolve(keys);
+ return;
+ }
+ keys.push(cursor.value);
+ cursor.continue();
+ };
+ });
+ }
+
+ /**
+ * Find existing key in the database, and use RNP to merge such a key
+ * with the passed in keyBlock.
+ * Merging will always use the email addresses and user IDs of the merged key,
+ * which causes old revoked entries to be removed.
+ * We keep the list of previously seen source locations.
+ *
+ * @param {EnigmailKeyOb} - key object
+ * @param {string} keyBlock - public key to merge
+ * @param {object} source - source of the information
+ * @param {string} source.type - source type
+ * @param {string} source.uri - source uri
+ * @param {string?} source.description - source description
+ * @returns {CollectedKey} merged key - not yet stored in the database
+ */
+ async mergeExisting(keyobj, keyBlock, source) {
+ let fpr = keyobj.fpr;
+ let existing = await this.findKeyForFingerprint(fpr);
+ let newKey;
+ let pubKey;
+ if (existing) {
+ pubKey = await lazy.RNP.mergePublicKeyBlocks(
+ fpr,
+ existing.pubKey,
+ keyBlock
+ );
+ // Don't use EnigmailKey.getKeyListFromKeyBlock interactive.
+ // Use low level API for obtaining key list, we don't want to
+ // poison the app key cache.
+ // We also don't want to obtain any additional revocation certs.
+ let keys = await lazy.RNP.getKeyListFromKeyBlockImpl(
+ pubKey,
+ true,
+ false,
+ false,
+ false
+ );
+ if (!keys || !keys.length) {
+ throw new Error("Error getting keys from block");
+ }
+ if (keys.length != 1) {
+ throw new Error(`Got ${keys.length} keys for fpr=${fpr}`);
+ }
+ newKey = keys[0];
+ } else {
+ pubKey = keyBlock;
+ newKey = keyobj;
+ }
+
+ let key = {
+ emails: newKey.userIds.map(uid =>
+ MailServices.headerParser
+ .makeFromDisplayAddress(uid.userId)[0]
+ ?.email.toLowerCase()
+ .trim()
+ ),
+ fingerprint: newKey.fpr,
+ userIds: newKey.userIds.map(uid => uid.userId),
+ id: newKey.keyId,
+ pubKey,
+ created: new Date(newKey.keyCreated * 1000),
+ expires: newKey.expiryTime ? new Date(newKey.expiryTime * 1000) : null,
+ sources: [source],
+ };
+ if (existing) {
+ // Keep existing sources meta information.
+ let sourceType = source.type;
+ let sourceURI = source.uri;
+ for (let oldSource of existing.sources.filter(
+ s => !(s.type == sourceType && s.uri == sourceURI)
+ )) {
+ key.sources.push(oldSource);
+ }
+ }
+ return key;
+ }
+
+ /**
+ * Delete keys for email.
+ *
+ * @param {string} email - Email to delete keys for.
+ */
+ async deleteKeysForEmail(email) {
+ email = email.toLowerCase();
+ return new Promise((resolve, reject) => {
+ let transaction = this.db.transaction(["seen_keys"], "readwrite");
+ let objectStore = transaction.objectStore("seen_keys");
+ let request = objectStore.index("emails").openKeyCursor();
+ request.onsuccess = event => {
+ let cursor = request.result;
+ if (cursor) {
+ objectStore.delete(cursor.primaryKey);
+ cursor.continue();
+ } else {
+ log.debug(`Deleted all keys for ${email}.`);
+ }
+ };
+ transaction.oncomplete = () => {
+ log.debug(`Keys gone for email ${email}.`);
+ resolve(email);
+ };
+ transaction.onerror = event => {
+ log.debug(
+ `Could not delete keys for email ${email}: ${transaction.error.message}`
+ );
+ reject(transaction.error);
+ };
+ });
+ }
+
+ /**
+ * Delete key by fingerprint.
+ *
+ * @param {string} fingerprint - fingerprint of key to delete.
+ */
+ async deleteKey(fingerprint) {
+ if (fingerprint.length != 40) {
+ throw new Error(`Invalid fingerprint: ${fingerprint}`);
+ }
+ return new Promise((resolve, reject) => {
+ let transaction = this.db.transaction(["seen_keys"], "readwrite");
+ let request = transaction.objectStore("seen_keys").delete(fingerprint);
+ request.onsuccess = () => {
+ log.debug(`Keys gone for fingerprint ${fingerprint}.`);
+ resolve(fingerprint);
+ };
+ request.onerror = event => {
+ log.debug(
+ `Could not delete keys for fingerprint ${fingerprint}: ${transaction.error.message}`
+ );
+ reject(transaction.error);
+ };
+ });
+ }
+
+ /**
+ * Clear out data from the database.
+ */
+ async reset() {
+ return new Promise((resolve, reject) => {
+ let transaction = this.db.transaction(["seen_keys"], "readwrite");
+ let objectStore = transaction.objectStore("seen_keys");
+ transaction.oncomplete = () => {
+ log.debug(`Objectstore cleared.`);
+ resolve();
+ };
+ transaction.onerror = () => {
+ log.debug(`Could not clear objectstore: ${transaction.error.message}`);
+ reject(transaction.error);
+ };
+ objectStore.clear();
+ transaction.commit();
+ });
+ }
+
+ /**
+ * Delete database.
+ */
+ static async deleteDb() {
+ return new Promise((resolve, reject) => {
+ let DBOpenRequest = indexedDB.deleteDatabase("seen_keys");
+ DBOpenRequest.onsuccess = () => {
+ log.debug(`Success deleting database.`);
+ resolve();
+ };
+ DBOpenRequest.onerror = () => {
+ log.debug(`Error deleting database: ${DBOpenRequest.error.message}`);
+ reject(DBOpenRequest.error);
+ };
+ });
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/GPGME.jsm b/comm/mail/extensions/openpgp/content/modules/GPGME.jsm
new file mode 100644
index 0000000000..899b5ebd35
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/GPGME.jsm
@@ -0,0 +1,338 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["GPGME"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ GPGMELibLoader: "chrome://openpgp/content/modules/GPGMELib.jsm",
+});
+
+var GPGMELib;
+
+var GPGME = {
+ hasRan: false,
+ libLoaded: false,
+ once() {
+ this.hasRan = true;
+ try {
+ GPGMELib = lazy.GPGMELibLoader.init();
+ if (!GPGMELib) {
+ return;
+ }
+ if (GPGMELib && GPGMELib.init()) {
+ GPGME.libLoaded = true;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ },
+
+ init(opts) {
+ opts = opts || {};
+
+ if (!this.hasRan) {
+ this.once();
+ }
+
+ return GPGME.libLoaded;
+ },
+
+ allDependenciesLoaded() {
+ return GPGME.libLoaded;
+ },
+
+ /**
+ * High level interface to retrieve public keys from GnuPG that
+ * contain a user ID that matches the given email address.
+ *
+ * @param {string} email - The email address to search for.
+ *
+ * @returns {Map} - a Map that contains ASCII armored key blocks
+ * indexed by fingerprint.
+ */
+ getPublicKeysForEmail(email) {
+ function keyFilterFunction(key) {
+ if (
+ key.contents.bitfield & GPGMELib.gpgme_key_t_revoked ||
+ key.contents.bitfield & GPGMELib.gpgme_key_t_expired ||
+ key.contents.bitfield & GPGMELib.gpgme_key_t_disabled ||
+ key.contents.bitfield & GPGMELib.gpgme_key_t_invalid ||
+ !(key.contents.bitfield & GPGMELib.gpgme_key_t_can_encrypt)
+ ) {
+ return false;
+ }
+
+ let matchesEmail = false;
+ let nextUid = key.contents.uids;
+ while (nextUid && !nextUid.isNull()) {
+ let uidEmail = nextUid.contents.email.readString();
+ // Variable email is provided by the outer scope.
+ if (uidEmail == email) {
+ matchesEmail = true;
+ break;
+ }
+ nextUid = nextUid.contents.next;
+ }
+ return matchesEmail;
+ }
+
+ return GPGMELib.exportKeys(email, false, keyFilterFunction);
+ },
+
+ async decrypt(encrypted, enArmorCB) {
+ let result = {};
+ result.decryptedData = "";
+
+ let arr = encrypted.split("").map(e => e.charCodeAt());
+ let encrypted_array = lazy.ctypes.uint8_t.array()(arr);
+ let tmp_array = lazy.ctypes.cast(
+ encrypted_array,
+ lazy.ctypes.char.array(encrypted_array.length)
+ );
+
+ let data_ciphertext = new GPGMELib.gpgme_data_t();
+ if (
+ GPGMELib.gpgme_data_new_from_mem(
+ data_ciphertext.address(),
+ tmp_array,
+ tmp_array.length,
+ 0
+ )
+ ) {
+ throw new Error("gpgme_data_new_from_mem failed");
+ }
+
+ let data_plain = new GPGMELib.gpgme_data_t();
+ if (GPGMELib.gpgme_data_new(data_plain.address())) {
+ throw new Error("gpgme_data_new failed");
+ }
+
+ let c1 = new GPGMELib.gpgme_ctx_t();
+ if (GPGMELib.gpgme_new(c1.address())) {
+ throw new Error("gpgme_new failed");
+ }
+
+ GPGMELib.gpgme_set_armor(c1, 1);
+
+ result.exitCode = GPGMELib.gpgme_op_decrypt_ext(
+ c1,
+ GPGMELib.GPGME_DECRYPT_UNWRAP,
+ data_ciphertext,
+ data_plain
+ );
+
+ if (GPGMELib.gpgme_data_release(data_ciphertext)) {
+ throw new Error("gpgme_data_release failed");
+ }
+
+ let result_len = new lazy.ctypes.size_t();
+ let result_buf = GPGMELib.gpgme_data_release_and_get_mem(
+ data_plain,
+ result_len.address()
+ );
+
+ if (!result_buf.isNull()) {
+ let unwrapped = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ // The result of decrypt(GPGME_DECRYPT_UNWRAP) is an OpenPGP message.
+ // Because old versions of GPGME (e.g. 1.12.0) may return the
+ // results as a binary encoding (despite gpgme_set_armor),
+ // we check if the result looks like an armored message.
+ // If it doesn't we apply armoring ourselves.
+
+ let armor_head = "-----BEGIN PGP MESSAGE-----";
+
+ let head_of_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(armor_head.length).ptr
+ ).contents;
+
+ let isArmored = false;
+
+ try {
+ // If this is binary, which usually isn't a valid UTF-8
+ // encoding, it will throw an error.
+ let head_of_array_string = head_of_array.readString();
+ if (head_of_array_string == armor_head) {
+ isArmored = true;
+ }
+ } catch (ex) {}
+
+ if (isArmored) {
+ result.decryptedData = unwrapped.readString();
+ } else {
+ result.decryptedData = enArmorCB(unwrapped, result_len.value);
+ }
+
+ GPGMELib.gpgme_free(result_buf);
+ }
+
+ GPGMELib.gpgme_release(c1);
+
+ return result;
+ },
+
+ async signDetached(plaintext, args, resultStatus) {
+ resultStatus.exitCode = -1;
+ resultStatus.statusFlags = 0;
+ resultStatus.statusMsg = "";
+ resultStatus.errorMsg = "";
+
+ if (args.encrypt || !args.sign || !args.sigTypeDetached) {
+ throw new Error("invalid encrypt/sign parameters");
+ }
+ if (!plaintext) {
+ throw new Error("cannot sign empty data");
+ }
+
+ let result = null;
+ //args.sender must be keyId
+ let keyId = args.sender.replace(/^0x/, "").toUpperCase();
+
+ let ctx = new GPGMELib.gpgme_ctx_t();
+ if (GPGMELib.gpgme_new(ctx.address())) {
+ throw new Error("gpgme_new failed");
+ }
+ GPGMELib.gpgme_set_armor(ctx, 1);
+ GPGMELib.gpgme_set_textmode(ctx, 1);
+ let keyHandle = new GPGMELib.gpgme_key_t();
+ if (!GPGMELib.gpgme_get_key(ctx, keyId, keyHandle.address(), 1)) {
+ if (!GPGMELib.gpgme_signers_add(ctx, keyHandle)) {
+ var tmp_array = lazy.ctypes.char.array()(plaintext);
+ let data_plaintext = new GPGMELib.gpgme_data_t();
+
+ // The tmp_array will have one additional byte to store the
+ // trailing null character, we don't want to sign it, thus -1.
+ if (
+ !GPGMELib.gpgme_data_new_from_mem(
+ data_plaintext.address(),
+ tmp_array,
+ tmp_array.length - 1,
+ 0
+ )
+ ) {
+ let data_signed = new GPGMELib.gpgme_data_t();
+ if (!GPGMELib.gpgme_data_new(data_signed.address())) {
+ let exitCode = GPGMELib.gpgme_op_sign(
+ ctx,
+ data_plaintext,
+ data_signed,
+ GPGMELib.GPGME_SIG_MODE_DETACH
+ );
+ if (exitCode != GPGMELib.GPG_ERR_NO_ERROR) {
+ GPGMELib.gpgme_data_release(data_signed);
+ } else {
+ let result_len = new lazy.ctypes.size_t();
+ let result_buf = GPGMELib.gpgme_data_release_and_get_mem(
+ data_signed,
+ result_len.address()
+ );
+ if (!result_buf.isNull()) {
+ let unwrapped = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = unwrapped.readString();
+ resultStatus.exitCode = 0;
+ resultStatus.statusFlags |= lazy.EnigmailConstants.SIG_CREATED;
+ GPGMELib.gpgme_free(result_buf);
+ }
+ }
+ }
+ }
+ }
+ GPGMELib.gpgme_key_release(keyHandle);
+ }
+ GPGMELib.gpgme_release(ctx);
+ return result;
+ },
+
+ async sign(plaintext, args, resultStatus) {
+ resultStatus.exitCode = -1;
+ resultStatus.statusFlags = 0;
+ resultStatus.statusMsg = "";
+ resultStatus.errorMsg = "";
+
+ if (args.encrypt || !args.sign) {
+ throw new Error("invalid encrypt/sign parameters");
+ }
+ if (!plaintext) {
+ throw new Error("cannot sign empty data");
+ }
+
+ let result = null;
+ //args.sender must be keyId
+ let keyId = args.sender.replace(/^0x/, "").toUpperCase();
+
+ let ctx = new GPGMELib.gpgme_ctx_t();
+ if (GPGMELib.gpgme_new(ctx.address())) {
+ throw new Error("gpgme_new failed");
+ }
+ let keyHandle = new GPGMELib.gpgme_key_t();
+ if (!GPGMELib.gpgme_get_key(ctx, keyId, keyHandle.address(), 1)) {
+ if (!GPGMELib.gpgme_signers_add(ctx, keyHandle)) {
+ var tmp_array = lazy.ctypes.char.array()(plaintext);
+ let data_plaintext = new GPGMELib.gpgme_data_t();
+
+ // The tmp_array will have one additional byte to store the
+ // trailing null character, we don't want to sign it, thus -1.
+ if (
+ !GPGMELib.gpgme_data_new_from_mem(
+ data_plaintext.address(),
+ tmp_array,
+ tmp_array.length - 1,
+ 0
+ )
+ ) {
+ let data_signed = new GPGMELib.gpgme_data_t();
+ if (!GPGMELib.gpgme_data_new(data_signed.address())) {
+ let exitCode = GPGMELib.gpgme_op_sign(
+ ctx,
+ data_plaintext,
+ data_signed,
+ GPGMELib.GPGME_SIG_MODE_NORMAL
+ );
+ if (exitCode != GPGMELib.GPG_ERR_NO_ERROR) {
+ GPGMELib.gpgme_data_release(data_signed);
+ } else {
+ let result_len = new lazy.ctypes.size_t();
+ let result_buf = GPGMELib.gpgme_data_release_and_get_mem(
+ data_signed,
+ result_len.address()
+ );
+ if (!result_buf.isNull()) {
+ let unwrapped = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.uint8_t.array(result_len.value).ptr
+ ).contents;
+
+ result = unwrapped.readTypedArray();
+ resultStatus.exitCode = 0;
+ resultStatus.statusFlags |= lazy.EnigmailConstants.SIG_CREATED;
+ GPGMELib.gpgme_free(result_buf);
+ }
+ }
+ }
+ }
+ }
+ GPGMELib.gpgme_key_release(keyHandle);
+ }
+ GPGMELib.gpgme_release(ctx);
+ return result;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/GPGMELib.jsm b/comm/mail/extensions/openpgp/content/modules/GPGMELib.jsm
new file mode 100644
index 0000000000..c58181da37
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/GPGMELib.jsm
@@ -0,0 +1,584 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["GPGMELibLoader"];
+
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+
+var systemOS = Services.appinfo.OS.toLowerCase();
+var abi = ctypes.default_abi;
+
+// Default library paths to look for on macOS
+const ADDITIONAL_LIB_PATHS = [
+ "/usr/local/lib",
+ "/opt/local/lib",
+ "/opt/homebrew/lib",
+];
+
+// Open libgpgme. Determine the path to the chrome directory and look for it
+// there first. If not, fallback to searching the standard locations.
+var libgpgme, libgpgmePath;
+
+function tryLoadGPGME(name, suffix) {
+ let filename = ctypes.libraryName(name) + suffix;
+ let binPath = Services.dirsvc.get("XpcomLib", Ci.nsIFile).path;
+ let binDir = PathUtils.parent(binPath);
+ libgpgmePath = PathUtils.join(binDir, filename);
+
+ let loadFromInfo;
+
+ try {
+ loadFromInfo = libgpgmePath;
+ libgpgme = ctypes.open(libgpgmePath);
+ } catch (e) {}
+
+ if (!libgpgme) {
+ try {
+ loadFromInfo = "system's standard library locations";
+ // look in standard locations
+ libgpgmePath = filename;
+ libgpgme = ctypes.open(libgpgmePath);
+ } catch (e) {}
+ }
+
+ if (!libgpgme && systemOS !== "winnt") {
+ // try specific additional directories
+
+ for (let tryPath of ADDITIONAL_LIB_PATHS) {
+ try {
+ loadFromInfo = "additional standard locations";
+ libgpgmePath = tryPath + "/" + filename;
+ libgpgme = ctypes.open(libgpgmePath);
+
+ if (libgpgme) {
+ break;
+ }
+ } catch (e) {}
+ }
+ }
+
+ if (libgpgme) {
+ console.debug(
+ "Successfully loaded optional OpenPGP library " +
+ filename +
+ " from " +
+ loadFromInfo
+ );
+ }
+}
+
+function loadExternalGPGMELib() {
+ if (!libgpgme) {
+ if (systemOS === "winnt") {
+ tryLoadGPGME("libgpgme6-11", "");
+
+ if (!libgpgme) {
+ tryLoadGPGME("libgpgme-11", "");
+ }
+
+ if (!libgpgme) {
+ tryLoadGPGME("gpgme-11", "");
+ }
+ }
+
+ if (!libgpgme) {
+ tryLoadGPGME("gpgme", "");
+ }
+
+ if (!libgpgme) {
+ tryLoadGPGME("gpgme", ".11");
+ }
+
+ if (!libgpgme) {
+ tryLoadGPGME("gpgme.11");
+ }
+ }
+
+ return !!libgpgme;
+}
+
+var GPGMELibLoader = {
+ init() {
+ if (!loadExternalGPGMELib()) {
+ return null;
+ }
+ if (libgpgme) {
+ enableGPGMELibJS();
+ }
+ return GPGMELib;
+ },
+};
+
+const gpgme_error_t = ctypes.unsigned_int;
+const gpgme_ctx_t = ctypes.void_t.ptr;
+const gpgme_data_t = ctypes.void_t.ptr;
+const gpgme_validity_t = ctypes.int;
+const gpgme_keylist_mode_t = ctypes.unsigned_int;
+const gpgme_protocol_t = ctypes.int;
+const gpgme_pubkey_algo_t = ctypes.int;
+const gpgme_sig_notation_flags_t = ctypes.unsigned_int;
+const gpgme_export_mode_t = ctypes.unsigned_int;
+const gpgme_decrypt_flags_t = ctypes.unsigned_int;
+const gpgme_data_encoding_t = ctypes.unsigned_int;
+const gpgme_sig_mode_t = ctypes.int; // it's an enum, risk of wrong type.
+
+let _gpgme_subkey = ctypes.StructType("_gpgme_subkey");
+_gpgme_subkey.define([
+ { next: _gpgme_subkey.ptr },
+ { bitfield: ctypes.unsigned_int },
+ { pubkey_algo: gpgme_pubkey_algo_t },
+ { length: ctypes.unsigned_int },
+ { keyid: ctypes.char.ptr },
+ { _keyid: ctypes.char.array(17) },
+ { fpr: ctypes.char.ptr },
+ { timestamp: ctypes.long },
+ { expires: ctypes.long },
+ { card_number: ctypes.char.ptr },
+ { curve: ctypes.char.ptr },
+ { keygrip: ctypes.char.ptr },
+]);
+let gpgme_subkey_t = _gpgme_subkey.ptr;
+
+let _gpgme_sig_notation = ctypes.StructType("_gpgme_sig_notation");
+_gpgme_sig_notation.define([
+ { next: _gpgme_sig_notation.ptr },
+ { name: ctypes.char.ptr },
+ { value: ctypes.char.ptr },
+ { name_len: ctypes.int },
+ { value_len: ctypes.int },
+ { flags: gpgme_sig_notation_flags_t },
+ { bitfield: ctypes.unsigned_int },
+]);
+let gpgme_sig_notation_t = _gpgme_sig_notation.ptr;
+
+let _gpgme_key_sig = ctypes.StructType("_gpgme_key_sig");
+_gpgme_key_sig.define([
+ { next: _gpgme_key_sig.ptr },
+ { bitfield: ctypes.unsigned_int },
+ { pubkey_algo: gpgme_pubkey_algo_t },
+ { keyid: ctypes.char.ptr },
+ { _keyid: ctypes.char.array(17) },
+ { timestamp: ctypes.long },
+ { expires: ctypes.long },
+ { status: gpgme_error_t },
+ { class_: ctypes.unsigned_int },
+ { uid: ctypes.char.ptr },
+ { name: ctypes.char.ptr },
+ { email: ctypes.char.ptr },
+ { comment: ctypes.char.ptr },
+ { sig_class: ctypes.unsigned_int },
+ { notations: gpgme_sig_notation_t },
+ { last_notation: gpgme_sig_notation_t },
+]);
+let gpgme_key_sig_t = _gpgme_key_sig.ptr;
+
+let _gpgme_tofu_info = ctypes.StructType("_gpgme_tofu_info");
+_gpgme_tofu_info.define([
+ { next: _gpgme_tofu_info.ptr },
+ { bitfield: ctypes.unsigned_int },
+ { signcount: ctypes.unsigned_short },
+ { encrcount: ctypes.unsigned_short },
+ { signfirst: ctypes.unsigned_short },
+ { signlast: ctypes.unsigned_short },
+ { encrfirst: ctypes.unsigned_short },
+ { encrlast: ctypes.unsigned_short },
+ { description: ctypes.char.ptr },
+]);
+let gpgme_tofu_info_t = _gpgme_tofu_info.ptr;
+
+let _gpgme_user_id = ctypes.StructType("_gpgme_user_id");
+_gpgme_user_id.define([
+ { next: _gpgme_user_id.ptr },
+ { bitfield: ctypes.unsigned_int },
+ { validity: gpgme_validity_t },
+ { uid: ctypes.char.ptr },
+ { name: ctypes.char.ptr },
+ { email: ctypes.char.ptr },
+ { comment: ctypes.char.ptr },
+ { signatures: gpgme_key_sig_t },
+ { _last_keysig: gpgme_key_sig_t },
+ { address: ctypes.char.ptr },
+ { tofu: gpgme_tofu_info_t },
+ { last_update: ctypes.unsigned_long },
+]);
+let gpgme_user_id_t = _gpgme_user_id.ptr;
+
+let _gpgme_key = ctypes.StructType("gpgme_key_t", [
+ { _refs: ctypes.unsigned_int },
+ { bitfield: ctypes.unsigned_int },
+ { protocol: gpgme_protocol_t },
+ { issuer_serial: ctypes.char.ptr },
+ { issuer_name: ctypes.char.ptr },
+ { chain_id: ctypes.char.ptr },
+ { owner_trust: gpgme_validity_t },
+ { subkeys: gpgme_subkey_t },
+ { uids: gpgme_user_id_t },
+ { _last_subkey: gpgme_subkey_t },
+ { _last_uid: gpgme_user_id_t },
+ { keylist_mode: gpgme_keylist_mode_t },
+ { fpr: ctypes.char.ptr },
+ { last_update: ctypes.unsigned_long },
+]);
+let gpgme_key_t = _gpgme_key.ptr;
+
+var GPGMELib;
+
+function enableGPGMELibJS() {
+ // this must be delayed until after "libgpgme" is initialized
+
+ GPGMELib = {
+ path: libgpgmePath,
+
+ init() {
+ // GPGME 1.9.0 released 2017-03-28 is the first version that
+ // supports GPGME_DECRYPT_UNWRAP, requiring >= gpg 2.1.12
+ let versionPtr = this.gpgme_check_version("1.9.0");
+ let version = versionPtr.readString();
+ console.debug("gpgme version: " + version);
+
+ let gpgExe = Services.prefs.getStringPref(
+ "mail.openpgp.alternative_gpg_path"
+ );
+ if (!gpgExe) {
+ return true;
+ }
+
+ let extResult = this.gpgme_set_engine_info(
+ this.GPGME_PROTOCOL_OpenPGP,
+ gpgExe,
+ null
+ );
+ let success = extResult === this.GPG_ERR_NO_ERROR;
+ let info = success ? "success" : "failure: " + extResult;
+ console.debug(
+ "configuring GPGME to use an external OpenPGP engine " +
+ gpgExe +
+ " - " +
+ info
+ );
+ return success;
+ },
+
+ /**
+ * Export key blocks from GnuPG that match the given paramters.
+ *
+ * @param {string} pattern - A pattern given to GnuPG for listing keys.
+ * @param {boolean} secret - If true, retrieve secret keys.
+ * If false, retrieve public keys.
+ * @param {function} keyFilterFunction - An optional test function that
+ * will be called for each candidate key that GnuPG lists for the
+ * given pattern. Allows the caller to decide whether a candidate
+ * key is wanted or not. Function will be called with a
+ * {gpgme_key_t} parameter, the candidate key returned by GPGME.
+ *
+ * @returns {Map} - a Map that contains ASCII armored key blocks
+ * indexed by fingerprint.
+ */
+ exportKeys(pattern, secret = false, keyFilterFunction = undefined) {
+ let resultMap = new Map();
+ let allFingerprints = [];
+
+ let c1 = new gpgme_ctx_t();
+ if (this.gpgme_new(c1.address())) {
+ throw new Error("gpgme_new failed");
+ }
+
+ if (this.gpgme_op_keylist_start(c1, pattern, secret ? 1 : 0)) {
+ throw new Error("gpgme_op_keylist_start failed");
+ }
+
+ do {
+ let key = new gpgme_key_t();
+ let rv = this.gpgme_op_keylist_next(c1, key.address());
+ if (rv & GPGMELib.GPG_ERR_EOF) {
+ break;
+ } else if (rv) {
+ throw new Error("gpgme_op_keylist_next failed: " + rv);
+ }
+
+ if (key.contents.protocol == GPGMELib.GPGME_PROTOCOL_OpenPGP) {
+ if (!keyFilterFunction || keyFilterFunction(key)) {
+ let fpr = key.contents.fpr.readString();
+ allFingerprints.push(fpr);
+ }
+ }
+ this.gpgme_key_release(key);
+ } while (true);
+
+ if (this.gpgme_op_keylist_end(c1)) {
+ throw new Error("gpgme_op_keylist_end failed");
+ }
+
+ this.gpgme_release(c1);
+
+ for (let aFpr of allFingerprints) {
+ let c2 = new gpgme_ctx_t();
+ if (this.gpgme_new(c2.address())) {
+ throw new Error("gpgme_new failed");
+ }
+
+ this.gpgme_set_armor(c2, 1);
+
+ let data = new gpgme_data_t();
+ let rv = this.gpgme_data_new(data.address());
+ if (rv) {
+ throw new Error("gpgme_op_keylist_next gpgme_data_new: " + rv);
+ }
+
+ rv = this.gpgme_op_export(
+ c2,
+ aFpr,
+ secret ? GPGMELib.GPGME_EXPORT_MODE_SECRET : 0,
+ data
+ );
+ if (rv) {
+ throw new Error("gpgme_op_export gpgme_data_new: " + rv);
+ }
+
+ let result_len = new ctypes.size_t();
+ let result_buf = this.gpgme_data_release_and_get_mem(
+ data,
+ result_len.address()
+ );
+
+ let keyData = ctypes.cast(
+ result_buf,
+ ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ resultMap.set(aFpr, keyData.readString());
+
+ this.gpgme_free(result_buf);
+ this.gpgme_release(c2);
+ }
+ return resultMap;
+ },
+
+ gpgme_check_version: libgpgme.declare(
+ "gpgme_check_version",
+ abi,
+ ctypes.char.ptr,
+ ctypes.char.ptr
+ ),
+
+ gpgme_set_engine_info: libgpgme.declare(
+ "gpgme_set_engine_info",
+ abi,
+ gpgme_error_t,
+ gpgme_protocol_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr
+ ),
+
+ gpgme_new: libgpgme.declare("gpgme_new", abi, gpgme_error_t, gpgme_ctx_t),
+
+ gpgme_release: libgpgme.declare(
+ "gpgme_release",
+ abi,
+ ctypes.void_t,
+ gpgme_ctx_t
+ ),
+
+ gpgme_key_release: libgpgme.declare(
+ "gpgme_key_release",
+ abi,
+ ctypes.void_t,
+ gpgme_key_t
+ ),
+
+ gpgme_op_keylist_start: libgpgme.declare(
+ "gpgme_op_keylist_start",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ ctypes.char.ptr,
+ ctypes.int
+ ),
+
+ gpgme_op_keylist_next: libgpgme.declare(
+ "gpgme_op_keylist_next",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ gpgme_key_t.ptr
+ ),
+
+ gpgme_op_keylist_end: libgpgme.declare(
+ "gpgme_op_keylist_end",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t
+ ),
+
+ gpgme_op_export: libgpgme.declare(
+ "gpgme_op_export",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ ctypes.char.ptr,
+ gpgme_export_mode_t,
+ gpgme_data_t
+ ),
+
+ gpgme_set_armor: libgpgme.declare(
+ "gpgme_set_armor",
+ abi,
+ ctypes.void_t,
+ gpgme_ctx_t,
+ ctypes.int
+ ),
+
+ gpgme_data_new: libgpgme.declare(
+ "gpgme_data_new",
+ abi,
+ gpgme_error_t,
+ gpgme_data_t.ptr
+ ),
+
+ gpgme_data_release: libgpgme.declare(
+ "gpgme_data_release",
+ abi,
+ ctypes.void_t,
+ gpgme_data_t
+ ),
+
+ gpgme_data_release_and_get_mem: libgpgme.declare(
+ "gpgme_data_release_and_get_mem",
+ abi,
+ ctypes.char.ptr,
+ gpgme_data_t,
+ ctypes.size_t.ptr
+ ),
+
+ gpgme_free: libgpgme.declare(
+ "gpgme_free",
+ abi,
+ ctypes.void_t,
+ ctypes.void_t.ptr
+ ),
+
+ gpgme_op_decrypt_ext: libgpgme.declare(
+ "gpgme_op_decrypt_ext",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ gpgme_decrypt_flags_t,
+ gpgme_data_t,
+ gpgme_data_t
+ ),
+
+ gpgme_data_new_from_mem: libgpgme.declare(
+ "gpgme_data_new_from_mem",
+ abi,
+ gpgme_error_t,
+ gpgme_data_t.ptr,
+ ctypes.char.ptr,
+ ctypes.size_t,
+ ctypes.int
+ ),
+
+ gpgme_data_read: libgpgme.declare(
+ "gpgme_data_read",
+ abi,
+ ctypes.ssize_t,
+ gpgme_data_t,
+ ctypes.void_t.ptr,
+ ctypes.size_t
+ ),
+
+ gpgme_data_rewind: libgpgme.declare(
+ "gpgme_data_rewind",
+ abi,
+ gpgme_error_t,
+ gpgme_data_t
+ ),
+
+ gpgme_data_get_encoding: libgpgme.declare(
+ "gpgme_data_get_encoding",
+ abi,
+ gpgme_data_encoding_t,
+ gpgme_data_t
+ ),
+
+ gpgme_data_set_encoding: libgpgme.declare(
+ "gpgme_data_set_encoding",
+ abi,
+ gpgme_error_t,
+ gpgme_data_t,
+ gpgme_data_encoding_t
+ ),
+
+ gpgme_op_sign: libgpgme.declare(
+ "gpgme_op_sign",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ gpgme_data_t,
+ gpgme_data_t,
+ gpgme_sig_mode_t
+ ),
+
+ gpgme_signers_add: libgpgme.declare(
+ "gpgme_signers_add",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ gpgme_key_t
+ ),
+
+ gpgme_get_key: libgpgme.declare(
+ "gpgme_get_key",
+ abi,
+ gpgme_error_t,
+ gpgme_ctx_t,
+ ctypes.char.ptr,
+ gpgme_key_t.ptr,
+ ctypes.int
+ ),
+
+ gpgme_set_textmode: libgpgme.declare(
+ "gpgme_set_textmode",
+ abi,
+ ctypes.void_t,
+ gpgme_ctx_t,
+ ctypes.int
+ ),
+
+ gpgme_error_t,
+ gpgme_ctx_t,
+ gpgme_data_t,
+ gpgme_validity_t,
+ gpgme_keylist_mode_t,
+ gpgme_pubkey_algo_t,
+ gpgme_sig_notation_flags_t,
+ gpgme_export_mode_t,
+ gpgme_decrypt_flags_t,
+ gpgme_data_encoding_t,
+
+ gpgme_protocol_t,
+ gpgme_subkey_t,
+ gpgme_sig_notation_t,
+ gpgme_key_sig_t,
+ gpgme_tofu_info_t,
+ gpgme_user_id_t,
+ gpgme_key_t,
+
+ GPG_ERR_NO_ERROR: 0x00000000,
+ GPG_ERR_EOF: 16383,
+ GPGME_PROTOCOL_OpenPGP: 0,
+ GPGME_EXPORT_MODE_SECRET: 16,
+ GPGME_DECRYPT_UNWRAP: 128,
+ GPGME_DATA_ENCODING_ARMOR: 3,
+ GPGME_SIG_MODE_DETACH: 1,
+ GPGME_SIG_MODE_NORMAL: 0,
+
+ gpgme_key_t_revoked: 1,
+ gpgme_key_t_expired: 2,
+ gpgme_key_t_disabled: 4,
+ gpgme_key_t_invalid: 8,
+ gpgme_key_t_can_encrypt: 16,
+ };
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/OpenPGPAlias.jsm b/comm/mail/extensions/openpgp/content/modules/OpenPGPAlias.jsm
new file mode 100644
index 0000000000..f480b727f6
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/OpenPGPAlias.jsm
@@ -0,0 +1,173 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["OpenPGPAlias"];
+
+var OpenPGPAlias = {
+ _aliasDomains: null,
+ _aliasEmails: null,
+
+ _loaded() {
+ return this._aliasDomains && this._aliasEmails;
+ },
+
+ async load() {
+ let path = Services.prefs.getStringPref(
+ "mail.openpgp.alias_rules_file",
+ ""
+ );
+
+ if (!path) {
+ this._clear();
+ return;
+ }
+
+ await this._loadFromFile(path);
+ },
+
+ _clear() {
+ this._aliasDomains = new Map();
+ this._aliasEmails = new Map();
+ },
+
+ _hasExpectedKeysStructure(keys) {
+ try {
+ for (let entry of keys) {
+ if (!("id" in entry) && !("fingerprint" in entry)) {
+ return false;
+ }
+ }
+ // all entries passed the test
+ return true;
+ } catch (ex) {
+ return false;
+ }
+ },
+
+ async _loadFromFile(src) {
+ this._clear();
+
+ let aliasRules;
+ let jsonData;
+ if (src.startsWith("file://")) {
+ let response = await fetch(src);
+ jsonData = await response.json();
+ } else if (src.includes("/") || src.includes("\\")) {
+ throw new Error(`Invalid alias rules src: ${src}`);
+ } else {
+ let spec = PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ src
+ );
+ let response = await fetch(PathUtils.toFileURI(spec));
+ jsonData = await response.json();
+ }
+ if (!("rules" in jsonData)) {
+ throw new Error(
+ "alias file contains invalid JSON data, no rules element found"
+ );
+ }
+ aliasRules = jsonData.rules;
+
+ for (let entry of aliasRules) {
+ if (!("keys" in entry) || !entry.keys || !entry.keys.length) {
+ console.log("Ignoring invalid alias rule without keys");
+ continue;
+ }
+ if ("email" in entry && "domain" in entry) {
+ console.log("Ignoring invalid alias rule with both email and domain");
+ continue;
+ }
+ // Ignore duplicate rules, only use first rule per key.
+ // Require email address contains @, and domain doesn't contain @.
+ if ("email" in entry) {
+ let email = entry.email.toLowerCase();
+ if (!email.includes("@")) {
+ console.log("Ignoring invalid email alias rule: " + email);
+ continue;
+ }
+ if (this._aliasEmails.get(email)) {
+ console.log("Ignoring duplicate email alias rule: " + email);
+ continue;
+ }
+ if (!this._hasExpectedKeysStructure(entry.keys)) {
+ console.log(
+ "Ignoring alias rule with invalid key entries for email " + email
+ );
+ continue;
+ }
+ this._aliasEmails.set(email, entry.keys);
+ } else if ("domain" in entry) {
+ let domain = entry.domain.toLowerCase();
+ if (domain.includes("@")) {
+ console.log("Ignoring invalid domain alias rule: " + domain);
+ continue;
+ }
+ if (this._aliasDomains.get(domain)) {
+ console.log("Ignoring duplicate domain alias rule: " + domain);
+ continue;
+ }
+ if (!this._hasExpectedKeysStructure(entry.keys)) {
+ console.log(
+ "Ignoring alias rule with invalid key entries for domain " + domain
+ );
+ continue;
+ }
+ this._aliasDomains.set(domain, entry.keys);
+ } else {
+ console.log(
+ "Ignoring invalid alias rule without domain and without email"
+ );
+ }
+ }
+ },
+
+ getDomainAliasKeyList(email) {
+ if (!this._loaded()) {
+ return null;
+ }
+
+ let lastAt = email.lastIndexOf("@");
+ if (lastAt == -1) {
+ return null;
+ }
+
+ let domain = email.substr(lastAt + 1);
+ if (!domain) {
+ return null;
+ }
+
+ return this._aliasDomains.get(domain.toLowerCase());
+ },
+
+ getEmailAliasKeyList(email) {
+ if (!this._loaded()) {
+ return null;
+ }
+ return this._aliasEmails.get(email.toLowerCase());
+ },
+
+ hasAliasDefinition(email) {
+ if (!this._loaded()) {
+ return false;
+ }
+ email = email.toLowerCase();
+ let hasEmail = this._aliasEmails.has(email);
+ if (hasEmail) {
+ return true;
+ }
+
+ let lastAt = email.lastIndexOf("@");
+ if (lastAt == -1) {
+ return false;
+ }
+
+ let domain = email.substr(lastAt + 1);
+ if (!domain) {
+ return false;
+ }
+
+ return this._aliasDomains.has(domain);
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/RNP.jsm b/comm/mail/extensions/openpgp/content/modules/RNP.jsm
new file mode 100644
index 0000000000..c6969842c8
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/RNP.jsm
@@ -0,0 +1,4787 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["RNP", "RnpPrivateKeyUnlockTracker"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ GPGME: "chrome://openpgp/content/modules/GPGME.jsm",
+ OpenPGPMasterpass: "chrome://openpgp/content/modules/masterpass.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+ RNPLibLoader: "chrome://openpgp/content/modules/RNPLib.jsm",
+});
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"]);
+
+const str_encrypt = "encrypt";
+const str_sign = "sign";
+const str_certify = "certify";
+const str_authenticate = "authenticate";
+const RNP_PHOTO_USERID_ID = "(photo)"; // string is hardcoded inside RNP
+
+var RNPLib;
+
+/**
+ * Opens a prompt, asking the user to enter passphrase for given key id.
+
+ * @param {?nsIWindow} win - Parent window, may be null
+ * @param {string} promptString - This message will be shown to the user
+ * @param {object} resultFlags - Attribute .canceled is set to true
+ * if the user clicked cancel, other it's set to false.
+ * @returns {string} - The passphrase the user entered
+ */
+function passphrasePromptCallback(win, promptString, resultFlags) {
+ let password = { value: "" };
+ if (!Services.prompt.promptPassword(win, "", promptString, password)) {
+ resultFlags.canceled = true;
+ return "";
+ }
+
+ resultFlags.canceled = false;
+ return password.value;
+}
+
+/**
+ * Helper class to track resources related to a private/secret key,
+ * holding the key handle obtained from RNP, and offering services
+ * related to that key and its handle, including releasing the handle
+ * when done. Tracking a null handle is allowed.
+ */
+class RnpPrivateKeyUnlockTracker {
+ #rnpKeyHandle = null;
+ #wasUnlocked = false;
+ #allowPromptingUserForPassword = false;
+ #allowAutoUnlockWithCachedPasswords = false;
+ #passwordCache = null;
+ #fingerprint = "";
+ #passphraseCallback = null;
+ #rememberUnlockPasswordForUnprotect = false;
+ #unlockPassword = null;
+ #isLocked = true;
+
+ /**
+ * Initialize this object as a tracker for the private key identified
+ * by the given fingerprint. The fingerprint will be looked up in an
+ * RNP space (FFI) and the resulting handle will be tracked. The
+ * default FFI is used for performing the lookup, unless a specific
+ * FFI is given. If no key can be found, the object is initialized
+ * with a null handle. If a handle was found, the handle and any
+ * additional resources can be freed by calling the object's release()
+ * method.
+ *
+ * @param {string} fingerprint - the fingerprint of a key to look up.
+ * @param {rnp_ffi_t} ffi - An optional specific FFI.
+ * @returns {RnpPrivateKeyUnlockTracker} - a new instance, which was
+ * either initialized with a found key handle, or with null-
+ */
+ static constructFromFingerprint(fingerprint, ffi = RNPLib.ffi) {
+ if (fingerprint.startsWith("0x")) {
+ throw new Error("fingerprint must not start with 0x");
+ }
+
+ let handle = RNP.getKeyHandleByKeyIdOrFingerprint(ffi, `0x${fingerprint}`);
+
+ return new RnpPrivateKeyUnlockTracker(handle);
+ }
+
+ /**
+ * Construct this object as a tracker for the private key referenced
+ * by the given handle. The object may also be initialized
+ * with null, if no key was found. A valid handle and any additional
+ * resources can be freed by calling the object's release() method.
+ *
+ * @param {?rnp_key_handle_t} handle - the handle of a RNP key, or null
+ */
+ constructor(handle) {
+ if (this.#rnpKeyHandle) {
+ throw new Error("Instance already initialized");
+ }
+ if (!handle) {
+ return;
+ }
+ this.#rnpKeyHandle = handle;
+
+ if (!this.available()) {
+ // Not a private key. We tolerate this use to enable automatic
+ // handle releasing, for code that sometimes needs to track a
+ // secret key, and sometimes only a public key.
+ // The only functionality that is allowed on such a key is to
+ // call the .available() and the .release() methods.
+ this.#isLocked = false;
+ } else {
+ let is_locked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_locked(this.#rnpKeyHandle, is_locked.address())) {
+ throw new Error("rnp_key_is_locked failed");
+ }
+ this.#isLocked = is_locked.value;
+ }
+
+ if (!this.#fingerprint) {
+ let fingerprint = new lazy.ctypes.char.ptr();
+ if (
+ RNPLib.rnp_key_get_fprint(this.#rnpKeyHandle, fingerprint.address())
+ ) {
+ throw new Error("rnp_key_get_fprint failed");
+ }
+ this.#fingerprint = fingerprint.readString();
+ RNPLib.rnp_buffer_destroy(fingerprint);
+ }
+ }
+
+ /**
+ * @param {Function} cb - Override the callback function that this
+ * object will call to obtain the passphrase to unlock the private
+ * key for tracked key handle, if the object needs to unlock
+ * the key and prompting the user is allowed.
+ * If no alternative callback is set, the global
+ * passphrasePromptCallback function will be used.
+ */
+ setPassphraseCallback(cb) {
+ this.#passphraseCallback = cb;
+ }
+
+ /**
+ * Allow or forbid prompting the user for a passphrase.
+ *
+ * @param {boolean} isAllowed - True if allowed, false if forbidden
+ */
+ setAllowPromptingUserForPassword(isAllowed) {
+ this.#allowPromptingUserForPassword = isAllowed;
+ }
+
+ /**
+ * Allow or forbid automatically using passphrases from a configured
+ * cache of passphrase, if it's necessary to obtain a passphrase
+ * for unlocking.
+ *
+ * @param {boolean} isAllowed - True if allowed, false if forbidden
+ */
+ setAllowAutoUnlockWithCachedPasswords(isAllowed) {
+ this.#allowAutoUnlockWithCachedPasswords = isAllowed;
+ }
+
+ /**
+ * Allow or forbid this object to remember the passphrase that was
+ * successfully used to to unlock it. This is necessary when intending
+ * to subsequently call the unprotect() function to remove the key's
+ * passphrase protection. Care should be taken that a tracker object
+ * with a remembered passphrase is held in memory only for a short
+ * amount of time, and should be released as soon as a task has
+ * completed.
+ *
+ * @param {boolean} isAllowed - True if allowed, false if forbidden
+ */
+ setRememberUnlockPassword(isAllowed) {
+ this.#rememberUnlockPasswordForUnprotect = isAllowed;
+ }
+
+ /**
+ * Registers a reference to shared object that implements an optional
+ * password cache. Will be used to look up passwords if
+ * #allowAutoUnlockWithCachedPasswords is set to true. Will be used
+ * to store additional passwords that are found to unlock a key.
+ */
+ setPasswordCache(cacheObj) {
+ this.#passwordCache = cacheObj;
+ }
+
+ /**
+ * Completely remove the encryption layer that protects the private
+ * key. Requires that setRememberUnlockPassword(true) was already
+ * called on this object, prior to unlocking the key, because this
+ * code requires that the unlock/unprotect passphrase has been cached
+ * in this object already, and that the tracked key has already been
+ * unlocked.
+ */
+ unprotect() {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ const is_protected = new lazy.ctypes.bool();
+ if (
+ RNPLib.rnp_key_is_protected(this.#rnpKeyHandle, is_protected.address())
+ ) {
+ throw new Error("rnp_key_is_protected failed");
+ }
+ if (!is_protected.value) {
+ return;
+ }
+
+ if (!this.#wasUnlocked || !this.#rememberUnlockPasswordForUnprotect) {
+ // This precondition ensures we have the correct password cached.
+ throw new Error("Key should have been unlocked already.");
+ }
+
+ if (RNPLib.rnp_key_unprotect(this.#rnpKeyHandle, this.#unlockPassword)) {
+ throw new Error(`Failed to unprotect private key ${this.#fingerprint}`);
+ }
+ }
+
+ /**
+ * Attempt to unlock the tracked key with the given passphrase,
+ * can also be used with the empty string, which will unlock the key
+ * if no passphrase is set.
+ *
+ * @param {string} pass - try to unlock the key using this passphrase
+ */
+ unlockWithPassword(pass) {
+ if (!this.#rnpKeyHandle || !this.#isLocked) {
+ return;
+ }
+ this.#wasUnlocked = false;
+
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, pass)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = pass;
+ }
+ }
+ }
+
+ /**
+ * Attempt to unlock the tracked key, using the allowed unlock
+ * mechanisms that have been configured/allowed for this tracker,
+ * which must been configured as desired prior to calling this function.
+ * Attempts will potentially be made to unlock using the automatic
+ * passphrase, or using password available in the password cache,
+ * or by prompting the user for a password, repeatedly prompting
+ * until the user enters the correct password or cancels.
+ * When prompting the user for a passphrase, and the key is a subkey,
+ * it might be necessary to lookup the primary key. A RNP FFI handle
+ * is necessary for that potential lookup.
+ * Unless a ffi parameter is provided, the default ffi is used.
+ *
+ * @param {rnp_ffi_t} ffi - An optional specific FFI.
+ */
+ async unlock(ffi = RNPLib.ffi) {
+ if (!this.#rnpKeyHandle || !this.#isLocked) {
+ return;
+ }
+ this.#wasUnlocked = false;
+ let autoPassword = await lazy.OpenPGPMasterpass.retrieveOpenPGPPassword();
+
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, autoPassword)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = autoPassword;
+ }
+ return;
+ }
+
+ if (this.#allowAutoUnlockWithCachedPasswords && this.#passwordCache) {
+ for (let pw of this.#passwordCache.passwords) {
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, pw)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = pw;
+ }
+ return;
+ }
+ }
+ }
+
+ if (!this.#allowPromptingUserForPassword) {
+ return;
+ }
+
+ let promptString = await RNP.getPassphrasePrompt(this.#rnpKeyHandle, ffi);
+
+ while (true) {
+ let userFlags = { canceled: false };
+ let pass;
+ if (this.#passphraseCallback) {
+ pass = this.#passphraseCallback(null, promptString, userFlags);
+ } else {
+ pass = passphrasePromptCallback(null, promptString, userFlags);
+ }
+ if (userFlags.canceled) {
+ return;
+ }
+
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, pass)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = pass;
+ }
+
+ if (this.#passwordCache) {
+ this.#passwordCache.passwords.push(pass);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Check that this tracker has a reference to a private key.
+ *
+ * @returns {boolean} - true if the tracked key is a secret/private
+ */
+ isSecret() {
+ return (
+ this.#rnpKeyHandle &&
+ RNPLib.getSecretAvailableFromHandle(this.#rnpKeyHandle)
+ );
+ }
+
+ /**
+ * Check that this tracker has a reference to a valid private key.
+ * The check will fail e.g. for offline secret keys, where a
+ * primary key is marked as being a secret key, but not having
+ * the raw key data available. (In that scenario, the raw key data
+ * for subkeys is usually available.)
+ *
+ * @returns {boolean} - true if the tracked key is a secret/private
+ * key with its key material available.
+ */
+ available() {
+ return (
+ this.#rnpKeyHandle &&
+ RNPLib.getSecretAvailableFromHandle(this.#rnpKeyHandle) &&
+ RNPLib.isSecretKeyMaterialAvailable(this.#rnpKeyHandle)
+ );
+ }
+
+ /**
+ * Obtain the raw RNP key handle managed by this tracker.
+ * The returned handle may be temporarily used by the caller,
+ * but the caller must not destroy the handle. The returned handle
+ * will become invalid as soon as the release() function is called
+ * on this tracker object.
+ *
+ * @returns {rnp_key_handle_t} - the handle of the tracked private key
+ * or null, if no key is tracked by this tracker.
+ */
+ getHandle() {
+ return this.#rnpKeyHandle;
+ }
+
+ /**
+ * @returns {string} - key fingerprint of the tracked key, or the
+ * empty string.
+ */
+ getFingerprint() {
+ return this.#fingerprint;
+ }
+
+ /**
+ * @returns {boolean} - true if the tracked key is currently unlocked.
+ */
+ isUnlocked() {
+ return !this.#isLocked;
+ }
+
+ /**
+ * Protect the key with the automatic passphrase mechanism, that is,
+ * using the classic mechanism that uses an automatically generated
+ * passphrase, which is either unprotected, or protected by the
+ * primary password.
+ * Requires that the key is unlocked already.
+ */
+ async setAutoPassphrase() {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ let autoPassword = await lazy.OpenPGPMasterpass.retrieveOpenPGPPassword();
+ if (
+ RNPLib.rnp_key_protect(
+ this.#rnpKeyHandle,
+ autoPassword,
+ null,
+ null,
+ null,
+ 0
+ )
+ ) {
+ throw new Error(`rnp_key_protect failed for ${this.#fingerprint}`);
+ }
+ }
+
+ /**
+ * Protect the key with the given passphrase.
+ * Requires that the key is unlocked already.
+ *
+ * @param {string} pass - protect the key with this passphrase
+ */
+ setPassphrase(pass) {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ if (RNPLib.rnp_key_protect(this.#rnpKeyHandle, pass, null, null, null, 0)) {
+ throw new Error(`rnp_key_protect failed for ${this.#fingerprint}`);
+ }
+ }
+
+ /**
+ * Release all data managed by this tracker, if necessary locking the
+ * tracked private key, forgetting the remembered unlock password,
+ * and destroying the handle.
+ * Note that data passed on to a password cache isn't released.
+ */
+ release() {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ this.#unlockPassword = null;
+ if (!this.#isLocked && this.#wasUnlocked) {
+ RNPLib.rnp_key_lock(this.#rnpKeyHandle);
+ this.#isLocked = true;
+ }
+
+ RNPLib.rnp_key_handle_destroy(this.#rnpKeyHandle);
+ this.#rnpKeyHandle = null;
+ }
+}
+
+var RNP = {
+ hasRan: false,
+ libLoaded: false,
+ async once() {
+ this.hasRan = true;
+ try {
+ RNPLib = lazy.RNPLibLoader.init();
+ if (!RNPLib || !RNPLib.loaded) {
+ return;
+ }
+ if (await RNPLib.init()) {
+ //this.initUiOps();
+ RNP.libLoaded = true;
+ }
+ await lazy.OpenPGPMasterpass.ensurePasswordIsCached();
+ } catch (e) {
+ console.log(e);
+ }
+ },
+
+ getRNPLibStatus() {
+ return RNPLib.getRNPLibStatus();
+ },
+
+ async init(opts) {
+ opts = opts || {};
+
+ if (!this.hasRan) {
+ await this.once();
+ }
+
+ return RNP.libLoaded;
+ },
+
+ isAllowedPublicKeyAlgo(algo) {
+ // see rnp/src/lib/rnp.cpp pubkey_alg_map
+ switch (algo) {
+ case "SM2":
+ return false;
+
+ default:
+ return true;
+ }
+ },
+
+ /**
+ * returns {integer} - the raw value of the key's creation date
+ */
+ getKeyCreatedValueFromHandle(handle) {
+ let key_creation = new lazy.ctypes.uint32_t();
+ if (RNPLib.rnp_key_get_creation(handle, key_creation.address())) {
+ throw new Error("rnp_key_get_creation failed");
+ }
+ return key_creation.value;
+ },
+
+ addKeyAttributes(handle, meta, keyObj, is_subkey, forListing) {
+ let algo = new lazy.ctypes.char.ptr();
+ let bits = new lazy.ctypes.uint32_t();
+ let key_expiration = new lazy.ctypes.uint32_t();
+ let allowed = new lazy.ctypes.bool();
+
+ keyObj.secretAvailable = this.getSecretAvailableFromHandle(handle);
+
+ if (keyObj.secretAvailable) {
+ keyObj.secretMaterial = RNPLib.isSecretKeyMaterialAvailable(handle);
+ } else {
+ keyObj.secretMaterial = false;
+ }
+
+ if (is_subkey) {
+ keyObj.type = "sub";
+ } else {
+ keyObj.type = "pub";
+ }
+
+ keyObj.keyId = this.getKeyIDFromHandle(handle);
+ if (forListing) {
+ keyObj.id = keyObj.keyId;
+ }
+
+ keyObj.fpr = this.getFingerprintFromHandle(handle);
+
+ if (RNPLib.rnp_key_get_alg(handle, algo.address())) {
+ throw new Error("rnp_key_get_alg failed");
+ }
+ keyObj.algoSym = algo.readString();
+ RNPLib.rnp_buffer_destroy(algo);
+
+ if (RNPLib.rnp_key_get_bits(handle, bits.address())) {
+ throw new Error("rnp_key_get_bits failed");
+ }
+ keyObj.keySize = bits.value;
+
+ keyObj.keyCreated = this.getKeyCreatedValueFromHandle(handle);
+ keyObj.created = new Services.intl.DateTimeFormat().format(
+ new Date(keyObj.keyCreated * 1000)
+ );
+
+ if (RNPLib.rnp_key_get_expiration(handle, key_expiration.address())) {
+ throw new Error("rnp_key_get_expiration failed");
+ }
+ if (key_expiration.value > 0) {
+ keyObj.expiryTime = keyObj.keyCreated + key_expiration.value;
+ } else {
+ keyObj.expiryTime = 0;
+ }
+ keyObj.expiry = keyObj.expiryTime
+ ? new Services.intl.DateTimeFormat().format(
+ new Date(keyObj.expiryTime * 1000)
+ )
+ : "";
+ keyObj.keyUseFor = "";
+
+ if (!this.isAllowedPublicKeyAlgo(keyObj.algoSym)) {
+ return;
+ }
+
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+
+ if (key_revoked.value) {
+ keyObj.keyTrust = "r";
+ if (forListing) {
+ keyObj.revoke = true;
+ }
+ } else if (this.isExpiredTime(keyObj.expiryTime)) {
+ keyObj.keyTrust = "e";
+ } else if (keyObj.secretAvailable) {
+ keyObj.keyTrust = "u";
+ } else {
+ keyObj.keyTrust = "o";
+ }
+
+ if (RNPLib.rnp_key_allows_usage(handle, str_encrypt, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "e";
+ meta.e = true;
+ }
+ if (RNPLib.rnp_key_allows_usage(handle, str_sign, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "s";
+ meta.s = true;
+ }
+ if (RNPLib.rnp_key_allows_usage(handle, str_certify, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "c";
+ meta.c = true;
+ }
+ if (
+ RNPLib.rnp_key_allows_usage(handle, str_authenticate, allowed.address())
+ ) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "a";
+ meta.a = true;
+ }
+ },
+
+ async getKeys(onlyKeys = null) {
+ return this.getKeysFromFFI(RNPLib.ffi, false, onlyKeys, false);
+ },
+
+ async getSecretKeys(onlyKeys = null) {
+ return this.getKeysFromFFI(RNPLib.ffi, false, onlyKeys, true);
+ },
+
+ getProtectedKeysCount() {
+ return RNPLib.getProtectedKeysCount();
+ },
+
+ async protectUnprotectedKeys() {
+ return RNPLib.protectUnprotectedKeys();
+ },
+
+ /**
+ * This function inspects the keys contained in the RNP space "ffi",
+ * and returns objects of type KeyObj that describe the keys.
+ *
+ * Some consumers want a different listing of keys, and expect
+ * slightly different attribute names.
+ * If forListing is true, we'll set those additional attributes.
+ * If onlyKeys is given: only returns keys in that array.
+ *
+ * @param {rnp_ffi_t} ffi - RNP library handle to key storage area
+ * @param {boolean} forListing - Request additional attributes
+ * in the returned objects, for backwards compatibility.
+ * @param {string[]} onlyKeys - An array of key IDs or fingerprints.
+ * If non-null, only the given elements will be returned.
+ * If null, all elements are returned.
+ * @param {boolean} onlySecret - If true, only information for
+ * available secret keys is returned.
+ * @param {boolean} withPubKey - If true, an additional attribute
+ * "pubKey" will be added to each returned KeyObj, which will
+ * contain an ascii armor copy of the public key.
+ * @returns {KeyObj[]} - An array of KeyObj objects that describe the
+ * available keys.
+ */
+ async getKeysFromFFI(
+ ffi,
+ forListing,
+ onlyKeys = null,
+ onlySecret = false,
+ withPubKey = false
+ ) {
+ if (!!onlyKeys && onlySecret) {
+ throw new Error(
+ "filtering by both white list and only secret keys isn't supported"
+ );
+ }
+
+ let keys = [];
+
+ if (onlyKeys) {
+ for (let ki = 0; ki < onlyKeys.length; ki++) {
+ let handle = await this.getKeyHandleByIdentifier(ffi, onlyKeys[ki]);
+ if (!handle || handle.isNull()) {
+ continue;
+ }
+
+ let keyObj = {};
+ try {
+ // Skip if it is a sub key, it will be processed together with primary key later.
+ let ok = this.getKeyInfoFromHandle(
+ ffi,
+ handle,
+ keyObj,
+ false,
+ forListing,
+ false
+ );
+ if (!ok) {
+ continue;
+ }
+ } catch (ex) {
+ console.log(ex);
+ } finally {
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ if (keyObj) {
+ if (withPubKey) {
+ let pubKey = await this.getPublicKey("0x" + keyObj.id, ffi);
+ if (pubKey) {
+ keyObj.pubKey = pubKey;
+ }
+ }
+ keys.push(keyObj);
+ }
+ }
+ } else {
+ let rv;
+
+ let iter = new RNPLib.rnp_identifier_iterator_t();
+ let grip = new lazy.ctypes.char.ptr();
+
+ rv = RNPLib.rnp_identifier_iterator_create(ffi, iter.address(), "grip");
+ if (rv) {
+ return null;
+ }
+
+ while (!RNPLib.rnp_identifier_iterator_next(iter, grip.address())) {
+ if (grip.isNull()) {
+ break;
+ }
+
+ let handle = new RNPLib.rnp_key_handle_t();
+
+ if (RNPLib.rnp_locate_key(ffi, "grip", grip, handle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+
+ let keyObj = {};
+ try {
+ if (RNP.isBadKey(handle, null, ffi)) {
+ continue;
+ }
+
+ // Skip if it is a sub key, it will be processed together with primary key later.
+ if (
+ !this.getKeyInfoFromHandle(
+ ffi,
+ handle,
+ keyObj,
+ false,
+ forListing,
+ onlySecret
+ )
+ ) {
+ continue;
+ }
+ } catch (ex) {
+ console.log(ex);
+ } finally {
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ if (keyObj) {
+ if (withPubKey) {
+ let pubKey = await this.getPublicKey("0x" + keyObj.id, ffi);
+ if (pubKey) {
+ keyObj.pubKey = pubKey;
+ }
+ }
+ keys.push(keyObj);
+ }
+ }
+ RNPLib.rnp_identifier_iterator_destroy(iter);
+ }
+ return keys;
+ },
+
+ getFingerprintFromHandle(handle) {
+ let fingerprint = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_fprint(handle, fingerprint.address())) {
+ throw new Error("rnp_key_get_fprint failed");
+ }
+ let result = fingerprint.readString();
+ RNPLib.rnp_buffer_destroy(fingerprint);
+ return result;
+ },
+
+ getKeyIDFromHandle(handle) {
+ let ctypes_key_id = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_keyid(handle, ctypes_key_id.address())) {
+ throw new Error("rnp_key_get_keyid failed");
+ }
+ let result = ctypes_key_id.readString();
+ RNPLib.rnp_buffer_destroy(ctypes_key_id);
+ return result;
+ },
+
+ getSecretAvailableFromHandle(handle) {
+ return RNPLib.getSecretAvailableFromHandle(handle);
+ },
+
+ // We already know sub_handle is a subkey
+ getPrimaryKeyHandleFromSub(ffi, sub_handle) {
+ let newHandle = new RNPLib.rnp_key_handle_t();
+ // test my expectation is correct
+ if (!newHandle.isNull()) {
+ throw new Error("unexpected, new handle isn't null");
+ }
+ let primary_grip = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_primary_grip(sub_handle, primary_grip.address())) {
+ throw new Error("rnp_key_get_primary_grip failed");
+ }
+ if (primary_grip.isNull()) {
+ return newHandle;
+ }
+ if (RNPLib.rnp_locate_key(ffi, "grip", primary_grip, newHandle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+ return newHandle;
+ },
+
+ // We don't know if handle is a subkey. If it's not, return null handle
+ getPrimaryKeyHandleIfSub(ffi, handle) {
+ let is_subkey = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_sub(handle, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (!is_subkey.value) {
+ let nullHandle = new RNPLib.rnp_key_handle_t();
+ // test my expectation is correct
+ if (!nullHandle.isNull()) {
+ throw new Error("unexpected, new handle isn't null");
+ }
+ return nullHandle;
+ }
+ return this.getPrimaryKeyHandleFromSub(ffi, handle);
+ },
+
+ hasKeyWeakSelfSignature(selfId, handle) {
+ let sig_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_signature_count(handle, sig_count.address())) {
+ throw new Error("rnp_key_get_signature_count failed");
+ }
+
+ let hasWeak = false;
+ for (let i = 0; !hasWeak && i < sig_count.value; i++) {
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+
+ if (RNPLib.rnp_key_get_signature_at(handle, i, sig_handle.address())) {
+ throw new Error("rnp_key_get_signature_at failed");
+ }
+
+ hasWeak = RNP.isWeakSelfSignature(selfId, sig_handle);
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+ }
+ return hasWeak;
+ },
+
+ isWeakSelfSignature(selfId, sig_handle) {
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+
+ let sigId = sig_id_str.readString();
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+
+ // Is it a self-signature?
+ if (sigId != selfId) {
+ return false;
+ }
+
+ let creation = new lazy.ctypes.uint32_t();
+ if (RNPLib.rnp_signature_get_creation(sig_handle, creation.address())) {
+ throw new Error("rnp_signature_get_creation failed");
+ }
+
+ let hash_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_hash_alg(sig_handle, hash_str.address())) {
+ throw new Error("rnp_signature_get_hash_alg failed");
+ }
+
+ let creation64 = new lazy.ctypes.uint64_t();
+ creation64.value = creation.value;
+
+ let level = new lazy.ctypes.uint32_t();
+
+ if (
+ RNPLib.rnp_get_security_rule(
+ RNPLib.ffi,
+ RNPLib.RNP_FEATURE_HASH_ALG,
+ hash_str,
+ creation64,
+ null,
+ null,
+ level.address()
+ )
+ ) {
+ throw new Error("rnp_get_security_rule failed");
+ }
+
+ RNPLib.rnp_buffer_destroy(hash_str);
+ return level.value < RNPLib.RNP_SECURITY_DEFAULT;
+ },
+
+ // return false if handle refers to subkey and should be ignored
+ getKeyInfoFromHandle(
+ ffi,
+ handle,
+ keyObj,
+ usePrimaryIfSubkey,
+ forListing,
+ onlyIfSecret
+ ) {
+ keyObj.ownerTrust = null;
+ keyObj.userId = null;
+ keyObj.userIds = [];
+ keyObj.subKeys = [];
+ keyObj.photoAvailable = false;
+ keyObj.hasIgnoredAttributes = false;
+
+ let is_subkey = new lazy.ctypes.bool();
+ let sub_count = new lazy.ctypes.size_t();
+ let uid_count = new lazy.ctypes.size_t();
+
+ if (RNPLib.rnp_key_is_sub(handle, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (is_subkey.value) {
+ if (!usePrimaryIfSubkey) {
+ return false;
+ }
+ let rv = false;
+ let newHandle = this.getPrimaryKeyHandleFromSub(ffi, handle);
+ if (!newHandle.isNull()) {
+ // recursively call ourselves to get primary key info
+ rv = this.getKeyInfoFromHandle(
+ ffi,
+ newHandle,
+ keyObj,
+ false,
+ forListing,
+ onlyIfSecret
+ );
+ RNPLib.rnp_key_handle_destroy(newHandle);
+ }
+ return rv;
+ }
+
+ if (onlyIfSecret) {
+ let have_secret = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_have_secret(handle, have_secret.address())) {
+ throw new Error("rnp_key_have_secret failed");
+ }
+ if (!have_secret.value) {
+ return false;
+ }
+ }
+
+ let meta = {
+ a: false,
+ s: false,
+ c: false,
+ e: false,
+ };
+ this.addKeyAttributes(handle, meta, keyObj, false, forListing);
+
+ let hasAnySecretKey = keyObj.secretAvailable;
+
+ /* The remaining actions are done for primary keys, only. */
+ if (!is_subkey.value) {
+ if (RNPLib.rnp_key_get_uid_count(handle, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+ let firstValidUid = null;
+ for (let i = 0; i < uid_count.value; i++) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+
+ if (RNPLib.rnp_key_get_uid_handle_at(handle, i, uid_handle.address())) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+
+ // Never allow revoked user IDs
+ let uidOkToUse = !this.isRevokedUid(uid_handle);
+ if (uidOkToUse) {
+ // Usually, we don't allow user IDs reported as not valid
+ uidOkToUse = !this.isBadUid(uid_handle);
+
+ let { hasGoodSignature, hasWeakSignature } =
+ this.getUidSignatureQuality(keyObj.keyId, uid_handle);
+
+ if (hasWeakSignature) {
+ keyObj.hasIgnoredAttributes = true;
+ }
+
+ if (!uidOkToUse && keyObj.keyTrust == "e") {
+ // However, a user might be not valid, because it has
+ // expired. If the primary key has expired, we should show
+ // some user ID, even if all user IDs have expired,
+ // otherwise the user cannot see any text description.
+ // We allow showing user IDs with a good self-signature.
+ uidOkToUse = hasGoodSignature;
+ }
+ }
+
+ if (uidOkToUse) {
+ let uid_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_uid_at(handle, i, uid_str.address())) {
+ throw new Error("rnp_key_get_uid_at failed");
+ }
+ let userIdStr = uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(uid_str);
+
+ if (userIdStr !== RNP_PHOTO_USERID_ID) {
+ if (!firstValidUid) {
+ firstValidUid = userIdStr;
+ }
+
+ if (!keyObj.userId && this.isPrimaryUid(uid_handle)) {
+ keyObj.userId = userIdStr;
+ }
+
+ let uidObj = {};
+ uidObj.userId = userIdStr;
+ uidObj.type = "uid";
+ uidObj.keyTrust = keyObj.keyTrust;
+ uidObj.uidFpr = "??fpr??";
+
+ keyObj.userIds.push(uidObj);
+ }
+ }
+
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+
+ if (!keyObj.userId && firstValidUid) {
+ // No user ID marked as primary, so let's use the first valid.
+ keyObj.userId = firstValidUid;
+ }
+
+ if (!keyObj.userId) {
+ keyObj.userId = "?";
+ }
+
+ if (forListing) {
+ keyObj.name = keyObj.userId;
+ }
+
+ if (RNPLib.rnp_key_get_subkey_count(handle, sub_count.address())) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_key_get_subkey_at(handle, i, sub_handle.address())) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ if (RNP.hasKeyWeakSelfSignature(keyObj.keyId, sub_handle)) {
+ keyObj.hasIgnoredAttributes = true;
+ }
+
+ if (!RNP.isBadKey(sub_handle, handle, null)) {
+ let subKeyObj = {};
+ this.addKeyAttributes(sub_handle, meta, subKeyObj, true, forListing);
+ keyObj.subKeys.push(subKeyObj);
+ hasAnySecretKey = hasAnySecretKey || subKeyObj.secretAvailable;
+ }
+
+ RNPLib.rnp_key_handle_destroy(sub_handle);
+ }
+
+ let haveNonExpiringEncryptionKey = false;
+ let haveNonExpiringSigningKey = false;
+
+ let effectiveEncryptionExpiry = keyObj.expiry;
+ let effectiveSigningExpiry = keyObj.expiry;
+ let effectiveEncryptionExpiryTime = keyObj.expiryTime;
+ let effectiveSigningExpiryTime = keyObj.expiryTime;
+
+ if (keyObj.keyUseFor.match(/e/) && !keyObj.expiryTime) {
+ haveNonExpiringEncryptionKey = true;
+ }
+
+ if (keyObj.keyUseFor.match(/s/) && !keyObj.expiryTime) {
+ haveNonExpiringSigningKey = true;
+ }
+
+ let mostFutureEncExpiryTime = 0;
+ let mostFutureSigExpiryTime = 0;
+ let mostFutureEncExpiry = "";
+ let mostFutureSigExpiry = "";
+
+ for (let aSub of keyObj.subKeys) {
+ if (aSub.keyTrust == "r") {
+ continue;
+ }
+
+ // Expiring subkeys may shorten the effective expiry,
+ // unless the primary key is non-expiring and can be used
+ // for a purpose.
+ // Subkeys cannot extend the expiry beyond the primary key's.
+
+ // Strategy: If we don't have a non-expiring usable primary key,
+ // then find the usable subkey that has the most future
+ // expiration date. Stop searching is a non-expiring subkey
+ // is found. Then compare with primary key expiry.
+
+ if (!haveNonExpiringEncryptionKey && aSub.keyUseFor.match(/e/)) {
+ if (!aSub.expiryTime) {
+ haveNonExpiringEncryptionKey = true;
+ } else if (!mostFutureEncExpiryTime) {
+ mostFutureEncExpiryTime = aSub.expiryTime;
+ mostFutureEncExpiry = aSub.expiry;
+ } else if (aSub.expiryTime > mostFutureEncExpiryTime) {
+ mostFutureEncExpiryTime = aSub.expiryTime;
+ mostFutureEncExpiry = aSub.expiry;
+ }
+ }
+
+ // We only need to calculate the effective signing expiration
+ // if it's about a personal key (we require both signing and
+ // encryption capability).
+ if (
+ hasAnySecretKey &&
+ !haveNonExpiringSigningKey &&
+ aSub.keyUseFor.match(/s/)
+ ) {
+ if (!aSub.expiryTime) {
+ haveNonExpiringSigningKey = true;
+ } else if (!mostFutureSigExpiryTime) {
+ mostFutureSigExpiryTime = aSub.expiryTime;
+ mostFutureSigExpiry = aSub.expiry;
+ } else if (aSub.expiryTime > mostFutureEncExpiryTime) {
+ mostFutureSigExpiryTime = aSub.expiryTime;
+ mostFutureSigExpiry = aSub.expiry;
+ }
+ }
+ }
+
+ if (
+ !haveNonExpiringEncryptionKey &&
+ mostFutureEncExpiryTime &&
+ (!keyObj.expiryTime || mostFutureEncExpiryTime < keyObj.expiryTime)
+ ) {
+ effectiveEncryptionExpiryTime = mostFutureEncExpiryTime;
+ effectiveEncryptionExpiry = mostFutureEncExpiry;
+ }
+
+ if (
+ !haveNonExpiringSigningKey &&
+ mostFutureSigExpiryTime &&
+ (!keyObj.expiryTime || mostFutureSigExpiryTime < keyObj.expiryTime)
+ ) {
+ effectiveSigningExpiryTime = mostFutureSigExpiryTime;
+ effectiveSigningExpiry = mostFutureSigExpiry;
+ }
+
+ if (!hasAnySecretKey) {
+ keyObj.effectiveExpiryTime = effectiveEncryptionExpiryTime;
+ keyObj.effectiveExpiry = effectiveEncryptionExpiry;
+ } else {
+ let effectiveSignOrEncExpiry = "";
+ let effectiveSignOrEncExpiryTime = 0;
+
+ if (!effectiveEncryptionExpiryTime) {
+ if (effectiveSigningExpiryTime) {
+ effectiveSignOrEncExpiryTime = effectiveSigningExpiryTime;
+ effectiveSignOrEncExpiry = effectiveSigningExpiry;
+ }
+ } else if (!effectiveSigningExpiryTime) {
+ effectiveSignOrEncExpiryTime = effectiveEncryptionExpiryTime;
+ effectiveSignOrEncExpiry = effectiveEncryptionExpiry;
+ } else if (effectiveSigningExpiryTime < effectiveEncryptionExpiryTime) {
+ effectiveSignOrEncExpiryTime = effectiveSigningExpiryTime;
+ effectiveSignOrEncExpiry = effectiveSigningExpiry;
+ } else {
+ effectiveSignOrEncExpiryTime = effectiveEncryptionExpiryTime;
+ effectiveSignOrEncExpiry = effectiveEncryptionExpiry;
+ }
+
+ keyObj.effectiveExpiryTime = effectiveSignOrEncExpiryTime;
+ keyObj.effectiveExpiry = effectiveSignOrEncExpiry;
+ }
+
+ if (meta.s) {
+ keyObj.keyUseFor += "S";
+ }
+ if (meta.a) {
+ keyObj.keyUseFor += "A";
+ }
+ if (meta.c) {
+ keyObj.keyUseFor += "C";
+ }
+ if (meta.e) {
+ keyObj.keyUseFor += "E";
+ }
+
+ if (RNP.hasKeyWeakSelfSignature(keyObj.keyId, handle)) {
+ keyObj.hasIgnoredAttributes = true;
+ }
+ }
+
+ return true;
+ },
+
+ /*
+ // We don't need these functions currently, but it's helpful
+ // information that I'd like to keep around as documentation.
+
+ isUInt64WithinBounds(val) {
+ // JS integers are limited to 53 bits precision.
+ // Numbers smaller than 2^53 -1 are safe to use.
+ // (For comparison, that's 8192 TB or 8388608 GB).
+ const num53BitsMinus1 = ctypes.UInt64("0x1fffffffffffff");
+ return ctypes.UInt64.compare(val, num53BitsMinus1) < 0;
+ },
+
+ isUInt64Max(val) {
+ // 2^64-1, 18446744073709551615
+ const max = ctypes.UInt64("0xffffffffffffffff");
+ return ctypes.UInt64.compare(val, max) == 0;
+ },
+ */
+
+ isBadKey(handle, knownPrimaryKey, knownContextFFI) {
+ let validTill64 = new lazy.ctypes.uint64_t();
+ if (RNPLib.rnp_key_valid_till64(handle, validTill64.address())) {
+ throw new Error("rnp_key_valid_till64 failed");
+ }
+
+ // For the purpose of this function, we define bad as: there isn't
+ // any valid self-signature on the key, and thus the key should
+ // be completely avoided.
+ // In this scenario, zero is returned. In other words,
+ // if a non-zero value is returned, we know the key isn't completely
+ // bad according to our definition.
+
+ // ctypes.uint64_t().value is of type ctypes.UInt64
+
+ if (
+ lazy.ctypes.UInt64.compare(validTill64.value, lazy.ctypes.UInt64("0")) > 0
+ ) {
+ return false;
+ }
+
+ // If zero was returned, it could potentially have been revoked.
+ // If it was revoked, we don't treat is as generally bad,
+ // to allow importing it and to consume the revocation information.
+ // If the key was not revoked, then treat it as a bad key.
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+
+ if (!key_revoked.value) {
+ // Also check if the primary key was revoked. If the primary key
+ // is revoked, the subkey is considered revoked, too.
+ if (knownPrimaryKey) {
+ if (RNPLib.rnp_key_is_revoked(knownPrimaryKey, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+ } else if (knownContextFFI) {
+ let primaryHandle = this.getPrimaryKeyHandleIfSub(
+ knownContextFFI,
+ handle
+ );
+ if (!primaryHandle.isNull()) {
+ if (RNPLib.rnp_key_is_revoked(primaryHandle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+ RNPLib.rnp_key_handle_destroy(primaryHandle);
+ }
+ }
+ }
+
+ return !key_revoked.value;
+ },
+
+ isPrimaryUid(uid_handle) {
+ let is_primary = new lazy.ctypes.bool();
+
+ if (RNPLib.rnp_uid_is_primary(uid_handle, is_primary.address())) {
+ throw new Error("rnp_uid_is_primary failed");
+ }
+
+ return is_primary.value;
+ },
+
+ getUidSignatureQuality(self_key_id, uid_handle) {
+ let result = {
+ hasGoodSignature: false,
+ hasWeakSignature: false,
+ };
+
+ let sig_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_uid_get_signature_count(uid_handle, sig_count.address())) {
+ throw new Error("rnp_uid_get_signature_count failed");
+ }
+
+ for (let i = 0; i < sig_count.value; i++) {
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+
+ if (
+ RNPLib.rnp_uid_get_signature_at(uid_handle, i, sig_handle.address())
+ ) {
+ throw new Error("rnp_uid_get_signature_at failed");
+ }
+
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+
+ if (sig_id_str.readString() == self_key_id) {
+ if (!result.hasGoodSignature) {
+ let sig_validity = RNPLib.rnp_signature_is_valid(sig_handle, 0);
+ result.hasGoodSignature =
+ sig_validity == RNPLib.RNP_SUCCESS ||
+ sig_validity == RNPLib.RNP_ERROR_SIGNATURE_EXPIRED;
+ }
+
+ if (!result.hasWeakSignature) {
+ result.hasWeakSignature = RNP.isWeakSelfSignature(
+ self_key_id,
+ sig_handle
+ );
+ }
+ }
+
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+ }
+
+ return result;
+ },
+
+ isBadUid(uid_handle) {
+ let is_valid = new lazy.ctypes.bool();
+
+ if (RNPLib.rnp_uid_is_valid(uid_handle, is_valid.address())) {
+ throw new Error("rnp_uid_is_valid failed");
+ }
+
+ return !is_valid.value;
+ },
+
+ isRevokedUid(uid_handle) {
+ let is_revoked = new lazy.ctypes.bool();
+
+ if (RNPLib.rnp_uid_is_revoked(uid_handle, is_revoked.address())) {
+ throw new Error("rnp_uid_is_revoked failed");
+ }
+
+ return is_revoked.value;
+ },
+
+ getKeySignatures(keyId, ignoreUnknownUid) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + keyId
+ );
+ if (handle.isNull()) {
+ return null;
+ }
+
+ let mainKeyObj = {};
+ this.getKeyInfoFromHandle(
+ RNPLib.ffi,
+ handle,
+ mainKeyObj,
+ false,
+ true,
+ false
+ );
+
+ let result = RNP._getSignatures(mainKeyObj, handle, ignoreUnknownUid);
+ RNPLib.rnp_key_handle_destroy(handle);
+ return result;
+ },
+
+ getKeyObjSignatures(keyObj, ignoreUnknownUid) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + keyObj.keyId
+ );
+ if (handle.isNull()) {
+ return null;
+ }
+
+ let result = RNP._getSignatures(keyObj, handle, ignoreUnknownUid);
+ RNPLib.rnp_key_handle_destroy(handle);
+ return result;
+ },
+
+ _getSignatures(keyObj, handle, ignoreUnknownUid) {
+ let rList = [];
+
+ try {
+ let uid_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_uid_count(handle, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+ let outputIndex = 0;
+ for (let i = 0; i < uid_count.value; i++) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+
+ if (RNPLib.rnp_key_get_uid_handle_at(handle, i, uid_handle.address())) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+
+ if (!this.isBadUid(uid_handle) && !this.isRevokedUid(uid_handle)) {
+ let uid_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_uid_at(handle, i, uid_str.address())) {
+ throw new Error("rnp_key_get_uid_at failed");
+ }
+ let userIdStr = uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(uid_str);
+
+ if (userIdStr !== RNP_PHOTO_USERID_ID) {
+ let id = outputIndex;
+ ++outputIndex;
+
+ let subList = {};
+
+ subList = {};
+ subList.keyCreated = keyObj.keyCreated;
+ subList.created = keyObj.created;
+ subList.fpr = keyObj.fpr;
+ subList.keyId = keyObj.keyId;
+
+ subList.userId = userIdStr;
+ subList.sigList = [];
+
+ let sig_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_uid_get_signature_count(
+ uid_handle,
+ sig_count.address()
+ )
+ ) {
+ throw new Error("rnp_uid_get_signature_count failed");
+ }
+ for (let j = 0; j < sig_count.value; j++) {
+ let sigObj = {};
+
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+ if (
+ RNPLib.rnp_uid_get_signature_at(
+ uid_handle,
+ j,
+ sig_handle.address()
+ )
+ ) {
+ throw new Error("rnp_uid_get_signature_at failed");
+ }
+
+ let creation = new lazy.ctypes.uint32_t();
+ if (
+ RNPLib.rnp_signature_get_creation(
+ sig_handle,
+ creation.address()
+ )
+ ) {
+ throw new Error("rnp_signature_get_creation failed");
+ }
+ sigObj.keyCreated = creation.value;
+ sigObj.created = new Services.intl.DateTimeFormat().format(
+ new Date(sigObj.keyCreated * 1000)
+ );
+ sigObj.sigType = "?";
+
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (
+ RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())
+ ) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+
+ let sigIdStr = sig_id_str.readString();
+ sigObj.signerKeyId = sigIdStr;
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+
+ let signerHandle = new RNPLib.rnp_key_handle_t();
+
+ if (
+ RNPLib.rnp_signature_get_signer(
+ sig_handle,
+ signerHandle.address()
+ )
+ ) {
+ throw new Error("rnp_signature_get_signer failed");
+ }
+
+ if (
+ signerHandle.isNull() ||
+ this.isBadKey(signerHandle, null, RNPLib.ffi)
+ ) {
+ if (!ignoreUnknownUid) {
+ sigObj.userId = "?";
+ sigObj.sigKnown = false;
+ subList.sigList.push(sigObj);
+ }
+ } else {
+ let signer_uid_str = new lazy.ctypes.char.ptr();
+ if (
+ RNPLib.rnp_key_get_primary_uid(
+ signerHandle,
+ signer_uid_str.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_primary_uid failed");
+ }
+ sigObj.userId = signer_uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(signer_uid_str);
+ sigObj.sigKnown = true;
+ subList.sigList.push(sigObj);
+ RNPLib.rnp_key_handle_destroy(signerHandle);
+ }
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+ }
+ rList[id] = subList;
+ }
+ }
+
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+ } catch (ex) {
+ console.log(ex);
+ }
+ return rList;
+ },
+
+ policyForbidsAlg(alg) {
+ // TODO: implement policy
+ // Currently, all algorithms are allowed
+ return false;
+ },
+
+ getKeyIdsFromRecipHandle(recip_handle, resultRecipAndPrimary) {
+ resultRecipAndPrimary.keyId = "";
+ resultRecipAndPrimary.primaryKeyId = "";
+
+ let c_key_id = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_recipient_get_keyid(recip_handle, c_key_id.address())) {
+ throw new Error("rnp_recipient_get_keyid failed");
+ }
+ let recip_key_id = c_key_id.readString();
+ resultRecipAndPrimary.keyId = recip_key_id;
+ RNPLib.rnp_buffer_destroy(c_key_id);
+
+ let recip_key_handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + recip_key_id
+ );
+ if (!recip_key_handle.isNull()) {
+ let primary_signer_handle = this.getPrimaryKeyHandleIfSub(
+ RNPLib.ffi,
+ recip_key_handle
+ );
+ if (!primary_signer_handle.isNull()) {
+ resultRecipAndPrimary.primaryKeyId = this.getKeyIDFromHandle(
+ primary_signer_handle
+ );
+ RNPLib.rnp_key_handle_destroy(primary_signer_handle);
+ }
+ RNPLib.rnp_key_handle_destroy(recip_key_handle);
+ }
+ },
+
+ getCharCodeArray(pgpData) {
+ return pgpData.split("").map(e => e.charCodeAt());
+ },
+
+ is8Bit(charCodeArray) {
+ for (let i = 0; i < charCodeArray.length; i++) {
+ if (charCodeArray[i] > 255) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ removeCommentLines(str) {
+ const commentLine = /^Comment:.*(\r?\n|\r)/gm;
+ return str.replace(commentLine, "");
+ },
+
+ /**
+ * This function analyzes an encrypted message. It will check if one
+ * of the available secret keys can be used to decrypt a message,
+ * without actually performing the decryption.
+ * This is done by performing a decryption attempt in an empty
+ * environment, which doesn't have any keys available. The decryption
+ * attempt allows us to use the RNP APIs that list the key IDs of
+ * keys that would be able to decrypt the object.
+ * If a matching available secret ID is found, then the handle to that
+ * available secret key is returned.
+ *
+ * @param {rnp_input_t} - A prepared RNP input object that contains
+ * the encrypted message that should be analyzed.
+ * @returns {rnp_key_handle_t} - the handle of a private key that can
+ * be used to decrypt the message, or null, if no usable key was
+ * found.
+ */
+ getFirstAvailableDecryptionKeyHandle(encrypted_rnp_input_from_memory) {
+ let resultKey = null;
+
+ let dummyFfi = RNPLib.prepare_ffi();
+ if (!dummyFfi) {
+ return null;
+ }
+
+ const dummy_max_output_size = 1;
+ let dummy_output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(
+ dummy_output_to_memory.address(),
+ dummy_max_output_size
+ );
+
+ let dummy_verify_op = new RNPLib.rnp_op_verify_t();
+ RNPLib.rnp_op_verify_create(
+ dummy_verify_op.address(),
+ dummyFfi,
+ encrypted_rnp_input_from_memory,
+ dummy_output_to_memory
+ );
+
+ // It's expected and ok that this function returns an error,
+ // e.r. RNP_ERROR_NO_SUITABLE_KEY, we'll query detailed results.
+ RNPLib.rnp_op_verify_execute(dummy_verify_op);
+
+ let all_recip_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_count(
+ dummy_verify_op,
+ all_recip_count.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_count failed");
+ }
+
+ // Loop is skipped if all_recip_count is zero.
+ for (
+ let recip_i = 0;
+ recip_i < all_recip_count.value && !resultKey;
+ recip_i++
+ ) {
+ let recip_handle = new RNPLib.rnp_recipient_handle_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_at(
+ dummy_verify_op,
+ recip_i,
+ recip_handle.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_at failed");
+ }
+
+ let c_key_id = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_recipient_get_keyid(recip_handle, c_key_id.address())) {
+ throw new Error("rnp_recipient_get_keyid failed");
+ }
+ let recip_key_id = c_key_id.readString();
+ RNPLib.rnp_buffer_destroy(c_key_id);
+
+ let recip_key_handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + recip_key_id
+ );
+ if (!recip_key_handle.isNull()) {
+ if (
+ RNPLib.getSecretAvailableFromHandle(recip_key_handle) &&
+ RNPLib.isSecretKeyMaterialAvailable(recip_key_handle)
+ ) {
+ resultKey = recip_key_handle;
+ } else {
+ RNPLib.rnp_key_handle_destroy(recip_key_handle);
+ }
+ }
+ }
+
+ RNPLib.rnp_output_destroy(dummy_output_to_memory);
+ RNPLib.rnp_op_verify_destroy(dummy_verify_op);
+ RNPLib.rnp_ffi_destroy(dummyFfi);
+
+ return resultKey;
+ },
+
+ async decrypt(encrypted, options, alreadyDecrypted = false) {
+ let arr = encrypted.split("").map(e => e.charCodeAt());
+ var encrypted_array = lazy.ctypes.uint8_t.array()(arr);
+
+ let result = {};
+ result.decryptedData = "";
+ result.statusFlags = 0;
+ result.extStatusFlags = 0;
+
+ result.userId = "";
+ result.keyId = "";
+ result.encToDetails = {};
+ result.encToDetails.myRecipKey = {};
+ result.encToDetails.allRecipKeys = [];
+ result.sigDetails = {};
+ result.sigDetails.sigDate = null;
+
+ if (alreadyDecrypted) {
+ result.encToDetails = options.encToDetails;
+ }
+
+ // We cannot reuse the same rnp_input_t for both the dummy operation
+ // and the real decryption operation, as the rnp_input_t object
+ // apparently becomes unusable after operating on it.
+ // That's why we produce a separate rnp_input_t based on the same
+ // data for the dummy operation.
+ let dummy_input_from_memory = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ dummy_input_from_memory.address(),
+ encrypted_array,
+ encrypted_array.length,
+ false
+ );
+
+ let rnpCannotDecrypt = true;
+
+ let decryptKey = new RnpPrivateKeyUnlockTracker(
+ this.getFirstAvailableDecryptionKeyHandle(dummy_input_from_memory)
+ );
+
+ decryptKey.setAllowPromptingUserForPassword(true);
+ decryptKey.setAllowAutoUnlockWithCachedPasswords(true);
+
+ if (decryptKey.available()) {
+ // If the key cannot be automatically unlocked, we'll rely on
+ // the password prompt callback from RNP, and on the user to unlock.
+ await decryptKey.unlock();
+ }
+
+ // Even if we don't have a matching decryption key, run
+ // through full processing, to obtain all the various status flags,
+ // and because decryption might not be necessary.
+ try {
+ let input_from_memory = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ encrypted_array,
+ encrypted_array.length,
+ false
+ );
+
+ // Allow compressed encrypted messages, max factor 1200, up to 100 MiB.
+ const max_decrypted_message_size = 100 * 1024 * 1024;
+ let max_out = Math.min(
+ encrypted.length * 1200,
+ max_decrypted_message_size
+ );
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(output_to_memory.address(), max_out);
+
+ let verify_op = new RNPLib.rnp_op_verify_t();
+ // Apparently the exit code here is ignored (replaced below)
+ result.exitCode = RNPLib.rnp_op_verify_create(
+ verify_op.address(),
+ RNPLib.ffi,
+ input_from_memory,
+ output_to_memory
+ );
+
+ result.exitCode = RNPLib.rnp_op_verify_execute(verify_op);
+
+ rnpCannotDecrypt = false;
+ let queryAllEncryptionRecipients = false;
+ let stillUndecidedIfSignatureIsBad = false;
+
+ let useDecodedData;
+ let processSignature;
+ switch (result.exitCode) {
+ case RNPLib.RNP_SUCCESS:
+ useDecodedData = true;
+ processSignature = true;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_INVALID:
+ // Either the signing key is unavailable, or the signature is
+ // indeed bad. Must check signature status below.
+ stillUndecidedIfSignatureIsBad = true;
+ useDecodedData = true;
+ processSignature = true;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_EXPIRED:
+ useDecodedData = true;
+ processSignature = false;
+ result.statusFlags |= lazy.EnigmailConstants.EXPIRED_SIGNATURE;
+ break;
+ case RNPLib.RNP_ERROR_DECRYPT_FAILED:
+ rnpCannotDecrypt = true;
+ useDecodedData = false;
+ processSignature = false;
+ queryAllEncryptionRecipients = true;
+ result.statusFlags |= lazy.EnigmailConstants.DECRYPTION_FAILED;
+ break;
+ case RNPLib.RNP_ERROR_NO_SUITABLE_KEY:
+ rnpCannotDecrypt = true;
+ useDecodedData = false;
+ processSignature = false;
+ queryAllEncryptionRecipients = true;
+ result.statusFlags |=
+ lazy.EnigmailConstants.DECRYPTION_FAILED |
+ lazy.EnigmailConstants.NO_SECKEY;
+ break;
+ default:
+ useDecodedData = false;
+ processSignature = false;
+ console.debug(
+ "rnp_op_verify_execute returned unexpected: " + result.exitCode
+ );
+ break;
+ }
+
+ if (useDecodedData && alreadyDecrypted) {
+ result.statusFlags |= lazy.EnigmailConstants.DECRYPTION_OKAY;
+ } else if (useDecodedData && !alreadyDecrypted) {
+ let prot_mode_str = new lazy.ctypes.char.ptr();
+ let prot_cipher_str = new lazy.ctypes.char.ptr();
+ let prot_is_valid = new lazy.ctypes.bool();
+
+ if (
+ RNPLib.rnp_op_verify_get_protection_info(
+ verify_op,
+ prot_mode_str.address(),
+ prot_cipher_str.address(),
+ prot_is_valid.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_protection_info failed");
+ }
+ let mode = prot_mode_str.readString();
+ let cipher = prot_cipher_str.readString();
+ let validIntegrityProtection = prot_is_valid.value;
+
+ if (mode != "none") {
+ if (!validIntegrityProtection) {
+ useDecodedData = false;
+ result.statusFlags |=
+ lazy.EnigmailConstants.MISSING_MDC |
+ lazy.EnigmailConstants.DECRYPTION_FAILED;
+ } else if (mode == "null" || this.policyForbidsAlg(cipher)) {
+ // don't indicate decryption, because a non-protecting or insecure cipher was used
+ result.statusFlags |= lazy.EnigmailConstants.UNKNOWN_ALGO;
+ } else {
+ queryAllEncryptionRecipients = true;
+
+ let recip_handle = new RNPLib.rnp_recipient_handle_t();
+ let rv = RNPLib.rnp_op_verify_get_used_recipient(
+ verify_op,
+ recip_handle.address()
+ );
+ if (rv) {
+ throw new Error("rnp_op_verify_get_used_recipient failed");
+ }
+
+ let c_alg = new lazy.ctypes.char.ptr();
+ rv = RNPLib.rnp_recipient_get_alg(recip_handle, c_alg.address());
+ if (rv) {
+ throw new Error("rnp_recipient_get_alg failed");
+ }
+
+ if (this.policyForbidsAlg(c_alg.readString())) {
+ result.statusFlags |= lazy.EnigmailConstants.UNKNOWN_ALGO;
+ } else {
+ this.getKeyIdsFromRecipHandle(
+ recip_handle,
+ result.encToDetails.myRecipKey
+ );
+ result.statusFlags |= lazy.EnigmailConstants.DECRYPTION_OKAY;
+ }
+ }
+ }
+ }
+
+ if (queryAllEncryptionRecipients) {
+ let all_recip_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_count(
+ verify_op,
+ all_recip_count.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_count failed");
+ }
+ if (all_recip_count.value > 1) {
+ for (let recip_i = 0; recip_i < all_recip_count.value; recip_i++) {
+ let other_recip_handle = new RNPLib.rnp_recipient_handle_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_at(
+ verify_op,
+ recip_i,
+ other_recip_handle.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_at failed");
+ }
+ let encTo = {};
+ this.getKeyIdsFromRecipHandle(other_recip_handle, encTo);
+ result.encToDetails.allRecipKeys.push(encTo);
+ }
+ }
+ }
+
+ if (useDecodedData) {
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let rv = RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ // result_len is of type UInt64, I don't know of a better way
+ // to convert it to an integer.
+ let b_len = parseInt(result_len.value.toString());
+
+ if (!rv) {
+ // type casting the pointer type to an array type allows us to
+ // access the elements by index.
+ let uint8_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.uint8_t.array(result_len.value).ptr
+ ).contents;
+
+ let str = "";
+ for (let i = 0; i < b_len; i++) {
+ str += String.fromCharCode(uint8_array[i]);
+ }
+
+ result.decryptedData = str;
+ }
+
+ if (processSignature) {
+ // ignore "no signature" result, that's ok
+ await this.getVerifyDetails(
+ RNPLib.ffi,
+ options.fromAddr,
+ options.msgDate,
+ verify_op,
+ result
+ );
+
+ if (
+ (result.statusFlags &
+ (lazy.EnigmailConstants.GOOD_SIGNATURE |
+ lazy.EnigmailConstants.UNCERTAIN_SIGNATURE |
+ lazy.EnigmailConstants.EXPIRED_SIGNATURE |
+ lazy.EnigmailConstants.BAD_SIGNATURE)) !=
+ 0
+ ) {
+ // A decision was already made.
+ stillUndecidedIfSignatureIsBad = false;
+ }
+ }
+ }
+
+ if (stillUndecidedIfSignatureIsBad) {
+ // We didn't find more details above, so conclude it's bad.
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+ RNPLib.rnp_op_verify_destroy(verify_op);
+ } finally {
+ decryptKey.release();
+ RNPLib.rnp_input_destroy(dummy_input_from_memory);
+ }
+
+ if (
+ rnpCannotDecrypt &&
+ !alreadyDecrypted &&
+ Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg") &&
+ lazy.GPGME.allDependenciesLoaded()
+ ) {
+ // failure processing with RNP, attempt decryption with GPGME
+ let r2 = await lazy.GPGME.decrypt(
+ encrypted,
+ this.enArmorCDataMessage.bind(this)
+ );
+ if (!r2.exitCode && r2.decryptedData) {
+ // TODO: obtain info which key ID was used for decryption
+ // and set result.decryptKey*
+ // It isn't obvious how to do that with GPGME, because
+ // gpgme_op_decrypt_result provides the list of all the
+ // encryption keys, only.
+
+ // The result may still contain wrapping like compression,
+ // and optional signature data. Recursively call ourselves
+ // to perform the remaining processing.
+ options.encToDetails = result.encToDetails;
+ return RNP.decrypt(r2.decryptedData, options, true);
+ }
+ }
+
+ return result;
+ },
+
+ async getVerifyDetails(ffi, fromAddr, msgDate, verify_op, result) {
+ if (!fromAddr) {
+ // We cannot correctly verify without knowing the fromAddr.
+ // This scenario is reached when quoting an encrypted MIME part.
+ return false;
+ }
+
+ let sig_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_op_verify_get_signature_count(verify_op, sig_count.address())
+ ) {
+ throw new Error("rnp_op_verify_get_signature_count failed");
+ }
+
+ // TODO: How should handle (sig_count.value > 1) ?
+ if (sig_count.value == 0) {
+ // !sig_count.value didn't work, === also doesn't work
+ return false;
+ }
+
+ let sig = new RNPLib.rnp_op_verify_signature_t();
+ if (RNPLib.rnp_op_verify_get_signature_at(verify_op, 0, sig.address())) {
+ throw new Error("rnp_op_verify_get_signature_at failed");
+ }
+
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+ if (RNPLib.rnp_op_verify_signature_get_handle(sig, sig_handle.address())) {
+ throw new Error("rnp_op_verify_signature_get_handle failed");
+ }
+
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+ result.keyId = sig_id_str.readString();
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+
+ let sig_status = RNPLib.rnp_op_verify_signature_get_status(sig);
+ if (sig_status != RNPLib.RNP_SUCCESS && !result.exitCode) {
+ /* Don't allow a good exit code. Keep existing bad code. */
+ result.exitCode = -1;
+ }
+
+ let query_signer = true;
+
+ switch (sig_status) {
+ case RNPLib.RNP_SUCCESS:
+ result.statusFlags |= lazy.EnigmailConstants.GOOD_SIGNATURE;
+ break;
+ case RNPLib.RNP_ERROR_KEY_NOT_FOUND:
+ result.statusFlags |=
+ lazy.EnigmailConstants.UNCERTAIN_SIGNATURE |
+ lazy.EnigmailConstants.NO_PUBKEY;
+ query_signer = false;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_EXPIRED:
+ result.statusFlags |= lazy.EnigmailConstants.EXPIRED_SIGNATURE;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_INVALID:
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ break;
+ default:
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ query_signer = false;
+ break;
+ }
+
+ if (msgDate && result.statusFlags & lazy.EnigmailConstants.GOOD_SIGNATURE) {
+ let created = new lazy.ctypes.uint32_t();
+ let expires = new lazy.ctypes.uint32_t(); //relative
+
+ if (
+ RNPLib.rnp_op_verify_signature_get_times(
+ sig,
+ created.address(),
+ expires.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_signature_get_times failed");
+ }
+
+ result.sigDetails.sigDate = new Date(created.value * 1000);
+
+ let timeDelta;
+ if (result.sigDetails.sigDate > msgDate) {
+ timeDelta = result.sigDetails.sigDate - msgDate;
+ } else {
+ timeDelta = msgDate - result.sigDetails.sigDate;
+ }
+
+ if (timeDelta > 1000 * 60 * 60 * 1) {
+ result.statusFlags &= ~lazy.EnigmailConstants.GOOD_SIGNATURE;
+ result.statusFlags |= lazy.EnigmailConstants.MSG_SIG_INVALID;
+ }
+ }
+
+ let signer_key = new RNPLib.rnp_key_handle_t();
+ let have_signer_key = false;
+ let use_signer_key = false;
+
+ if (query_signer) {
+ if (RNPLib.rnp_op_verify_signature_get_key(sig, signer_key.address())) {
+ // If sig_status isn't RNP_ERROR_KEY_NOT_FOUND then we must
+ // be able to obtain the signer key.
+ throw new Error("rnp_op_verify_signature_get_key");
+ }
+
+ have_signer_key = true;
+ use_signer_key = !this.isBadKey(signer_key, null, RNPLib.ffi);
+ }
+
+ if (use_signer_key) {
+ let keyInfo = {};
+ let ok = this.getKeyInfoFromHandle(
+ ffi,
+ signer_key,
+ keyInfo,
+ true,
+ false,
+ false
+ );
+ if (!ok) {
+ throw new Error("getKeyInfoFromHandle failed");
+ }
+
+ let fromMatchesAnyUid = false;
+ let fromLower = fromAddr ? fromAddr.toLowerCase() : "";
+
+ for (let uid of keyInfo.userIds) {
+ if (uid.type !== "uid") {
+ continue;
+ }
+
+ if (
+ lazy.EnigmailFuncs.getEmailFromUserID(uid.userId).toLowerCase() ===
+ fromLower
+ ) {
+ fromMatchesAnyUid = true;
+ break;
+ }
+ }
+
+ let useUndecided = true;
+
+ if (keyInfo.secretAvailable) {
+ let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(
+ keyInfo.fpr
+ );
+ if (isPersonal && fromMatchesAnyUid) {
+ result.extStatusFlags |= lazy.EnigmailConstants.EXT_SELF_IDENTITY;
+ useUndecided = false;
+ } else {
+ result.statusFlags |= lazy.EnigmailConstants.INVALID_RECIPIENT;
+ useUndecided = true;
+ }
+ } else if (result.statusFlags & lazy.EnigmailConstants.GOOD_SIGNATURE) {
+ if (!fromMatchesAnyUid) {
+ /* At the time the user had accepted the key,
+ * a different set of email addresses might have been
+ * contained inside the key. In the meantime, we might
+ * have refreshed the key, a email addresses
+ * might have been removed or revoked.
+ * If the current from was removed/revoked, we'd still
+ * get an acceptance match, but the from is no longer found
+ * in the key's UID list. That should get "undecided".
+ */
+ result.statusFlags |= lazy.EnigmailConstants.INVALID_RECIPIENT;
+ useUndecided = true;
+ } else {
+ let acceptanceResult = {};
+ try {
+ await lazy.PgpSqliteDb2.getAcceptance(
+ keyInfo.fpr,
+ fromLower,
+ acceptanceResult
+ );
+ } catch (ex) {
+ console.debug("getAcceptance failed: " + ex);
+ }
+
+ // unverified key acceptance means, we consider the signature OK,
+ // but it's not a trusted identity.
+ // unverified signature means, we cannot decide if the signature
+ // is ok.
+
+ if (
+ "emailDecided" in acceptanceResult &&
+ acceptanceResult.emailDecided &&
+ "fingerprintAcceptance" in acceptanceResult &&
+ acceptanceResult.fingerprintAcceptance.length &&
+ acceptanceResult.fingerprintAcceptance != "undecided"
+ ) {
+ if (acceptanceResult.fingerprintAcceptance == "rejected") {
+ result.statusFlags &= ~lazy.EnigmailConstants.GOOD_SIGNATURE;
+ result.statusFlags |=
+ lazy.EnigmailConstants.BAD_SIGNATURE |
+ lazy.EnigmailConstants.INVALID_RECIPIENT;
+ useUndecided = false;
+ } else if (acceptanceResult.fingerprintAcceptance == "verified") {
+ result.statusFlags |= lazy.EnigmailConstants.TRUSTED_IDENTITY;
+ useUndecided = false;
+ } else if (acceptanceResult.fingerprintAcceptance == "unverified") {
+ useUndecided = false;
+ }
+ }
+ }
+ }
+
+ if (useUndecided) {
+ result.statusFlags &= ~lazy.EnigmailConstants.GOOD_SIGNATURE;
+ result.statusFlags |= lazy.EnigmailConstants.UNCERTAIN_SIGNATURE;
+ }
+ }
+
+ if (have_signer_key) {
+ RNPLib.rnp_key_handle_destroy(signer_key);
+ }
+
+ return true;
+ },
+
+ async verifyDetached(data, options) {
+ let result = {};
+ result.decryptedData = "";
+ result.statusFlags = 0;
+ result.exitCode = -1;
+ result.extStatusFlags = 0;
+ result.userId = "";
+ result.keyId = "";
+ result.sigDetails = {};
+ result.sigDetails.sigDate = null;
+
+ let sig_arr = options.mimeSignatureData.split("").map(e => e.charCodeAt());
+ var sig_array = lazy.ctypes.uint8_t.array()(sig_arr);
+
+ let input_sig = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ input_sig.address(),
+ sig_array,
+ sig_array.length,
+ false
+ );
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ let arr = data.split("").map(e => e.charCodeAt());
+ var data_array = lazy.ctypes.uint8_t.array()(arr);
+
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ data_array,
+ data_array.length,
+ false
+ );
+
+ let verify_op = new RNPLib.rnp_op_verify_t();
+ if (
+ RNPLib.rnp_op_verify_detached_create(
+ verify_op.address(),
+ RNPLib.ffi,
+ input_from_memory,
+ input_sig
+ )
+ ) {
+ throw new Error("rnp_op_verify_detached_create failed");
+ }
+
+ result.exitCode = RNPLib.rnp_op_verify_execute(verify_op);
+
+ let haveSignature = await this.getVerifyDetails(
+ RNPLib.ffi,
+ options.fromAddr,
+ options.msgDate,
+ verify_op,
+ result
+ );
+ if (!haveSignature) {
+ if (!result.exitCode) {
+ /* Don't allow a good exit code. Keep existing bad code. */
+ result.exitCode = -1;
+ }
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_input_destroy(input_sig);
+ RNPLib.rnp_op_verify_destroy(verify_op);
+
+ return result;
+ },
+
+ async genKey(userId, keyType, keyBits, expiryDays, passphrase) {
+ let newKeyId = "";
+ let newKeyFingerprint = "";
+
+ let primaryKeyType;
+ let primaryKeyBits = 0;
+ let subKeyType;
+ let subKeyBits = 0;
+ let primaryKeyCurve = null;
+ let subKeyCurve = null;
+ let expireSeconds = 0;
+
+ if (keyType == "RSA") {
+ primaryKeyType = subKeyType = "rsa";
+ primaryKeyBits = subKeyBits = keyBits;
+ } else if (keyType == "ECC") {
+ primaryKeyType = "eddsa";
+ subKeyType = "ecdh";
+ subKeyCurve = "Curve25519";
+ } else {
+ return null;
+ }
+
+ if (expiryDays != 0) {
+ expireSeconds = expiryDays * 24 * 60 * 60;
+ }
+
+ let genOp = new RNPLib.rnp_op_generate_t();
+ if (
+ RNPLib.rnp_op_generate_create(genOp.address(), RNPLib.ffi, primaryKeyType)
+ ) {
+ throw new Error("rnp_op_generate_create primary failed");
+ }
+
+ if (RNPLib.rnp_op_generate_set_userid(genOp, userId)) {
+ throw new Error("rnp_op_generate_set_userid failed");
+ }
+
+ if (passphrase != null && passphrase.length != 0) {
+ if (RNPLib.rnp_op_generate_set_protection_password(genOp, passphrase)) {
+ throw new Error("rnp_op_generate_set_protection_password failed");
+ }
+ }
+
+ if (primaryKeyBits != 0) {
+ if (RNPLib.rnp_op_generate_set_bits(genOp, primaryKeyBits)) {
+ throw new Error("rnp_op_generate_set_bits primary failed");
+ }
+ }
+
+ if (primaryKeyCurve != null) {
+ if (RNPLib.rnp_op_generate_set_curve(genOp, primaryKeyCurve)) {
+ throw new Error("rnp_op_generate_set_curve primary failed");
+ }
+ }
+
+ if (RNPLib.rnp_op_generate_set_expiration(genOp, expireSeconds)) {
+ throw new Error("rnp_op_generate_set_expiration primary failed");
+ }
+
+ if (RNPLib.rnp_op_generate_execute(genOp)) {
+ throw new Error("rnp_op_generate_execute primary failed");
+ }
+
+ let primaryKey = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_op_generate_get_key(genOp, primaryKey.address())) {
+ throw new Error("rnp_op_generate_get_key primary failed");
+ }
+
+ RNPLib.rnp_op_generate_destroy(genOp);
+
+ newKeyFingerprint = this.getFingerprintFromHandle(primaryKey);
+ newKeyId = this.getKeyIDFromHandle(primaryKey);
+
+ if (
+ RNPLib.rnp_op_generate_subkey_create(
+ genOp.address(),
+ RNPLib.ffi,
+ primaryKey,
+ subKeyType
+ )
+ ) {
+ throw new Error("rnp_op_generate_subkey_create primary failed");
+ }
+
+ if (passphrase != null && passphrase.length != 0) {
+ if (RNPLib.rnp_op_generate_set_protection_password(genOp, passphrase)) {
+ throw new Error("rnp_op_generate_set_protection_password failed");
+ }
+ }
+
+ if (subKeyBits != 0) {
+ if (RNPLib.rnp_op_generate_set_bits(genOp, subKeyBits)) {
+ throw new Error("rnp_op_generate_set_bits sub failed");
+ }
+ }
+
+ if (subKeyCurve != null) {
+ if (RNPLib.rnp_op_generate_set_curve(genOp, subKeyCurve)) {
+ throw new Error("rnp_op_generate_set_curve sub failed");
+ }
+ }
+
+ if (RNPLib.rnp_op_generate_set_expiration(genOp, expireSeconds)) {
+ throw new Error("rnp_op_generate_set_expiration sub failed");
+ }
+
+ let unlocked = false;
+ try {
+ if (passphrase != null && passphrase.length != 0) {
+ if (RNPLib.rnp_key_unlock(primaryKey, passphrase)) {
+ throw new Error("rnp_key_unlock failed");
+ }
+ unlocked = true;
+ }
+
+ if (RNPLib.rnp_op_generate_execute(genOp)) {
+ throw new Error("rnp_op_generate_execute sub failed");
+ }
+ } finally {
+ if (unlocked) {
+ RNPLib.rnp_key_lock(primaryKey);
+ }
+ }
+
+ RNPLib.rnp_op_generate_destroy(genOp);
+ RNPLib.rnp_key_handle_destroy(primaryKey);
+
+ await lazy.PgpSqliteDb2.acceptAsPersonalKey(newKeyFingerprint);
+
+ return newKeyId;
+ },
+
+ async saveKeyRings() {
+ RNPLib.saveKeys();
+ Services.obs.notifyObservers(null, "openpgp-key-change");
+ },
+
+ importToFFI(ffi, keyBlockStr, usePublic, useSecret, permissive) {
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ if (!keyBlockStr) {
+ throw new Error("no keyBlockStr parameter in importToFFI");
+ }
+
+ if (typeof keyBlockStr != "string") {
+ throw new Error(
+ "keyBlockStr of unepected type importToFFI: %o",
+ keyBlockStr
+ );
+ }
+
+ // Input might be either plain text or binary data.
+ // If the input is binary, do not modify it.
+ // If the input contains characters with a multi-byte char code value,
+ // we know the input doesn't consist of binary 8-bit values. Rather,
+ // it contains text with multi-byte characters. The only scenario
+ // in which we can tolerate those are comment lines, which we can
+ // filter out.
+
+ let arr = this.getCharCodeArray(keyBlockStr);
+ if (!this.is8Bit(arr)) {
+ let trimmed = this.removeCommentLines(keyBlockStr);
+ arr = this.getCharCodeArray(trimmed);
+ if (!this.is8Bit(arr)) {
+ throw new Error(`Non-ascii key block: ${keyBlockStr}`);
+ }
+ }
+ var key_array = lazy.ctypes.uint8_t.array()(arr);
+
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ key_array,
+ key_array.length,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let jsonInfo = new lazy.ctypes.char.ptr();
+
+ let flags = 0;
+ if (usePublic) {
+ flags |= RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS;
+ }
+ if (useSecret) {
+ flags |= RNPLib.RNP_LOAD_SAVE_SECRET_KEYS;
+ }
+
+ if (permissive) {
+ flags |= RNPLib.RNP_LOAD_SAVE_PERMISSIVE;
+ }
+
+ let rv = RNPLib.rnp_import_keys(
+ ffi,
+ input_from_memory,
+ flags,
+ jsonInfo.address()
+ );
+
+ // TODO: parse jsonInfo and return a list of keys,
+ // as seen in keyRing.importKeyAsync.
+ // (should prevent the incorrect popup "no keys imported".)
+
+ if (rv) {
+ console.debug("rnp_import_keys failed with rv: " + rv);
+ }
+
+ RNPLib.rnp_buffer_destroy(jsonInfo);
+ RNPLib.rnp_input_destroy(input_from_memory);
+
+ return rv;
+ },
+
+ maxImportKeyBlockSize: 5000000,
+
+ async getOnePubKeyFromKeyBlock(keyBlockStr, fpr, permissive = true) {
+ if (!keyBlockStr) {
+ throw new Error(`Invalid parameter; keyblock: ${keyBlockStr}`);
+ }
+
+ if (keyBlockStr.length > RNP.maxImportKeyBlockSize) {
+ throw new Error("rejecting big keyblock");
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let pubKey;
+ if (!this.importToFFI(tempFFI, keyBlockStr, true, false, permissive)) {
+ pubKey = await this.getPublicKey("0x" + fpr, tempFFI);
+ }
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return pubKey;
+ },
+
+ async getKeyListFromKeyBlockImpl(
+ keyBlockStr,
+ pubkey = true,
+ seckey = false,
+ permissive = true,
+ withPubKey = false
+ ) {
+ if (!keyBlockStr) {
+ throw new Error(`Invalid parameter; keyblock: ${keyBlockStr}`);
+ }
+
+ if (keyBlockStr.length > RNP.maxImportKeyBlockSize) {
+ throw new Error("rejecting big keyblock");
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let keyList = null;
+ if (!this.importToFFI(tempFFI, keyBlockStr, pubkey, seckey, permissive)) {
+ keyList = await this.getKeysFromFFI(
+ tempFFI,
+ true,
+ null,
+ false,
+ withPubKey
+ );
+ }
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return keyList;
+ },
+
+ /**
+ * Take two or more ASCII armored key blocks and import them into memory,
+ * and return the merged public key for the given fingerprint.
+ * (Other keys included in the key blocks are ignored.)
+ * The intention is to use it to combine keys obtained from different places,
+ * possibly with updated/different expiration date and userIds etc. to
+ * a canonical representation of them.
+ *
+ * @param {string} fingerprint - Key fingerprint.
+ * @param {...string} - Key blocks.
+ * @returns {string} the resulting public key of the blocks
+ */
+ async mergePublicKeyBlocks(fingerprint, ...keyBlocks) {
+ if (keyBlocks.some(b => b.length > RNP.maxImportKeyBlockSize)) {
+ throw new Error("keyBlock too big");
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ const pubkey = true;
+ const seckey = false;
+ const permissive = false;
+ for (let block of new Set(keyBlocks)) {
+ if (this.importToFFI(tempFFI, block, pubkey, seckey, permissive)) {
+ throw new Error("Merging public keys failed");
+ }
+ }
+ let pubKey = await this.getPublicKey(`0x${fingerprint}`, tempFFI);
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return pubKey;
+ },
+
+ async importRevImpl(data) {
+ if (!data || typeof data != "string") {
+ throw new Error("invalid data parameter");
+ }
+
+ let arr = data.split("").map(e => e.charCodeAt());
+ var key_array = lazy.ctypes.uint8_t.array()(arr);
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ key_array,
+ key_array.length,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let jsonInfo = new lazy.ctypes.char.ptr();
+
+ let flags = 0;
+ let rv = RNPLib.rnp_import_signatures(
+ RNPLib.ffi,
+ input_from_memory,
+ flags,
+ jsonInfo.address()
+ );
+
+ // TODO: parse jsonInfo
+
+ if (rv) {
+ console.debug("rnp_import_signatures failed with rv: " + rv);
+ }
+
+ RNPLib.rnp_buffer_destroy(jsonInfo);
+ RNPLib.rnp_input_destroy(input_from_memory);
+ await this.saveKeyRings();
+
+ return rv;
+ },
+
+ async importSecKeyBlockImpl(
+ win,
+ passCB,
+ keepPassphrases,
+ keyBlockStr,
+ permissive = false,
+ limitedFPRs = []
+ ) {
+ return this._importKeyBlockWithAutoAccept(
+ win,
+ passCB,
+ keepPassphrases,
+ keyBlockStr,
+ false,
+ true,
+ null,
+ permissive,
+ limitedFPRs
+ );
+ },
+
+ async importPubkeyBlockAutoAcceptImpl(
+ win,
+ keyBlockStr,
+ acceptance,
+ permissive = false,
+ limitedFPRs = []
+ ) {
+ return this._importKeyBlockWithAutoAccept(
+ win,
+ null,
+ false,
+ keyBlockStr,
+ true,
+ false,
+ acceptance,
+ permissive,
+ limitedFPRs
+ );
+ },
+
+ /**
+ * Import either a public key or a secret key.
+ * Importing both at the same time isn't supported by this API.
+ *
+ * @param {?nsIWindow} win - Parent window, may be null
+ * @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 {boolean} keepPassphrases - controls which passphrase will
+ * be used to protect imported secret keys. If true, the existing
+ * passphrase will be kept. If false, (of if currently there's no
+ * passphrase set), passphrase protection will be changed to use
+ * our automatic passphrase (to allow automatic protection by
+ * primary password, whether's it's currently enabled or not).
+ * @param {string} keyBlockStr - An block of OpenPGP key data. See
+ * implementation of function importToFFI for allowed contents.
+ * TODO: Write better documentation for this parameter.
+ * @param {boolean} pubkey - If true, import the public keys found in
+ * keyBlockStr.
+ * @param {boolean} seckey - If true, import the secret keys found in
+ * keyBlockStr.
+ * @param {string} acceptance - The key acceptance level that should
+ * be assigned to imported public keys.
+ * TODO: Write better documentation for the allowed values.
+ * @param {boolean} permissive - Whether it's allowed to fall back
+ * to a permissive import, if strict import fails.
+ * (See RNP documentation for RNP_LOAD_SAVE_PERMISSIVE.)
+ * @param {string[]} limitedFPRs - This is a filtering parameter.
+ * If the array is empty, all keys will be imported.
+ * If the array contains at least one entry, a key will be imported
+ * only if its fingerprint (of the primary key) is listed in this
+ * array.
+ */
+ async _importKeyBlockWithAutoAccept(
+ win,
+ passCB,
+ keepPassphrases,
+ keyBlockStr,
+ pubkey,
+ seckey,
+ acceptance,
+ permissive = false,
+ limitedFPRs = []
+ ) {
+ if (keyBlockStr.length > RNP.maxImportKeyBlockSize) {
+ throw new Error("rejecting big keyblock");
+ }
+ if (pubkey && seckey) {
+ // Currently no caller needs to import both at the save time,
+ // and the implementation hasn't been reviewed, whether it
+ // supports it or not, so we refuse this request.
+ throw new Error("Cannot import public and secret keys at the same time");
+ }
+
+ /*
+ * Import strategy:
+ * - import file into a temporary space, in-memory only (ffi)
+ * - if we failed to decrypt the secret keys, return null
+ * - set the password of secret keys that don't have one yet
+ * - get the key listing of all keys from the temporary space,
+ * which is want we want to return as the import report
+ * - export all keys from the temporary space, and import them
+ * into our permanent space.
+ */
+ let userFlags = { canceled: false };
+
+ let result = {};
+ result.exitCode = -1;
+ result.importedKeys = [];
+ result.errorMsg = "";
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ // TODO: check result
+ if (this.importToFFI(tempFFI, keyBlockStr, pubkey, seckey, permissive)) {
+ result.errorMsg = "RNP.importToFFI failed";
+ return result;
+ }
+
+ let keys = await this.getKeysFromFFI(tempFFI, true);
+ let pwCache = {
+ passwords: [],
+ };
+
+ // Prior to importing, ensure the user is able to unlock all keys
+
+ // If anything goes wrong during our attempt to unlock keys,
+ // we don't want to keep key material remain unprotected in memory,
+ // that's why we remember the trackers, including the respective
+ // unlock passphrase, temporarily in memory, and we'll minimize
+ // the period of time during which the key remains unprotected.
+ let secretKeyTrackers = new Map();
+
+ let unableToUnlockId = null;
+
+ for (let k of keys) {
+ let fprStr = "0x" + k.fpr;
+ if (limitedFPRs.length && !limitedFPRs.includes(fprStr)) {
+ continue;
+ }
+
+ let impKey = await this.getKeyHandleByIdentifier(tempFFI, fprStr);
+ if (impKey.isNull()) {
+ throw new Error("cannot get key handle for imported key: " + k.fpr);
+ }
+
+ if (!k.secretAvailable) {
+ RNPLib.rnp_key_handle_destroy(impKey);
+ impKey = null;
+ } else {
+ let primaryKey = new RnpPrivateKeyUnlockTracker(impKey);
+ impKey = null;
+
+ // Don't attempt to unlock secret keys that are unavailable.
+ if (primaryKey.available()) {
+ // Is it unprotected?
+ primaryKey.unlockWithPassword("");
+ if (primaryKey.isUnlocked()) {
+ // yes, it's unprotected (empty passphrase)
+ await primaryKey.setAutoPassphrase();
+ } else {
+ // try to unlock with the recently entered passwords,
+ // or ask the user, if allowed
+ primaryKey.setPasswordCache(pwCache);
+ primaryKey.setAllowAutoUnlockWithCachedPasswords(true);
+ primaryKey.setAllowPromptingUserForPassword(!!passCB);
+ primaryKey.setPassphraseCallback(passCB);
+ primaryKey.setRememberUnlockPassword(true);
+ await primaryKey.unlock(tempFFI);
+ if (!primaryKey.isUnlocked()) {
+ userFlags.canceled = true;
+ unableToUnlockId = RNP.getKeyIDFromHandle(primaryKey.getHandle());
+ } else {
+ secretKeyTrackers.set(fprStr, primaryKey);
+ }
+ }
+ }
+
+ if (!userFlags.canceled) {
+ let sub_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_key_get_subkey_count(
+ primaryKey.getHandle(),
+ sub_count.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ for (let i = 0; i < sub_count.value && !userFlags.canceled; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_key_get_subkey_at(
+ primaryKey.getHandle(),
+ i,
+ sub_handle.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ let subTracker = new RnpPrivateKeyUnlockTracker(sub_handle);
+ sub_handle = null;
+
+ if (subTracker.available()) {
+ // Is it unprotected?
+ subTracker.unlockWithPassword("");
+ if (subTracker.isUnlocked()) {
+ // yes, it's unprotected (empty passphrase)
+ await subTracker.setAutoPassphrase();
+ } else {
+ // try to unlock with the recently entered passwords,
+ // or ask the user, if allowed
+ subTracker.setPasswordCache(pwCache);
+ subTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ subTracker.setAllowPromptingUserForPassword(!!passCB);
+ subTracker.setPassphraseCallback(passCB);
+ subTracker.setRememberUnlockPassword(true);
+ await subTracker.unlock(tempFFI);
+ if (!subTracker.isUnlocked()) {
+ userFlags.canceled = true;
+ unableToUnlockId = RNP.getKeyIDFromHandle(
+ subTracker.getHandle()
+ );
+ break;
+ } else {
+ secretKeyTrackers.set(
+ this.getFingerprintFromHandle(subTracker.getHandle()),
+ subTracker
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (userFlags.canceled) {
+ break;
+ }
+ }
+
+ if (unableToUnlockId) {
+ result.errorMsg = "Cannot unlock key " + unableToUnlockId;
+ }
+
+ if (!userFlags.canceled) {
+ for (let k of keys) {
+ let fprStr = "0x" + k.fpr;
+ if (limitedFPRs.length && !limitedFPRs.includes(fprStr)) {
+ continue;
+ }
+
+ // We allow importing, if any of the following is true
+ // - it contains a secret key
+ // - it contains at least one user ID
+ // - it is an update for an existing key (possibly new validity/revocation)
+
+ if (k.userIds.length == 0 && !k.secretAvailable) {
+ let existingKey = await this.getKeyHandleByIdentifier(
+ RNPLib.ffi,
+ "0x" + k.fpr
+ );
+ if (existingKey.isNull()) {
+ continue;
+ }
+ RNPLib.rnp_key_handle_destroy(existingKey);
+ }
+
+ let impKeyPub;
+ let impKeySecTracker = secretKeyTrackers.get(fprStr);
+ if (!impKeySecTracker) {
+ impKeyPub = await this.getKeyHandleByIdentifier(tempFFI, fprStr);
+ }
+
+ if (!keepPassphrases) {
+ // It's possible that the primary key doesn't come with a
+ // secret key (only public key of primary key was imported).
+ // In that scenario, we must still process subkeys that come
+ // with a secret key.
+
+ if (impKeySecTracker) {
+ impKeySecTracker.unprotect();
+ await impKeySecTracker.setAutoPassphrase();
+ }
+
+ let sub_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_key_get_subkey_count(
+ impKeySecTracker ? impKeySecTracker.getHandle() : impKeyPub,
+ sub_count.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_key_get_subkey_at(
+ impKeySecTracker ? impKeySecTracker.getHandle() : impKeyPub,
+ i,
+ sub_handle.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ let subTracker = secretKeyTrackers.get(
+ this.getFingerprintFromHandle(sub_handle)
+ );
+ if (!subTracker) {
+ // There is no secret key material for this subkey available,
+ // that's why no tracker was created, we can skip it.
+ continue;
+ }
+ subTracker.unprotect();
+ await subTracker.setAutoPassphrase();
+ }
+ }
+
+ let exportFlags =
+ RNPLib.RNP_KEY_EXPORT_ARMORED | RNPLib.RNP_KEY_EXPORT_SUBKEYS;
+
+ if (pubkey) {
+ exportFlags |= RNPLib.RNP_KEY_EXPORT_PUBLIC;
+ }
+ if (seckey) {
+ exportFlags |= RNPLib.RNP_KEY_EXPORT_SECRET;
+ }
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ if (
+ RNPLib.rnp_key_export(
+ impKeySecTracker ? impKeySecTracker.getHandle() : impKeyPub,
+ output_to_memory,
+ exportFlags
+ )
+ ) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ if (impKeyPub) {
+ RNPLib.rnp_key_handle_destroy(impKeyPub);
+ impKeyPub = null;
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ }
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ result_buf,
+ result_len,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let importFlags = 0;
+ if (pubkey) {
+ importFlags |= RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS;
+ }
+ if (seckey) {
+ importFlags |= RNPLib.RNP_LOAD_SAVE_SECRET_KEYS;
+ }
+ if (permissive) {
+ importFlags |= RNPLib.RNP_LOAD_SAVE_PERMISSIVE;
+ }
+
+ if (
+ RNPLib.rnp_import_keys(
+ RNPLib.ffi,
+ input_from_memory,
+ importFlags,
+ null
+ )
+ ) {
+ throw new Error("rnp_import_keys failed");
+ }
+
+ result.importedKeys.push("0x" + k.id);
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+
+ // For acceptance "undecided", we don't store it, because that's
+ // the default if no value is stored.
+ let actionableAcceptances = ["rejected", "unverified", "verified"];
+
+ if (
+ pubkey &&
+ !k.secretAvailable &&
+ actionableAcceptances.includes(acceptance)
+ ) {
+ // For each imported public key and associated email address,
+ // update the acceptance to unverified, but only if it's only
+ // currently undecided. In other words, we keep the acceptance
+ // if it's rejected or verified.
+
+ let currentAcceptance =
+ await lazy.PgpSqliteDb2.getFingerprintAcceptance(null, k.fpr);
+
+ if (!currentAcceptance || currentAcceptance == "undecided") {
+ // Currently undecided, allowed to change.
+ let allEmails = [];
+
+ for (let uid of k.userIds) {
+ if (uid.type != "uid") {
+ continue;
+ }
+
+ let uidEmail = lazy.EnigmailFuncs.getEmailFromUserID(uid.userId);
+ if (uidEmail) {
+ allEmails.push(uidEmail);
+ }
+ }
+ await lazy.PgpSqliteDb2.updateAcceptance(
+ k.fpr,
+ allEmails,
+ acceptance
+ );
+ }
+ }
+ }
+
+ result.exitCode = 0;
+ await this.saveKeyRings();
+ }
+
+ for (let valTracker of secretKeyTrackers.values()) {
+ valTracker.release();
+ }
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return result;
+ },
+
+ async deleteKey(keyFingerprint, deleteSecret) {
+ let handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_locate_key(
+ RNPLib.ffi,
+ "fingerprint",
+ keyFingerprint,
+ handle.address()
+ )
+ ) {
+ throw new Error("rnp_locate_key failed");
+ }
+
+ let flags = RNPLib.RNP_KEY_REMOVE_PUBLIC | RNPLib.RNP_KEY_REMOVE_SUBKEYS;
+ if (deleteSecret) {
+ flags |= RNPLib.RNP_KEY_REMOVE_SECRET;
+ }
+
+ if (RNPLib.rnp_key_remove(handle, flags)) {
+ throw new Error("rnp_key_remove failed");
+ }
+
+ RNPLib.rnp_key_handle_destroy(handle);
+ await this.saveKeyRings();
+ },
+
+ async revokeKey(keyFingerprint) {
+ let tracker =
+ RnpPrivateKeyUnlockTracker.constructFromFingerprint(keyFingerprint);
+ if (!tracker.available()) {
+ return;
+ }
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ await tracker.unlock();
+ if (!tracker.isUnlocked()) {
+ return;
+ }
+
+ let flags = 0;
+ let revokeResult = RNPLib.rnp_key_revoke(
+ tracker.getHandle(),
+ flags,
+ null,
+ null,
+ null
+ );
+ tracker.release();
+ if (revokeResult) {
+ throw new Error(
+ `rnp_key_revoke failed for fingerprint=${keyFingerprint}`
+ );
+ }
+ await this.saveKeyRings();
+ },
+
+ _getKeyHandleByKeyIdOrFingerprint(ffi, id, findPrimary) {
+ if (!id.startsWith("0x")) {
+ throw new Error("unexpected identifier " + id);
+ } else {
+ // remove 0x
+ id = id.substring(2);
+ }
+
+ let type = null;
+ if (id.length == 16) {
+ type = "keyid";
+ } else if (id.length == 40 || id.length == 32) {
+ type = "fingerprint";
+ } else {
+ throw new Error("key/fingerprint identifier of unexpected length: " + id);
+ }
+
+ let key = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_locate_key(ffi, type, id, key.address())) {
+ throw new Error("rnp_locate_key failed, " + type + ", " + id);
+ }
+
+ if (!key.isNull() && findPrimary) {
+ let is_subkey = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_sub(key, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (is_subkey.value) {
+ let primaryKey = this.getPrimaryKeyHandleFromSub(ffi, key);
+ RNPLib.rnp_key_handle_destroy(key);
+ key = primaryKey;
+ }
+ }
+
+ if (!key.isNull() && this.isBadKey(key, null, ffi)) {
+ RNPLib.rnp_key_handle_destroy(key);
+ key = new RNPLib.rnp_key_handle_t();
+ }
+
+ return key;
+ },
+
+ getPrimaryKeyHandleByKeyIdOrFingerprint(ffi, id) {
+ return this._getKeyHandleByKeyIdOrFingerprint(ffi, id, true);
+ },
+
+ getKeyHandleByKeyIdOrFingerprint(ffi, id) {
+ return this._getKeyHandleByKeyIdOrFingerprint(ffi, id, false);
+ },
+
+ async getKeyHandleByIdentifier(ffi, id) {
+ let key = null;
+
+ if (id.startsWith("<")) {
+ //throw new Error("search by email address not yet implemented: " + id);
+ if (!id.endsWith(">")) {
+ throw new Error(
+ "if search identifier starts with < then it must end with > : " + id
+ );
+ }
+ key = await this.findKeyByEmail(id);
+ } else {
+ key = this.getKeyHandleByKeyIdOrFingerprint(ffi, id);
+ }
+ return key;
+ },
+
+ isKeyUsableFor(key, usage) {
+ let allowed = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_allows_usage(key, usage, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (!allowed.value) {
+ return false;
+ }
+
+ if (usage != str_sign) {
+ return true;
+ }
+
+ return (
+ RNPLib.getSecretAvailableFromHandle(key) &&
+ RNPLib.isSecretKeyMaterialAvailable(key)
+ );
+ },
+
+ getSuitableSubkey(primary, usage) {
+ let sub_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_subkey_count(primary, sub_count.address())) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ // For compatibility with GnuPG, when encrypting to a single subkey,
+ // encrypt to the most recently created subkey. (Bug 1665281)
+ let newest_created = null;
+ let newest_handle = null;
+
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_key_get_subkey_at(primary, i, sub_handle.address())) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+ let skip =
+ this.isBadKey(sub_handle, primary, null) ||
+ this.isKeyExpired(sub_handle);
+ if (!skip) {
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(sub_handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+ if (key_revoked.value) {
+ skip = true;
+ }
+ }
+ if (!skip) {
+ if (!this.isKeyUsableFor(sub_handle, usage)) {
+ skip = true;
+ }
+ }
+
+ if (!skip) {
+ let created = this.getKeyCreatedValueFromHandle(sub_handle);
+ if (!newest_handle || created > newest_created) {
+ if (newest_handle) {
+ RNPLib.rnp_key_handle_destroy(newest_handle);
+ }
+ newest_handle = sub_handle;
+ sub_handle = null;
+ newest_created = created;
+ }
+ }
+
+ if (sub_handle) {
+ RNPLib.rnp_key_handle_destroy(sub_handle);
+ }
+ }
+
+ return newest_handle;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible public key, for the given key
+ * that exactly matches the given userId.
+ *
+ * @param {rnp_key_handle_t} key - RNP key handle.
+ * @param {string} uidString - The userID to include.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ getSuitableEncryptKeyAsAutocrypt(key, userId) {
+ // Prefer usable subkeys, because they are always newer
+ // (or same age) as primary key.
+
+ let use_sub = this.getSuitableSubkey(key, str_encrypt);
+ if (!use_sub && !this.isKeyUsableFor(key, str_encrypt)) {
+ return "";
+ }
+
+ let result = this.getAutocryptKeyB64ByHandle(key, use_sub, userId);
+
+ if (use_sub) {
+ RNPLib.rnp_key_handle_destroy(use_sub);
+ }
+ return result;
+ },
+
+ addSuitableEncryptKey(key, op) {
+ // Prefer usable subkeys, because they are always newer
+ // (or same age) as primary key.
+
+ let use_sub = this.getSuitableSubkey(key, str_encrypt);
+ if (!use_sub && !this.isKeyUsableFor(key, str_encrypt)) {
+ throw new Error("no suitable subkey found for " + str_encrypt);
+ }
+
+ if (
+ RNPLib.rnp_op_encrypt_add_recipient(op, use_sub != null ? use_sub : key)
+ ) {
+ throw new Error("rnp_op_encrypt_add_recipient sender failed");
+ }
+ if (use_sub) {
+ RNPLib.rnp_key_handle_destroy(use_sub);
+ }
+ },
+
+ addAliasKeys(aliasKeys, op) {
+ for (let ak of aliasKeys) {
+ let key = this.getKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, "0x" + ak);
+ if (!key || key.isNull()) {
+ console.debug(
+ "addAliasKeys: cannot find key used by alias rule: " + ak
+ );
+ return false;
+ }
+ this.addSuitableEncryptKey(key, op);
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ return true;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible public key, for the given email
+ * address.
+ *
+ * @param {string} email - Use a userID with this email address.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ async getRecipientAutocryptKeyForEmail(email) {
+ email = email.toLowerCase();
+
+ let key = await this.findKeyByEmail("<" + email + ">", true);
+ if (!key || key.isNull()) {
+ return "";
+ }
+
+ let keyInfo = {};
+ let ok = this.getKeyInfoFromHandle(
+ RNPLib.ffi,
+ key,
+ keyInfo,
+ false,
+ false,
+ false
+ );
+ if (!ok) {
+ throw new Error("getKeyInfoFromHandle failed");
+ }
+
+ let result = "";
+ let userId = keyInfo.userIds.find(
+ uid =>
+ uid.type == "uid" &&
+ lazy.EnigmailFuncs.getEmailFromUserID(uid.userId).toLowerCase() == email
+ );
+ if (userId) {
+ result = this.getSuitableEncryptKeyAsAutocrypt(key, userId.userId);
+ }
+ RNPLib.rnp_key_handle_destroy(key);
+ return result;
+ },
+
+ async addEncryptionKeyForEmail(email, op) {
+ let key = await this.findKeyByEmail(email, true);
+ if (!key || key.isNull()) {
+ return false;
+ }
+ this.addSuitableEncryptKey(key, op);
+ RNPLib.rnp_key_handle_destroy(key);
+ return true;
+ },
+
+ getEmailWithoutBrackets(email) {
+ if (email.startsWith("<") && email.endsWith(">")) {
+ return email.substring(1, email.length - 1);
+ }
+ return email;
+ },
+
+ async encryptAndOrSign(plaintext, args, resultStatus) {
+ let signedInner;
+
+ if (args.sign && args.senderKeyIsExternal) {
+ if (!lazy.GPGME.allDependenciesLoaded()) {
+ throw new Error(
+ "invalid configuration, request to use external GnuPG key, but GPGME isn't working"
+ );
+ }
+ if (args.sigTypeClear) {
+ throw new Error(
+ "unexpected signing request with external GnuPG key configuration"
+ );
+ }
+
+ if (args.encrypt) {
+ // If we are asked to encrypt and sign at the same time, it
+ // means we're asked to produce the combined OpenPGP encoding.
+ // We ask GPG to produce a regular signature, and will then
+ // combine it with the encryption produced by RNP.
+ let orgEncrypt = args.encrypt;
+ args.encrypt = false;
+ signedInner = await lazy.GPGME.sign(plaintext, args, resultStatus);
+ args.encrypt = orgEncrypt;
+ } else {
+ // We aren't asked to encrypt, but sign only. That means the
+ // caller needs the detatched signature, either for MIME
+ // mime encoding with separate signature part, or for the nested
+ // approach with separate signing and encryption layers.
+ return lazy.GPGME.signDetached(plaintext, args, resultStatus);
+ }
+ }
+
+ resultStatus.exitCode = -1;
+ resultStatus.statusFlags = 0;
+ resultStatus.statusMsg = "";
+ resultStatus.errorMsg = "";
+
+ let data_array;
+ if (args.sign && args.senderKeyIsExternal) {
+ data_array = lazy.ctypes.uint8_t.array()(signedInner);
+ } else {
+ let arr = plaintext.split("").map(e => e.charCodeAt());
+ data_array = lazy.ctypes.uint8_t.array()(arr);
+ }
+
+ let input = new RNPLib.rnp_input_t();
+ if (
+ RNPLib.rnp_input_from_memory(
+ input.address(),
+ data_array,
+ data_array.length,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let output = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ let op;
+ if (args.encrypt) {
+ op = new RNPLib.rnp_op_encrypt_t();
+ if (
+ RNPLib.rnp_op_encrypt_create(op.address(), RNPLib.ffi, input, output)
+ ) {
+ throw new Error("rnp_op_encrypt_create failed");
+ }
+ } else if (args.sign && !args.senderKeyIsExternal) {
+ op = new RNPLib.rnp_op_sign_t();
+ if (args.sigTypeClear) {
+ if (
+ RNPLib.rnp_op_sign_cleartext_create(
+ op.address(),
+ RNPLib.ffi,
+ input,
+ output
+ )
+ ) {
+ throw new Error("rnp_op_sign_cleartext_create failed");
+ }
+ } else if (args.sigTypeDetached) {
+ if (
+ RNPLib.rnp_op_sign_detached_create(
+ op.address(),
+ RNPLib.ffi,
+ input,
+ output
+ )
+ ) {
+ throw new Error("rnp_op_sign_detached_create failed");
+ }
+ } else {
+ throw new Error(
+ "not yet implemented scenario: signing, neither clear nor encrypt, without encryption"
+ );
+ }
+ } else {
+ throw new Error("invalid parameters, neither encrypt nor sign");
+ }
+
+ let senderKeyTracker = null;
+ let subKeyTracker = null;
+
+ try {
+ if ((args.sign && !args.senderKeyIsExternal) || args.encryptToSender) {
+ {
+ // Use a temporary scope to ensure the senderKey variable
+ // cannot be accessed later on.
+ let senderKey = await this.getKeyHandleByIdentifier(
+ RNPLib.ffi,
+ args.sender
+ );
+ if (!senderKey || senderKey.isNull()) {
+ return null;
+ }
+
+ senderKeyTracker = new RnpPrivateKeyUnlockTracker(senderKey);
+ senderKeyTracker.setAllowPromptingUserForPassword(true);
+ senderKeyTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ }
+
+ // Manually configured external key overrides the check for
+ // a valid personal key.
+ if (!args.senderKeyIsExternal) {
+ if (!senderKeyTracker.isSecret()) {
+ throw new Error(
+ `configured sender key ${args.sender} isn't available`
+ );
+ }
+ if (
+ !(await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(
+ senderKeyTracker.getFingerprint()
+ ))
+ ) {
+ throw new Error(
+ `configured sender key ${args.sender} isn't accepted as a personal key`
+ );
+ }
+ }
+
+ if (args.encryptToSender) {
+ this.addSuitableEncryptKey(senderKeyTracker.getHandle(), op);
+ }
+
+ if (args.sign && !args.senderKeyIsExternal) {
+ let signingKeyTrackerReference = senderKeyTracker;
+
+ // Prefer usable subkeys, because they are always newer
+ // (or same age) as primary key.
+ let usableSubKeyHandle = this.getSuitableSubkey(
+ senderKeyTracker.getHandle(),
+ str_sign
+ );
+ if (
+ !usableSubKeyHandle &&
+ !this.isKeyUsableFor(senderKeyTracker.getHandle(), str_sign)
+ ) {
+ throw new Error("no suitable (sub)key found for " + str_sign);
+ }
+ if (usableSubKeyHandle) {
+ subKeyTracker = new RnpPrivateKeyUnlockTracker(usableSubKeyHandle);
+ subKeyTracker.setAllowPromptingUserForPassword(true);
+ subKeyTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ if (subKeyTracker.available()) {
+ signingKeyTrackerReference = subKeyTracker;
+ }
+ }
+
+ await signingKeyTrackerReference.unlock();
+
+ if (args.encrypt) {
+ if (
+ RNPLib.rnp_op_encrypt_add_signature(
+ op,
+ signingKeyTrackerReference.getHandle(),
+ null
+ )
+ ) {
+ throw new Error("rnp_op_encrypt_add_signature failed");
+ }
+ } else if (
+ RNPLib.rnp_op_sign_add_signature(
+ op,
+ signingKeyTrackerReference.getHandle(),
+ null
+ )
+ ) {
+ throw new Error("rnp_op_sign_add_signature failed");
+ }
+ // This was just a reference, no ownership.
+ signingKeyTrackerReference = null;
+ }
+ }
+
+ if (args.encrypt) {
+ // If we have an alias definition, it will be used, and the usual
+ // lookup by email address will be skipped. Earlier code should
+ // have already checked that alias keys are available and usable
+ // for encryption, so we fail if a problem is found.
+
+ for (let rcpList of [args.to, args.bcc]) {
+ for (let rcpEmail of rcpList) {
+ rcpEmail = rcpEmail.toLowerCase();
+ let aliasKeys = args.aliasKeys.get(
+ this.getEmailWithoutBrackets(rcpEmail)
+ );
+ if (aliasKeys) {
+ if (!this.addAliasKeys(aliasKeys, op)) {
+ resultStatus.statusFlags |=
+ lazy.EnigmailConstants.INVALID_RECIPIENT;
+ return null;
+ }
+ } else if (!(await this.addEncryptionKeyForEmail(rcpEmail, op))) {
+ resultStatus.statusFlags |=
+ lazy.EnigmailConstants.INVALID_RECIPIENT;
+ return null;
+ }
+ }
+ }
+
+ if (AppConstants.MOZ_UPDATE_CHANNEL != "release") {
+ let debugKey = Services.prefs.getStringPref(
+ "mail.openpgp.debug.extra_encryption_key"
+ );
+ if (debugKey) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ debugKey
+ );
+ if (!handle.isNull()) {
+ console.debug("encrypting to debug key " + debugKey);
+ this.addSuitableEncryptKey(handle, op);
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+ }
+ }
+
+ // Don't use AEAD as long as RNP uses v5 packets which aren't
+ // widely compatible with other clients.
+ if (RNPLib.rnp_op_encrypt_set_aead(op, "NONE")) {
+ throw new Error("rnp_op_encrypt_set_aead failed");
+ }
+
+ if (RNPLib.rnp_op_encrypt_set_cipher(op, "AES256")) {
+ throw new Error("rnp_op_encrypt_set_cipher failed");
+ }
+
+ // TODO, map args.signatureHash string to RNP and call
+ // rnp_op_encrypt_set_hash
+ if (RNPLib.rnp_op_encrypt_set_hash(op, "SHA256")) {
+ throw new Error("rnp_op_encrypt_set_hash failed");
+ }
+
+ if (RNPLib.rnp_op_encrypt_set_armor(op, args.armor)) {
+ throw new Error("rnp_op_encrypt_set_armor failed");
+ }
+
+ if (args.sign && args.senderKeyIsExternal) {
+ if (RNPLib.rnp_op_encrypt_set_flags(op, RNPLib.RNP_ENCRYPT_NOWRAP)) {
+ throw new Error("rnp_op_encrypt_set_flags failed");
+ }
+ }
+
+ let rv = RNPLib.rnp_op_encrypt_execute(op);
+ if (rv) {
+ throw new Error("rnp_op_encrypt_execute failed: " + rv);
+ }
+ RNPLib.rnp_op_encrypt_destroy(op);
+ } else if (args.sign && !args.senderKeyIsExternal) {
+ if (RNPLib.rnp_op_sign_set_hash(op, "SHA256")) {
+ throw new Error("rnp_op_sign_set_hash failed");
+ }
+ // TODO, map args.signatureHash string to RNP and call
+ // rnp_op_encrypt_set_hash
+
+ if (RNPLib.rnp_op_sign_set_armor(op, args.armor)) {
+ throw new Error("rnp_op_sign_set_armor failed");
+ }
+
+ if (RNPLib.rnp_op_sign_execute(op)) {
+ throw new Error("rnp_op_sign_execute failed");
+ }
+ RNPLib.rnp_op_sign_destroy(op);
+ }
+ } finally {
+ if (subKeyTracker) {
+ subKeyTracker.release();
+ }
+ if (senderKeyTracker) {
+ senderKeyTracker.release();
+ }
+ }
+
+ RNPLib.rnp_input_destroy(input);
+
+ let result = null;
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ !RNPLib.rnp_output_memory_get_buf(
+ output,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(output);
+
+ resultStatus.exitCode = 0;
+
+ if (args.encrypt) {
+ resultStatus.statusFlags |= lazy.EnigmailConstants.END_ENCRYPTION;
+ }
+
+ if (args.sign) {
+ resultStatus.statusFlags |= lazy.EnigmailConstants.SIG_CREATED;
+ }
+
+ return result;
+ },
+
+ /**
+ * @param {number} expiryTime - Time to check, in seconds from the epoch.
+ * @returns {boolean} - true if the given time is after now.
+ */
+ isExpiredTime(expiryTime) {
+ if (!expiryTime) {
+ return false;
+ }
+ let nowSeconds = Math.floor(Date.now() / 1000);
+ return nowSeconds > expiryTime;
+ },
+
+ isKeyExpired(handle) {
+ let expiration = new lazy.ctypes.uint32_t();
+ if (RNPLib.rnp_key_get_expiration(handle, expiration.address())) {
+ throw new Error("rnp_key_get_expiration failed");
+ }
+ if (!expiration.value) {
+ return false;
+ }
+
+ let created = this.getKeyCreatedValueFromHandle(handle);
+ let expirationSeconds = created + expiration.value;
+ return this.isExpiredTime(expirationSeconds);
+ },
+
+ async findKeyByEmail(id, onlyIfAcceptableAsRecipientKey = false) {
+ if (!id.startsWith("<") || !id.endsWith(">") || id.includes(" ")) {
+ throw new Error(`Invalid argument; id=${id}`);
+ }
+
+ let emailWithoutBrackets = id.substring(1, id.length - 1);
+
+ let iter = new RNPLib.rnp_identifier_iterator_t();
+ let grip = new lazy.ctypes.char.ptr();
+
+ if (
+ RNPLib.rnp_identifier_iterator_create(RNPLib.ffi, iter.address(), "grip")
+ ) {
+ throw new Error("rnp_identifier_iterator_create failed");
+ }
+
+ let foundHandle = null;
+ let tentativeUnverifiedHandle = null;
+
+ while (
+ !foundHandle &&
+ !RNPLib.rnp_identifier_iterator_next(iter, grip.address())
+ ) {
+ if (grip.isNull()) {
+ break;
+ }
+
+ let have_handle = false;
+ let handle = new RNPLib.rnp_key_handle_t();
+
+ try {
+ let is_subkey = new lazy.ctypes.bool();
+ let uid_count = new lazy.ctypes.size_t();
+
+ if (RNPLib.rnp_locate_key(RNPLib.ffi, "grip", grip, handle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+ have_handle = true;
+ if (RNPLib.rnp_key_is_sub(handle, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (is_subkey.value) {
+ continue;
+ }
+ if (this.isBadKey(handle, null, RNPLib.ffi)) {
+ continue;
+ }
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+
+ if (key_revoked.value) {
+ continue;
+ }
+
+ if (this.isKeyExpired(handle)) {
+ continue;
+ }
+
+ if (RNPLib.rnp_key_get_uid_count(handle, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+
+ let foundUid = false;
+ for (let i = 0; i < uid_count.value && !foundUid; i++) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+
+ if (
+ RNPLib.rnp_key_get_uid_handle_at(handle, i, uid_handle.address())
+ ) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+
+ if (!this.isBadUid(uid_handle) && !this.isRevokedUid(uid_handle)) {
+ let uid_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_uid_at(handle, i, uid_str.address())) {
+ throw new Error("rnp_key_get_uid_at failed");
+ }
+
+ let userId = uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(uid_str);
+
+ if (
+ lazy.EnigmailFuncs.getEmailFromUserID(userId).toLowerCase() ==
+ emailWithoutBrackets
+ ) {
+ foundUid = true;
+
+ if (onlyIfAcceptableAsRecipientKey) {
+ // a key is acceptable, either:
+ // - without secret key, it's accepted verified or unverified
+ // - with secret key, must be marked as personal
+
+ let have_secret = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_have_secret(handle, have_secret.address())) {
+ throw new Error("rnp_key_have_secret failed");
+ }
+
+ let fingerprint = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_fprint(handle, fingerprint.address())) {
+ throw new Error("rnp_key_get_fprint failed");
+ }
+ let fpr = fingerprint.readString();
+ RNPLib.rnp_buffer_destroy(fingerprint);
+
+ if (have_secret.value) {
+ let isAccepted =
+ await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(fpr);
+ if (isAccepted) {
+ foundHandle = handle;
+ have_handle = false;
+ if (tentativeUnverifiedHandle) {
+ RNPLib.rnp_key_handle_destroy(tentativeUnverifiedHandle);
+ tentativeUnverifiedHandle = null;
+ }
+ }
+ } else {
+ let acceptanceResult = {};
+ try {
+ await lazy.PgpSqliteDb2.getAcceptance(
+ fpr,
+ emailWithoutBrackets,
+ acceptanceResult
+ );
+ } catch (ex) {
+ console.debug("getAcceptance failed: " + ex);
+ }
+
+ if (!acceptanceResult.emailDecided) {
+ continue;
+ }
+ if (acceptanceResult.fingerprintAcceptance == "unverified") {
+ /* keep searching for a better, verified key */
+ if (!tentativeUnverifiedHandle) {
+ tentativeUnverifiedHandle = handle;
+ have_handle = false;
+ }
+ } else if (
+ acceptanceResult.fingerprintAcceptance == "verified"
+ ) {
+ foundHandle = handle;
+ have_handle = false;
+ if (tentativeUnverifiedHandle) {
+ RNPLib.rnp_key_handle_destroy(tentativeUnverifiedHandle);
+ tentativeUnverifiedHandle = null;
+ }
+ }
+ }
+ } else {
+ foundHandle = handle;
+ have_handle = false;
+ }
+ }
+ }
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+ } catch (ex) {
+ console.log(ex);
+ } finally {
+ if (have_handle) {
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+ }
+ }
+
+ if (!foundHandle && tentativeUnverifiedHandle) {
+ foundHandle = tentativeUnverifiedHandle;
+ tentativeUnverifiedHandle = null;
+ }
+
+ RNPLib.rnp_identifier_iterator_destroy(iter);
+ return foundHandle;
+ },
+
+ async getPublicKey(id, store = RNPLib.ffi) {
+ let result = "";
+ let key = await this.getKeyHandleByIdentifier(store, id);
+
+ if (key.isNull()) {
+ return result;
+ }
+
+ let flags =
+ RNPLib.RNP_KEY_EXPORT_ARMORED |
+ RNPLib.RNP_KEY_EXPORT_PUBLIC |
+ RNPLib.RNP_KEY_EXPORT_SUBKEYS;
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(output_to_memory.address(), 0);
+
+ if (RNPLib.rnp_key_export(key, output_to_memory, flags)) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(output_to_memory);
+ RNPLib.rnp_key_handle_destroy(key);
+ return result;
+ },
+
+ /**
+ * Exports a public key, strips all signatures added by others,
+ * and optionally also strips user IDs. Self-signatures are kept.
+ * The given key handle will not be modified. The input key will be
+ * copied to a temporary area, only the temporary copy will be
+ * modified. The result key will be streamed to the given output.
+ *
+ * @param {rnp_key_handle_t} expKey - RNP key handle
+ * @param {boolean} keepUserIDs - if true keep users IDs
+ * @param {rnp_output_t} out_binary - output stream handle
+ *
+ */
+ export_pubkey_strip_sigs_uids(expKey, keepUserIDs, out_binary) {
+ let expKeyId = this.getKeyIDFromHandle(expKey);
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let exportFlags =
+ RNPLib.RNP_KEY_EXPORT_SUBKEYS | RNPLib.RNP_KEY_EXPORT_PUBLIC;
+ let importFlags = RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS;
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ if (RNPLib.rnp_key_export(expKey, output_to_memory, exportFlags)) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ }
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ result_buf,
+ result_len,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ if (RNPLib.rnp_import_keys(tempFFI, input_from_memory, importFlags, null)) {
+ throw new Error("rnp_import_keys failed");
+ }
+
+ let tempKey = this.getKeyHandleByKeyIdOrFingerprint(
+ tempFFI,
+ "0x" + expKeyId
+ );
+
+ // Strip
+
+ if (!keepUserIDs) {
+ let uid_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_uid_count(tempKey, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+ for (let i = uid_count.value; i > 0; i--) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+ if (
+ RNPLib.rnp_key_get_uid_handle_at(tempKey, i - 1, uid_handle.address())
+ ) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+ if (RNPLib.rnp_uid_remove(tempKey, uid_handle)) {
+ throw new Error("rnp_uid_remove failed");
+ }
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+ }
+
+ if (
+ RNPLib.rnp_key_remove_signatures(
+ tempKey,
+ RNPLib.RNP_KEY_SIGNATURE_NON_SELF_SIG,
+ null,
+ null
+ )
+ ) {
+ throw new Error("rnp_key_remove_signatures failed");
+ }
+
+ if (RNPLib.rnp_key_export(tempKey, out_binary, exportFlags)) {
+ throw new Error("rnp_key_export failed");
+ }
+ RNPLib.rnp_key_handle_destroy(tempKey);
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ },
+
+ /**
+ * Export one or multiple public keys.
+ *
+ * @param {string[]} idArrayFull - an array of key IDs or fingerprints
+ * that should be exported as full keys including all attributes.
+ * @param {string[]} idArrayReduced - an array of key IDs or
+ * fingerprints that should be exported with all self-signatures,
+ * but without signatures from others.
+ * @param {string[]} idArrayMinimal - an array of key IDs or
+ * fingerprints that should be exported as minimized keys.
+ * @returns {string} - An ascii armored key block containing all
+ * requested (available) keys.
+ */
+ getMultiplePublicKeys(idArrayFull, idArrayReduced, idArrayMinimal) {
+ let out_final = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(out_final.address(), 0);
+
+ let out_binary = new RNPLib.rnp_output_t();
+ let rv;
+ if (
+ (rv = RNPLib.rnp_output_to_armor(
+ out_final,
+ out_binary.address(),
+ "public key"
+ ))
+ ) {
+ throw new Error("rnp_output_to_armor failed:" + rv);
+ }
+
+ if ((rv = RNPLib.rnp_output_armor_set_line_length(out_binary, 64))) {
+ throw new Error("rnp_output_armor_set_line_length failed:" + rv);
+ }
+
+ let flags = RNPLib.RNP_KEY_EXPORT_PUBLIC | RNPLib.RNP_KEY_EXPORT_SUBKEYS;
+
+ if (idArrayFull) {
+ for (let id of idArrayFull) {
+ let key = this.getKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, id);
+ if (key.isNull()) {
+ continue;
+ }
+
+ if (RNPLib.rnp_key_export(key, out_binary, flags)) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ }
+
+ if (idArrayReduced) {
+ for (let id of idArrayReduced) {
+ let key = this.getPrimaryKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, id);
+ if (key.isNull()) {
+ continue;
+ }
+
+ this.export_pubkey_strip_sigs_uids(key, true, out_binary);
+
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ }
+
+ if (idArrayMinimal) {
+ for (let id of idArrayMinimal) {
+ let key = this.getPrimaryKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, id);
+ if (key.isNull()) {
+ continue;
+ }
+
+ this.export_pubkey_strip_sigs_uids(key, false, out_binary);
+
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ }
+
+ if ((rv = RNPLib.rnp_output_finish(out_binary))) {
+ throw new Error("rnp_output_finish failed: " + rv);
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ out_final,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ let result = "";
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(out_binary);
+ RNPLib.rnp_output_destroy(out_final);
+
+ return result;
+ },
+
+ /**
+ * The RNP library may store keys in a format that isn't compatible
+ * with GnuPG, see bug 1713621 for an example where this happened.
+ *
+ * This function modifies the input key to make it compatible.
+ *
+ * The caller must ensure that the key is unprotected when calling
+ * this function, and must apply the desired protection afterwards.
+ */
+ ensureECCSubkeyIsGnuPGCompatible(tempKey) {
+ let algo = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_alg(tempKey, algo.address())) {
+ throw new Error("rnp_key_get_alg failed");
+ }
+ let algoStr = algo.readString();
+ RNPLib.rnp_buffer_destroy(algo);
+
+ if (algoStr.toLowerCase() != "ecdh") {
+ return;
+ }
+
+ let curve = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_curve(tempKey, curve.address())) {
+ throw new Error("rnp_key_get_curve failed");
+ }
+ let curveStr = curve.readString();
+ RNPLib.rnp_buffer_destroy(curve);
+
+ if (curveStr.toLowerCase() != "curve25519") {
+ return;
+ }
+
+ let tweak_status = new lazy.ctypes.bool();
+ let rc = RNPLib.rnp_key_25519_bits_tweaked(tempKey, tweak_status.address());
+ if (rc) {
+ throw new Error("rnp_key_25519_bits_tweaked failed: " + rc);
+ }
+
+ // If it's not tweaked yet, then tweak to make it compatible.
+ if (!tweak_status.value) {
+ rc = RNPLib.rnp_key_25519_bits_tweak(tempKey);
+ if (rc) {
+ throw new Error("rnp_key_25519_bits_tweak failed: " + rc);
+ }
+ }
+ },
+
+ async backupSecretKeys(fprs, backupPassword) {
+ if (!fprs.length) {
+ throw new Error("invalid fprs parameter");
+ }
+
+ /*
+ * Strategy:
+ * - copy keys to a temporary space, in-memory only (ffi)
+ * - if we failed to decrypt the secret keys, return null
+ * - change the password of all secret keys in the temporary space
+ * - export from the temporary space
+ */
+
+ let out_final = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(out_final.address(), 0);
+
+ let out_binary = new RNPLib.rnp_output_t();
+ let rv;
+ if (
+ (rv = RNPLib.rnp_output_to_armor(
+ out_final,
+ out_binary.address(),
+ "secret key"
+ ))
+ ) {
+ throw new Error("rnp_output_to_armor failed:" + rv);
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let exportFlags =
+ RNPLib.RNP_KEY_EXPORT_SUBKEYS | RNPLib.RNP_KEY_EXPORT_SECRET;
+ let importFlags =
+ RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS | RNPLib.RNP_LOAD_SAVE_SECRET_KEYS;
+
+ let unlockFailed = false;
+ let pwCache = {
+ passwords: [],
+ };
+
+ for (let fpr of fprs) {
+ let fprStr = fpr;
+ let expKey = await this.getKeyHandleByIdentifier(
+ RNPLib.ffi,
+ "0x" + fprStr
+ );
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ if (RNPLib.rnp_key_export(expKey, output_to_memory, exportFlags)) {
+ throw new Error("rnp_key_export failed");
+ }
+ RNPLib.rnp_key_handle_destroy(expKey);
+ expKey = null;
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ }
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ result_buf,
+ result_len,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ if (
+ RNPLib.rnp_import_keys(tempFFI, input_from_memory, importFlags, null)
+ ) {
+ throw new Error("rnp_import_keys failed");
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+ input_from_memory = null;
+ output_to_memory = null;
+ result_buf = null;
+
+ let tracker = RnpPrivateKeyUnlockTracker.constructFromFingerprint(
+ fprStr,
+ tempFFI
+ );
+ if (!tracker.available()) {
+ tracker.release();
+ continue;
+ }
+
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ tracker.setPasswordCache(pwCache);
+ tracker.setRememberUnlockPassword(true);
+
+ await tracker.unlock();
+ if (!tracker.isUnlocked()) {
+ unlockFailed = true;
+ tracker.release();
+ break;
+ }
+
+ tracker.unprotect();
+ tracker.setPassphrase(backupPassword);
+
+ let sub_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_key_get_subkey_count(
+ tracker.getHandle(),
+ sub_count.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_key_get_subkey_at(
+ tracker.getHandle(),
+ i,
+ sub_handle.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ let subTracker = new RnpPrivateKeyUnlockTracker(sub_handle);
+ if (subTracker.available()) {
+ subTracker.setAllowPromptingUserForPassword(true);
+ subTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ subTracker.setPasswordCache(pwCache);
+ subTracker.setRememberUnlockPassword(true);
+
+ await subTracker.unlock();
+ if (!subTracker.isUnlocked()) {
+ unlockFailed = true;
+ } else {
+ subTracker.unprotect();
+ this.ensureECCSubkeyIsGnuPGCompatible(subTracker.getHandle());
+ subTracker.setPassphrase(backupPassword);
+ }
+ }
+ subTracker.release();
+ if (unlockFailed) {
+ break;
+ }
+ }
+
+ if (
+ !unlockFailed &&
+ RNPLib.rnp_key_export(tracker.getHandle(), out_binary, exportFlags)
+ ) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ tracker.release();
+ if (unlockFailed) {
+ break;
+ }
+ }
+ RNPLib.rnp_ffi_destroy(tempFFI);
+
+ let result = "";
+ if (!unlockFailed) {
+ if ((rv = RNPLib.rnp_output_finish(out_binary))) {
+ throw new Error("rnp_output_finish failed: " + rv);
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ out_final,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = char_array.readString();
+ }
+ }
+
+ RNPLib.rnp_output_destroy(out_binary);
+ RNPLib.rnp_output_destroy(out_final);
+
+ return result;
+ },
+
+ async unlockAndGetNewRevocation(id, pass) {
+ let result = "";
+ let key = await this.getKeyHandleByIdentifier(RNPLib.ffi, id);
+
+ if (key.isNull()) {
+ return result;
+ }
+
+ let tracker = new RnpPrivateKeyUnlockTracker(key);
+ tracker.setAllowPromptingUserForPassword(false);
+ tracker.setAllowAutoUnlockWithCachedPasswords(false);
+ tracker.unlockWithPassword(pass);
+ if (!tracker.isUnlocked()) {
+ throw new Error(`Couldn't unlock key ${key.fpr}`);
+ }
+
+ let out_final = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(out_final.address(), 0);
+
+ let out_binary = new RNPLib.rnp_output_t();
+ let rv;
+ if (
+ (rv = RNPLib.rnp_output_to_armor(
+ out_final,
+ out_binary.address(),
+ "public key"
+ ))
+ ) {
+ throw new Error("rnp_output_to_armor failed:" + rv);
+ }
+
+ if (
+ (rv = RNPLib.rnp_key_export_revocation(
+ key,
+ out_binary,
+ 0,
+ null,
+ null,
+ null
+ ))
+ ) {
+ throw new Error("rnp_key_export_revocation failed: " + rv);
+ }
+
+ if ((rv = RNPLib.rnp_output_finish(out_binary))) {
+ throw new Error("rnp_output_finish failed: " + rv);
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ out_final,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(out_binary);
+ RNPLib.rnp_output_destroy(out_final);
+ tracker.release();
+ return result;
+ },
+
+ enArmorString(input, type) {
+ let arr = input.split("").map(e => e.charCodeAt());
+ let input_array = lazy.ctypes.uint8_t.array()(arr);
+
+ return this.enArmorCData(input_array, input_array.length, type);
+ },
+
+ enArmorCDataMessage(buf, len) {
+ return this.enArmorCData(buf, len, "message");
+ },
+
+ enArmorCData(buf, len, type) {
+ let input_array = lazy.ctypes.cast(buf, lazy.ctypes.uint8_t.array(len));
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ input_array,
+ len,
+ false
+ );
+
+ let max_out = len * 2 + 150; // extra bytes for head/tail/hash lines
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(output_to_memory.address(), max_out);
+
+ if (RNPLib.rnp_enarmor(input_from_memory, output_to_memory, type)) {
+ throw new Error("rnp_enarmor failed");
+ }
+
+ let result = "";
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ !RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+
+ return result;
+ },
+
+ // Will change the expiration date of all given keys to newExpiry.
+ // fingerprintArray is an array, containing fingerprints, both
+ // primary key fingerprints and subkey fingerprints are allowed.
+ // The function assumes that all involved keys have already been
+ // unlocked. We shouldn't rely on password callbacks for unlocking,
+ // as it would be confusing if only some keys are changed.
+ async changeExpirationDate(fingerprintArray, newExpiry) {
+ for (let fingerprint of fingerprintArray) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + fingerprint
+ );
+
+ if (handle.isNull()) {
+ continue;
+ }
+
+ if (RNPLib.rnp_key_set_expiration(handle, newExpiry)) {
+ throw new Error(`rnp_key_set_expiration failed for ${fingerprint}`);
+ }
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ await this.saveKeyRings();
+ return true;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible key for the given key handles.
+ * If subkey is given, it must refer to an existing encryption subkey.
+ * This is a wrapper around RNP function rnp_key_export_autocrypt.
+ *
+ * @param {rnp_key_handle_t} primHandle - The handle of a primary key.
+ * @param {?rnp_key_handle_t} subHandle - The handle of an encryption subkey or null.
+ * @param {string} uidString - The userID to include.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ getAutocryptKeyB64ByHandle(primHandle, subHandle, userId) {
+ if (primHandle.isNull()) {
+ throw new Error("getAutocryptKeyB64ByHandle invalid parameter");
+ }
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ let result = "";
+
+ if (
+ RNPLib.rnp_key_export_autocrypt(
+ primHandle,
+ subHandle,
+ userId,
+ output_to_memory,
+ 0
+ )
+ ) {
+ console.debug("rnp_key_export_autocrypt failed");
+ } else {
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let rv = RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!rv) {
+ // result_len is of type UInt64, I don't know of a better way
+ // to convert it to an integer.
+ let b_len = parseInt(result_len.value.toString());
+
+ // type casting the pointer type to an array type allows us to
+ // access the elements by index.
+ let uint8_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.uint8_t.array(result_len.value).ptr
+ ).contents;
+
+ let str = "";
+ for (let i = 0; i < b_len; i++) {
+ str += String.fromCharCode(uint8_array[i]);
+ }
+
+ result = btoa(str);
+ }
+ }
+
+ RNPLib.rnp_output_destroy(output_to_memory);
+
+ return result;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible key for the given key ID.
+ * If subKeyId is given, it must refer to an existing encryption subkey.
+ * This is a wrapper around RNP function rnp_key_export_autocrypt.
+ *
+ * @param {string} primaryKeyId - The ID of a primary key.
+ * @param {?string} subKeyId - The ID of an encryption subkey or null.
+ * @param {string} uidString - The userID to include.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ getAutocryptKeyB64(primaryKeyId, subKeyId, uidString) {
+ let subHandle = null;
+
+ if (subKeyId) {
+ subHandle = this.getKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, subKeyId);
+ if (subHandle.isNull()) {
+ // Although subKeyId is optional, if it's given, it must be valid.
+ return "";
+ }
+ }
+
+ let primHandle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ primaryKeyId
+ );
+
+ let result = this.getAutocryptKeyB64ByHandle(
+ primHandle,
+ subHandle,
+ uidString
+ );
+
+ if (!primHandle.isNull()) {
+ RNPLib.rnp_key_handle_destroy(primHandle);
+ }
+ if (subHandle) {
+ RNPLib.rnp_key_handle_destroy(subHandle);
+ }
+ return result;
+ },
+
+ /**
+ * Helper function to produce the string that will be shown to the
+ * user, when the user is asked to unlock a key. If the key is a
+ * subkey, it might help to user to identify the respective key by
+ * also mentioning the key ID of the primary key, so both IDs are
+ * shown when prompting to unlock a subkey.
+ * Parameter nonDefaultFFI is required, if the prompt is related to
+ * a key that isn't (yet) stored in the global storage, for example
+ * a key that is being prepared for import or export in a temporary
+ * ffi space.
+ *
+ * @param {rnp_key_handle_t} handle - produce a passphrase prompt
+ * string based on the properties of this key.
+ * @param {rnp_ffi_t} ffi - the RNP FFI that relates the handle
+ * @returns {String} - a string that asks the user to enter the
+ * passphrase for the given string parameter, including details
+ * that allow the user to identify the key.
+ */
+ async getPassphrasePrompt(handle, ffi) {
+ let parentOfHandle = this.getPrimaryKeyHandleIfSub(ffi, handle);
+ let useThisHandle = !parentOfHandle.isNull() ? parentOfHandle : handle;
+
+ let keyObj = {};
+ if (
+ !this.getKeyInfoFromHandle(ffi, useThisHandle, keyObj, false, true, true)
+ ) {
+ return "";
+ }
+
+ let mainKeyId = keyObj.keyId;
+ let subKeyId;
+ if (!parentOfHandle.isNull()) {
+ subKeyId = this.getKeyIDFromHandle(handle);
+ }
+
+ if (subKeyId) {
+ return l10n.formatValue("passphrase-prompt2-sub", {
+ subkey: subKeyId,
+ key: mainKeyId,
+ date: keyObj.created,
+ username_and_email: keyObj.userId,
+ });
+ }
+ return l10n.formatValue("passphrase-prompt2", {
+ key: mainKeyId,
+ date: keyObj.created,
+ username_and_email: keyObj.userId,
+ });
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/RNPLib.jsm b/comm/mail/extensions/openpgp/content/modules/RNPLib.jsm
new file mode 100644
index 0000000000..58bcb383b5
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/RNPLib.jsm
@@ -0,0 +1,2109 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["RNPLibLoader"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ OpenPGPMasterpass: "chrome://openpgp/content/modules/masterpass.jsm",
+});
+
+const MIN_RNP_VERSION = [0, 17, 0];
+
+var systemOS = Services.appinfo.OS.toLowerCase();
+var abi = ctypes.default_abi;
+
+// Open librnp. Determine the path to the chrome directory and look for it
+// there first. If not, fallback to searching the standard locations.
+var librnp, librnpPath;
+
+function tryLoadRNP(name, suffix) {
+ let filename = ctypes.libraryName(name) + suffix;
+ let binPath = Services.dirsvc.get("XpcomLib", Ci.nsIFile).path;
+ let binDir = PathUtils.parent(binPath);
+ librnpPath = PathUtils.join(binDir, filename);
+
+ try {
+ librnp = ctypes.open(librnpPath);
+ } catch (e) {}
+
+ if (!librnp) {
+ try {
+ // look in standard locations
+ librnpPath = filename;
+ librnp = ctypes.open(librnpPath);
+ } catch (e) {}
+ }
+}
+
+function loadExternalRNPLib() {
+ if (!librnp) {
+ // Try loading librnp.so, librnp.dylib, or rnp.dll first
+ tryLoadRNP("rnp", "");
+ }
+
+ if (!librnp && (systemOS === "winnt" || systemOS === "darwin")) {
+ // rnp.0.dll or rnp.0.dylib
+ tryLoadRNP("rnp.0", "");
+ }
+
+ if (!librnp) {
+ tryLoadRNP("rnp-0", "");
+ }
+
+ if (!librnp && systemOS === "winnt") {
+ // librnp-0.dll
+ tryLoadRNP("librnp-0", "");
+ }
+
+ if (!librnp && !(systemOS === "winnt") && !(systemOS === "darwin")) {
+ // librnp.so.0
+ tryLoadRNP("rnp", ".0");
+ }
+}
+
+var RNPLibLoader = {
+ init() {
+ const required_version_str = `${MIN_RNP_VERSION[0]}.${MIN_RNP_VERSION[1]}.${MIN_RNP_VERSION[2]}`;
+
+ let dummyRNPLib = {
+ loaded: false,
+ loadedOfficial: false,
+ loadStatus: "libs-rnp-status-load-failed",
+ loadErrorReason: "RNP/OpenPGP library failed to load",
+ path: "",
+
+ getRNPLibStatus() {
+ return {
+ min_version: required_version_str,
+ loaded_version: "-",
+ status: this.loadStatus,
+ error: this.loadErrorReason,
+ path: this.path,
+ };
+ },
+ };
+
+ loadExternalRNPLib();
+ if (!librnp) {
+ return dummyRNPLib;
+ }
+
+ try {
+ enableRNPLibJS();
+ } catch (e) {
+ console.log(e);
+ return dummyRNPLib;
+ }
+
+ const rnp_version_str =
+ RNPLib.rnp_version_string_full().readStringReplaceMalformed();
+ RNPLib.loadedVersion = rnp_version_str;
+ RNPLib.expectedVersion = required_version_str;
+
+ let hasRequiredVersion = RNPLib.check_required_version();
+
+ if (!hasRequiredVersion) {
+ RNPLib.loadErrorReason = `RNP version ${rnp_version_str} does not meet minimum required ${required_version_str}.`;
+ RNPLib.loadStatus = "libs-rnp-status-incompatible";
+ return RNPLib;
+ }
+
+ RNPLib.loaded = true;
+
+ let hasOfficialVersion =
+ rnp_version_str.includes(".MZLA") ||
+ rnp_version_str.match("^[0-9]+.[0-9]+.[0-9]+(.[0-9]+)?$");
+ if (!hasOfficialVersion) {
+ RNPLib.loadErrorReason = `RNP reports unexpected version information, it's considered an unofficial version with unknown capabilities.`;
+ RNPLib.loadStatus = "libs-rnp-status-unofficial";
+ } else {
+ RNPLib.loadedOfficial = true;
+ }
+
+ return RNPLib;
+ },
+};
+
+const rnp_result_t = ctypes.uint32_t;
+const rnp_ffi_t = ctypes.void_t.ptr;
+const rnp_input_t = ctypes.void_t.ptr;
+const rnp_output_t = ctypes.void_t.ptr;
+const rnp_key_handle_t = ctypes.void_t.ptr;
+const rnp_uid_handle_t = ctypes.void_t.ptr;
+const rnp_identifier_iterator_t = ctypes.void_t.ptr;
+const rnp_op_generate_t = ctypes.void_t.ptr;
+const rnp_op_encrypt_t = ctypes.void_t.ptr;
+const rnp_op_sign_t = ctypes.void_t.ptr;
+const rnp_op_sign_signature_t = ctypes.void_t.ptr;
+const rnp_op_verify_t = ctypes.void_t.ptr;
+const rnp_op_verify_signature_t = ctypes.void_t.ptr;
+const rnp_signature_handle_t = ctypes.void_t.ptr;
+const rnp_recipient_handle_t = ctypes.void_t.ptr;
+const rnp_symenc_handle_t = ctypes.void_t.ptr;
+
+const rnp_password_cb_t = ctypes.FunctionType(abi, ctypes.bool, [
+ rnp_ffi_t,
+ ctypes.void_t.ptr,
+ rnp_key_handle_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.size_t,
+]).ptr;
+
+const rnp_key_signatures_cb = ctypes.FunctionType(abi, ctypes.void_t, [
+ rnp_ffi_t,
+ ctypes.void_t.ptr,
+ rnp_signature_handle_t,
+ ctypes.uint32_t.ptr,
+]).ptr;
+
+var RNPLib;
+
+function enableRNPLibJS() {
+ // this must be delayed until after "librnp" is initialized
+
+ RNPLib = {
+ loaded: false,
+ loadedOfficial: false,
+ loadStatus: "",
+ loadErrorReason: "",
+ expectedVersion: "",
+ loadedVersion: "",
+
+ getRNPLibStatus() {
+ return {
+ min_version: this.expectedVersion,
+ loaded_version: this.loadedVersion,
+ status:
+ this.loaded && this.loadedOfficial
+ ? "libs-rnp-status-ok"
+ : this.loadStatus,
+ error: this.loadErrorReason,
+ path: this.path,
+ };
+ },
+
+ path: librnpPath,
+
+ // Handle to the RNP library and primary key data store.
+ // Kept at null if init fails.
+ ffi: null,
+
+ // returns rnp_input_t, destroy using rnp_input_destroy
+ async createInputFromPath(path) {
+ // IOUtils.read always returns an array.
+ let u8 = await IOUtils.read(path);
+ if (!u8.length) {
+ return null;
+ }
+
+ let input_from_memory = new this.rnp_input_t();
+ try {
+ this.rnp_input_from_memory(
+ input_from_memory.address(),
+ u8,
+ u8.length,
+ false
+ );
+ } catch (ex) {
+ throw new Error("rnp_input_from_memory for file " + path + " failed");
+ }
+ return input_from_memory;
+ },
+
+ getFilenames() {
+ let secFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ secFile.append("secring.gpg");
+ let pubFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ pubFile.append("pubring.gpg");
+
+ let secRingPath = secFile.path;
+ let pubRingPath = pubFile.path;
+
+ return { pubRingPath, secRingPath };
+ },
+
+ /**
+ * Load a keyring file into the global ffi context.
+ *
+ * @param {string} filename - The file to load
+ * @param keyringFlag - either RNP_LOAD_SAVE_PUBLIC_KEYS
+ * or RNP_LOAD_SAVE_SECRET_KEYS
+ */
+ async loadFile(filename, keyringFlag) {
+ let in_file = await this.createInputFromPath(filename);
+ if (in_file) {
+ this.rnp_load_keys(this.ffi, "GPG", in_file, keyringFlag);
+ this.rnp_input_destroy(in_file);
+ }
+ },
+
+ /**
+ * Load a keyring file into the global ffi context.
+ * If the file couldn't be opened, fall back to a backup file,
+ * by appending ".old" to filename.
+ *
+ * @param {string} filename - The file to load
+ * @param keyringFlag - either RNP_LOAD_SAVE_PUBLIC_KEYS
+ * or RNP_LOAD_SAVE_SECRET_KEYS
+ */
+ async loadWithFallback(filename, keyringFlag) {
+ let loadBackup = false;
+ try {
+ await this.loadFile(filename, keyringFlag);
+ } catch (ex) {
+ if (DOMException.isInstance(ex)) {
+ loadBackup = true;
+ }
+ }
+ if (loadBackup) {
+ filename += ".old";
+ try {
+ await this.loadFile(filename, keyringFlag);
+ } catch (ex) {}
+ }
+ },
+
+ async _fixUnprotectedKeys() {
+ // Bug 1710290, protect all unprotected keys.
+ // To do so, we require that the user has already unlocked
+ // by entering the global primary password, if it is set.
+ // Ensure that other repairing is done first, if necessary,
+ // as handled by masterpass.jsm (OpenPGP automatic password).
+
+ // Note we have two failure scenarios, either a failure, or
+ // retrieveOpenPGPPassword() returning null (that function
+ // might fail because of inconsistencies or corruption).
+ let canRepair = false;
+ try {
+ console.log("Trying to automatically protect the unprotected keys.");
+ let mp = await lazy.OpenPGPMasterpass.retrieveOpenPGPPassword();
+ if (mp) {
+ await RNPLib.protectUnprotectedKeys();
+ await RNPLib.saveKeys();
+ canRepair = true;
+ console.log("Successfully protected the unprotected keys.");
+ let [prot, unprot] = RNPLib.getProtectedKeysCount();
+ console.debug(
+ `Found (${prot} protected and ${unprot} unprotected secret keys.`
+ );
+ }
+ } catch (ex) {
+ console.log(ex);
+ }
+
+ if (!canRepair) {
+ console.log("Cannot protect the unprotected keys at this time.");
+ }
+ },
+
+ check_required_version() {
+ const min_version = this.rnp_version_for(...MIN_RNP_VERSION);
+ const this_version = this.rnp_version();
+ return Boolean(this_version >= min_version);
+ },
+
+ /**
+ * Prepare an RNP library handle, and in addition set all the
+ * application's preferences for library behavior.
+ *
+ * Other application code should NOT call rnp_ffi_create directly,
+ * but obtain an RNP library handle from this function.
+ */
+ prepare_ffi() {
+ let ffi = new rnp_ffi_t();
+ if (this._rnp_ffi_create(ffi.address(), "GPG", "GPG")) {
+ return null;
+ }
+
+ // Treat MD5 as insecure.
+ if (
+ this.rnp_add_security_rule(
+ ffi,
+ this.RNP_FEATURE_HASH_ALG,
+ this.RNP_ALGNAME_MD5,
+ this.RNP_SECURITY_OVERRIDE,
+ 0,
+ this.RNP_SECURITY_INSECURE
+ )
+ ) {
+ return null;
+ }
+
+ // Use RNP's default rule for SHA1 used with data signatures,
+ // and use our override to allow it for key signatures.
+ if (
+ this.rnp_add_security_rule(
+ ffi,
+ this.RNP_FEATURE_HASH_ALG,
+ this.RNP_ALGNAME_SHA1,
+ this.RNP_SECURITY_VERIFY_KEY | this.RNP_SECURITY_OVERRIDE,
+ 0,
+ this.RNP_SECURITY_DEFAULT
+ )
+ ) {
+ return null;
+ }
+
+ /*
+ // Security rules API does not yet support PK and SYMM algs.
+ //
+ // If a hash algorithm is already disabled at build time,
+ // and an attempt is made to set a security rule for that
+ // algorithm, then RNP returns a failure.
+ //
+ // Ideally, RNP should allow these calls (regardless of build time
+ // settings) to define an application security rule, that is
+ // independent of the configuration used for building the
+ // RNP library.
+
+ if (
+ this.rnp_add_security_rule(
+ ffi,
+ this.RNP_FEATURE_HASH_ALG,
+ this.RNP_ALGNAME_SM3,
+ this.RNP_SECURITY_OVERRIDE,
+ 0,
+ this.RNP_SECURITY_PROHIBITED
+ )
+ ) {
+ return null;
+ }
+
+ if (
+ this.rnp_add_security_rule(
+ ffi,
+ this.RNP_FEATURE_PK_ALG,
+ this.RNP_ALGNAME_SM2,
+ this.RNP_SECURITY_OVERRIDE,
+ 0,
+ this.RNP_SECURITY_PROHIBITED
+ )
+ ) {
+ return null;
+ }
+
+ if (
+ this.rnp_add_security_rule(
+ ffi,
+ this.RNP_FEATURE_SYMM_ALG,
+ this.RNP_ALGNAME_SM4,
+ this.RNP_SECURITY_OVERRIDE,
+ 0,
+ this.RNP_SECURITY_PROHIBITED
+ )
+ ) {
+ return null;
+ }
+ */
+
+ return ffi;
+ },
+
+ /**
+ * Test the correctness of security rules, in particular, test
+ * if the given hash algorithm is allowed at the given time.
+ *
+ * This is an application consistency test. If the behavior isn't
+ * according to the expectation, the function throws an error.
+ *
+ * @param {string} hashAlg - Test this hash algorithm
+ * @param {time_t} time - Test status at this timestamp
+ * @param {boolean} keySigAllowed - Test if using the hash algorithm
+ * is allowed for signatures found inside OpenPGP keys.
+ * @param {boolean} dataSigAllowed - Test if using the hash algorithm
+ * is allowed for signatures on data.
+ */
+ _confirmSecurityRule(hashAlg, time, keySigAllowed, dataSigAllowed) {
+ let level = new ctypes.uint32_t();
+ let flag = new ctypes.uint32_t();
+
+ flag.value = this.RNP_SECURITY_VERIFY_DATA;
+ let testDataSuccess = false;
+ if (
+ !RNPLib.rnp_get_security_rule(
+ this.ffi,
+ this.RNP_FEATURE_HASH_ALG,
+ hashAlg,
+ time,
+ flag.address(),
+ null,
+ level.address()
+ )
+ ) {
+ if (dataSigAllowed) {
+ testDataSuccess = level.value == RNPLib.RNP_SECURITY_DEFAULT;
+ } else {
+ testDataSuccess = level.value < RNPLib.RNP_SECURITY_DEFAULT;
+ }
+ }
+
+ if (!testDataSuccess) {
+ throw new Error("security configuration for data signatures failed");
+ }
+
+ flag.value = this.RNP_SECURITY_VERIFY_KEY;
+ let testKeySuccess = false;
+ if (
+ !RNPLib.rnp_get_security_rule(
+ this.ffi,
+ this.RNP_FEATURE_HASH_ALG,
+ hashAlg,
+ time,
+ flag.address(),
+ null,
+ level.address()
+ )
+ ) {
+ if (keySigAllowed) {
+ testKeySuccess = level.value == RNPLib.RNP_SECURITY_DEFAULT;
+ } else {
+ testKeySuccess = level.value < RNPLib.RNP_SECURITY_DEFAULT;
+ }
+ }
+
+ if (!testKeySuccess) {
+ throw new Error("security configuration for key signatures failed");
+ }
+ },
+
+ /**
+ * Perform tests that the RNP library behaves according to the
+ * defined security rules.
+ * If a problem is found, the function throws an error.
+ */
+ _sanityCheckSecurityRules() {
+ let time_t_now = Math.round(Date.now() / 1000);
+ let ten_years_in_seconds = 10 * 365 * 24 * 60 * 60;
+ let ten_years_future = time_t_now + ten_years_in_seconds;
+
+ this._confirmSecurityRule(this.RNP_ALGNAME_MD5, time_t_now, false, false);
+ this._confirmSecurityRule(
+ this.RNP_ALGNAME_MD5,
+ ten_years_future,
+ false,
+ false
+ );
+
+ this._confirmSecurityRule(this.RNP_ALGNAME_SHA1, time_t_now, true, false);
+ this._confirmSecurityRule(
+ this.RNP_ALGNAME_SHA1,
+ ten_years_future,
+ true,
+ false
+ );
+ },
+
+ async init() {
+ this.ffi = this.prepare_ffi();
+ if (!this.ffi) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ this.rnp_ffi_set_log_fd(this.ffi, 2); // stderr
+
+ this.keep_password_cb_alive = rnp_password_cb_t(
+ this.password_cb,
+ this, // this value used while executing callback
+ false // callback return value if exception is thrown
+ );
+ this.rnp_ffi_set_pass_provider(
+ this.ffi,
+ this.keep_password_cb_alive,
+ null
+ );
+
+ let { pubRingPath, secRingPath } = this.getFilenames();
+
+ try {
+ this._sanityCheckSecurityRules();
+ } catch (e) {
+ // Disable all RNP operation
+ this.ffi = null;
+ throw e;
+ }
+
+ await this.loadWithFallback(pubRingPath, this.RNP_LOAD_SAVE_PUBLIC_KEYS);
+ await this.loadWithFallback(secRingPath, this.RNP_LOAD_SAVE_SECRET_KEYS);
+
+ let pubnum = new ctypes.size_t();
+ this.rnp_get_public_key_count(this.ffi, pubnum.address());
+
+ let secnum = new ctypes.size_t();
+ this.rnp_get_secret_key_count(this.ffi, secnum.address());
+
+ let [prot, unprot] = this.getProtectedKeysCount();
+ console.debug(
+ `Found ${pubnum.value} public keys and ${secnum.value} secret keys (${prot} protected, ${unprot} unprotected)`
+ );
+
+ if (unprot) {
+ // We need automatic repair, which can involve a primary password
+ // prompt. Let's use a short timer, so we keep it out of the
+ // early startup code.
+ console.log(
+ "Will attempt to automatically protect the unprotected keys in 30 seconds"
+ );
+ lazy.setTimeout(RNPLib._fixUnprotectedKeys, 30000);
+ }
+ return true;
+ },
+
+ /**
+ * Returns two numbers, the number of protected and unprotected keys.
+ * Because we use an automatic password for all secret keys
+ * (regardless of a primary password being used),
+ * the number of unprotected keys should be zero.
+ */
+ getProtectedKeysCount() {
+ let prot = 0;
+ let unprot = 0;
+
+ let iter = new RNPLib.rnp_identifier_iterator_t();
+ let grip = new ctypes.char.ptr();
+
+ if (
+ RNPLib.rnp_identifier_iterator_create(
+ RNPLib.ffi,
+ iter.address(),
+ "grip"
+ )
+ ) {
+ throw new Error("rnp_identifier_iterator_create failed");
+ }
+
+ while (
+ !RNPLib.rnp_identifier_iterator_next(iter, grip.address()) &&
+ !grip.isNull()
+ ) {
+ let handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_locate_key(RNPLib.ffi, "grip", grip, handle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+
+ if (this.getSecretAvailableFromHandle(handle)) {
+ let is_protected = new ctypes.bool();
+ if (RNPLib.rnp_key_is_protected(handle, is_protected.address())) {
+ throw new Error("rnp_key_is_protected failed");
+ }
+ if (is_protected.value) {
+ prot++;
+ } else {
+ unprot++;
+ }
+ }
+
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ RNPLib.rnp_identifier_iterator_destroy(iter);
+ return [prot, unprot];
+ },
+
+ getSecretAvailableFromHandle(handle) {
+ let have_secret = new ctypes.bool();
+ if (RNPLib.rnp_key_have_secret(handle, have_secret.address())) {
+ throw new Error("rnp_key_have_secret failed");
+ }
+ return have_secret.value;
+ },
+
+ /**
+ * If the given secret key is a pseudo secret key, which doesn't
+ * contain the underlying key material, then return false.
+ *
+ * Only call this function if getSecretAvailableFromHandle returns
+ * true for the given handle (which means it claims to contain a
+ * secret key).
+ *
+ * @param {rnp_key_handle_t} handle - handle of the key to query
+ * @returns {boolean} - true if secret key material is available
+ *
+ */
+ isSecretKeyMaterialAvailable(handle) {
+ let protection_type = new ctypes.char.ptr();
+ if (
+ RNPLib.rnp_key_get_protection_type(handle, protection_type.address())
+ ) {
+ throw new Error("rnp_key_get_protection_type failed");
+ }
+ let result;
+ switch (protection_type.readString()) {
+ case "GPG-None":
+ case "GPG-Smartcard":
+ case "Unknown":
+ result = false;
+ break;
+ default:
+ result = true;
+ break;
+ }
+ RNPLib.rnp_buffer_destroy(protection_type);
+ return result;
+ },
+
+ async protectUnprotectedKeys() {
+ let iter = new RNPLib.rnp_identifier_iterator_t();
+ let grip = new ctypes.char.ptr();
+
+ let newPass = await lazy.OpenPGPMasterpass.retrieveOpenPGPPassword();
+
+ if (
+ RNPLib.rnp_identifier_iterator_create(
+ RNPLib.ffi,
+ iter.address(),
+ "grip"
+ )
+ ) {
+ throw new Error("rnp_identifier_iterator_create failed");
+ }
+
+ while (
+ !RNPLib.rnp_identifier_iterator_next(iter, grip.address()) &&
+ !grip.isNull()
+ ) {
+ let handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_locate_key(RNPLib.ffi, "grip", grip, handle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+
+ if (RNPLib.getSecretAvailableFromHandle(handle)) {
+ let is_protected = new ctypes.bool();
+ if (RNPLib.rnp_key_is_protected(handle, is_protected.address())) {
+ throw new Error("rnp_key_is_protected failed");
+ }
+ if (!is_protected.value) {
+ RNPLib.protectKeyWithSubKeys(handle, newPass);
+ }
+ }
+
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ RNPLib.rnp_identifier_iterator_destroy(iter);
+ },
+
+ protectKeyWithSubKeys(handle, newPass) {
+ if (RNPLib.isSecretKeyMaterialAvailable(handle)) {
+ if (RNPLib.rnp_key_protect(handle, newPass, null, null, null, 0)) {
+ throw new Error("rnp_key_protect failed");
+ }
+ }
+
+ let sub_count = new ctypes.size_t();
+ if (RNPLib.rnp_key_get_subkey_count(handle, sub_count.address())) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_key_get_subkey_at(handle, i, sub_handle.address())) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+ if (
+ RNPLib.getSecretAvailableFromHandle(sub_handle) &&
+ RNPLib.isSecretKeyMaterialAvailable(sub_handle)
+ ) {
+ if (
+ RNPLib.rnp_key_protect(sub_handle, newPass, null, null, null, 0)
+ ) {
+ throw new Error("rnp_key_protect failed");
+ }
+ }
+ RNPLib.rnp_key_handle_destroy(sub_handle);
+ }
+ },
+
+ /**
+ * Save keyring file to the given path.
+ *
+ * @param {string} path - The file path to save to.
+ * @param {number} keyRingFlag - RNP_LOAD_SAVE_PUBLIC_KEYS or
+ * RNP_LOAD_SAVE_SECRET_KEYS.
+ */
+ async saveKeyRing(path, keyRingFlag) {
+ if (!this.ffi) {
+ return;
+ }
+
+ let oldPath = path + ".old";
+
+ // Ignore failure, oldPath might not exist yet.
+ await IOUtils.copy(path, oldPath).catch(() => {});
+
+ let u8 = null;
+ let keyCount = new ctypes.size_t();
+
+ if (keyRingFlag == this.RNP_LOAD_SAVE_SECRET_KEYS) {
+ this.rnp_get_secret_key_count(this.ffi, keyCount.address());
+ } else {
+ this.rnp_get_public_key_count(this.ffi, keyCount.address());
+ }
+
+ let keyCountNum = parseInt(keyCount.value.toString());
+ if (keyCountNum) {
+ let rnp_out = new this.rnp_output_t();
+ if (this.rnp_output_to_memory(rnp_out.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+ if (this.rnp_save_keys(this.ffi, "GPG", rnp_out, keyRingFlag)) {
+ throw new Error("rnp_save_keys failed");
+ }
+
+ let result_buf = new ctypes.uint8_t.ptr();
+ let result_len = new ctypes.size_t();
+
+ // Parameter false means "don't copy rnp_out to result_buf",
+ // rather a reference to the memory is used. Be careful to
+ // destroy rnp_out after we're done with the data.
+ if (
+ this.rnp_output_memory_get_buf(
+ rnp_out,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ } else {
+ let uint8_array = ctypes.cast(
+ result_buf,
+ ctypes.uint8_t.array(result_len.value).ptr
+ ).contents;
+ // This call creates a copy of the data, it should be
+ // safe to destroy rnp_out afterwards.
+ u8 = uint8_array.readTypedArray();
+ }
+ this.rnp_output_destroy(rnp_out);
+ }
+
+ u8 = u8 || new Uint8Array();
+
+ await IOUtils.write(path, u8, {
+ tmpPath: path + ".tmp-new",
+ });
+ },
+
+ async saveKeys() {
+ if (!this.ffi) {
+ return;
+ }
+ let { pubRingPath, secRingPath } = this.getFilenames();
+
+ let saveThem = async () => {
+ await this.saveKeyRing(pubRingPath, this.RNP_LOAD_SAVE_PUBLIC_KEYS);
+ await this.saveKeyRing(secRingPath, this.RNP_LOAD_SAVE_SECRET_KEYS);
+ };
+ let saveBlocker = saveThem();
+ IOUtils.profileBeforeChange.addBlocker(
+ "OpenPGP: writing out keyring",
+ saveBlocker
+ );
+ await saveBlocker;
+ IOUtils.profileBeforeChange.removeBlocker(saveBlocker);
+ },
+
+ keep_password_cb_alive: null,
+
+ cached_pw: null,
+
+ /**
+ * Past versions of Thunderbird used this callback to provide
+ * the automatically managed passphrase to RNP, which was used
+ * for all OpenPGP. Nowadays, Thunderbird supports the definition
+ * of used-defined passphrase. To better control the unlocking of
+ * keys, Thunderbird no longer uses this callback.
+ * The application is designed to unlock secret keys as needed,
+ * prior to calling the respective RNP APIs.
+ * If this callback is reached anyway, it's an internal error,
+ * it means that some Thunderbird code hasn't properly unlocked
+ * the required key yet.
+ *
+ * This is a C callback from an external library, so we cannot
+ * rely on the usual JS throw mechanism to abort this operation.
+ */
+ password_cb(ffi, app_ctx, key, pgp_context, buf, buf_len) {
+ let fingerprint = new ctypes.char.ptr();
+ let fpStr;
+ if (!RNPLib.rnp_key_get_fprint(key, fingerprint.address())) {
+ fpStr = "Fingerprint: " + fingerprint.readString();
+ }
+ RNPLib.rnp_buffer_destroy(fingerprint);
+
+ console.debug(
+ `Internal error, RNP password callback called unexpectedly. ${fpStr}.`
+ );
+ return false;
+ },
+
+ // For comparing version numbers
+ rnp_version_for: librnp.declare(
+ "rnp_version_for",
+ abi,
+ ctypes.uint32_t,
+ ctypes.uint32_t, // major
+ ctypes.uint32_t, // minor
+ ctypes.uint32_t // patch
+ ),
+
+ // Get the library version.
+ rnp_version: librnp.declare("rnp_version", abi, ctypes.uint32_t),
+
+ rnp_version_string_full: librnp.declare(
+ "rnp_version_string_full",
+ abi,
+ ctypes.char.ptr
+ ),
+
+ // Get a RNP library handle.
+ // Mark with leading underscore, to clarify that this function
+ // shouldn't be called directly - you should call prepare_ffi().
+ _rnp_ffi_create: librnp.declare(
+ "rnp_ffi_create",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_ffi_destroy: librnp.declare(
+ "rnp_ffi_destroy",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t
+ ),
+
+ rnp_ffi_set_log_fd: librnp.declare(
+ "rnp_ffi_set_log_fd",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.int
+ ),
+
+ rnp_get_public_key_count: librnp.declare(
+ "rnp_get_public_key_count",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_get_secret_key_count: librnp.declare(
+ "rnp_get_secret_key_count",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_input_from_path: librnp.declare(
+ "rnp_input_from_path",
+ abi,
+ rnp_result_t,
+ rnp_input_t.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_input_from_memory: librnp.declare(
+ "rnp_input_from_memory",
+ abi,
+ rnp_result_t,
+ rnp_input_t.ptr,
+ ctypes.uint8_t.ptr,
+ ctypes.size_t,
+ ctypes.bool
+ ),
+
+ rnp_output_to_memory: librnp.declare(
+ "rnp_output_to_memory",
+ abi,
+ rnp_result_t,
+ rnp_output_t.ptr,
+ ctypes.size_t
+ ),
+
+ rnp_output_to_path: librnp.declare(
+ "rnp_output_to_path",
+ abi,
+ rnp_result_t,
+ rnp_output_t.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_decrypt: librnp.declare(
+ "rnp_decrypt",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ rnp_input_t,
+ rnp_output_t
+ ),
+
+ rnp_output_memory_get_buf: librnp.declare(
+ "rnp_output_memory_get_buf",
+ abi,
+ rnp_result_t,
+ rnp_output_t,
+ ctypes.uint8_t.ptr.ptr,
+ ctypes.size_t.ptr,
+ ctypes.bool
+ ),
+
+ rnp_input_destroy: librnp.declare(
+ "rnp_input_destroy",
+ abi,
+ rnp_result_t,
+ rnp_input_t
+ ),
+
+ rnp_output_destroy: librnp.declare(
+ "rnp_output_destroy",
+ abi,
+ rnp_result_t,
+ rnp_output_t
+ ),
+
+ rnp_load_keys: librnp.declare(
+ "rnp_load_keys",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.char.ptr,
+ rnp_input_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_save_keys: librnp.declare(
+ "rnp_save_keys",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.char.ptr,
+ rnp_output_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_ffi_set_pass_provider: librnp.declare(
+ "rnp_ffi_set_pass_provider",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ rnp_password_cb_t,
+ ctypes.void_t.ptr
+ ),
+
+ rnp_identifier_iterator_create: librnp.declare(
+ "rnp_identifier_iterator_create",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ rnp_identifier_iterator_t.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_identifier_iterator_next: librnp.declare(
+ "rnp_identifier_iterator_next",
+ abi,
+ rnp_result_t,
+ rnp_identifier_iterator_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_identifier_iterator_destroy: librnp.declare(
+ "rnp_identifier_iterator_destroy",
+ abi,
+ rnp_result_t,
+ rnp_identifier_iterator_t
+ ),
+
+ rnp_locate_key: librnp.declare(
+ "rnp_locate_key",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ rnp_key_handle_t.ptr
+ ),
+
+ rnp_key_handle_destroy: librnp.declare(
+ "rnp_key_handle_destroy",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t
+ ),
+
+ rnp_key_allows_usage: librnp.declare(
+ "rnp_key_allows_usage",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_is_sub: librnp.declare(
+ "rnp_key_is_sub",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_is_primary: librnp.declare(
+ "rnp_key_is_primary",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_have_secret: librnp.declare(
+ "rnp_key_have_secret",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_have_public: librnp.declare(
+ "rnp_key_have_public",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_get_fprint: librnp.declare(
+ "rnp_key_get_fprint",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_get_keyid: librnp.declare(
+ "rnp_key_get_keyid",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_get_alg: librnp.declare(
+ "rnp_key_get_alg",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_get_grip: librnp.declare(
+ "rnp_key_get_grip",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_get_primary_grip: librnp.declare(
+ "rnp_key_get_primary_grip",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_is_revoked: librnp.declare(
+ "rnp_key_is_revoked",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_buffer_destroy: librnp.declare(
+ "rnp_buffer_destroy",
+ abi,
+ ctypes.void_t,
+ ctypes.void_t.ptr
+ ),
+
+ rnp_key_get_subkey_count: librnp.declare(
+ "rnp_key_get_subkey_count",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_key_get_subkey_at: librnp.declare(
+ "rnp_key_get_subkey_at",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.size_t,
+ rnp_key_handle_t.ptr
+ ),
+
+ rnp_key_get_creation: librnp.declare(
+ "rnp_key_get_creation",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_key_get_expiration: librnp.declare(
+ "rnp_key_get_expiration",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_key_get_bits: librnp.declare(
+ "rnp_key_get_bits",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_key_get_uid_count: librnp.declare(
+ "rnp_key_get_uid_count",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_key_get_primary_uid: librnp.declare(
+ "rnp_key_get_primary_uid",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_get_uid_at: librnp.declare(
+ "rnp_key_get_uid_at",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.size_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_get_uid_handle_at: librnp.declare(
+ "rnp_key_get_uid_handle_at",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.size_t,
+ rnp_uid_handle_t.ptr
+ ),
+
+ rnp_uid_handle_destroy: librnp.declare(
+ "rnp_uid_handle_destroy",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t
+ ),
+
+ rnp_uid_is_revoked: librnp.declare(
+ "rnp_uid_is_revoked",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_unlock: librnp.declare(
+ "rnp_key_unlock",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_key_lock: librnp.declare(
+ "rnp_key_lock",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t
+ ),
+
+ rnp_key_unprotect: librnp.declare(
+ "rnp_key_unprotect",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_key_protect: librnp.declare(
+ "rnp_key_protect",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.size_t
+ ),
+
+ rnp_key_is_protected: librnp.declare(
+ "rnp_key_is_protected",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_is_locked: librnp.declare(
+ "rnp_key_is_locked",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_op_generate_create: librnp.declare(
+ "rnp_op_generate_create",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t.ptr,
+ rnp_ffi_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_generate_subkey_create: librnp.declare(
+ "rnp_op_generate_subkey_create",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t.ptr,
+ rnp_ffi_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_generate_set_bits: librnp.declare(
+ "rnp_op_generate_set_bits",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_op_generate_set_curve: librnp.declare(
+ "rnp_op_generate_set_curve",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_generate_set_protection_password: librnp.declare(
+ "rnp_op_generate_set_protection_password",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_generate_set_userid: librnp.declare(
+ "rnp_op_generate_set_userid",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_generate_set_expiration: librnp.declare(
+ "rnp_op_generate_set_expiration",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_op_generate_execute: librnp.declare(
+ "rnp_op_generate_execute",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t
+ ),
+
+ rnp_op_generate_get_key: librnp.declare(
+ "rnp_op_generate_get_key",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t,
+ rnp_key_handle_t.ptr
+ ),
+
+ rnp_op_generate_destroy: librnp.declare(
+ "rnp_op_generate_destroy",
+ abi,
+ rnp_result_t,
+ rnp_op_generate_t
+ ),
+
+ rnp_guess_contents: librnp.declare(
+ "rnp_guess_contents",
+ abi,
+ rnp_result_t,
+ rnp_input_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_import_signatures: librnp.declare(
+ "rnp_import_signatures",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ rnp_input_t,
+ ctypes.uint32_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_import_keys: librnp.declare(
+ "rnp_import_keys",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ rnp_input_t,
+ ctypes.uint32_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_key_remove: librnp.declare(
+ "rnp_key_remove",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_uid_remove: librnp.declare(
+ "rnp_uid_remove",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ rnp_uid_handle_t
+ ),
+
+ rnp_key_remove_signatures: librnp.declare(
+ "rnp_key_remove_signatures",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t,
+ rnp_key_signatures_cb,
+ ctypes.void_t.ptr
+ ),
+
+ rnp_op_encrypt_create: librnp.declare(
+ "rnp_op_encrypt_create",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t.ptr,
+ rnp_ffi_t,
+ rnp_input_t,
+ rnp_output_t
+ ),
+
+ rnp_op_sign_cleartext_create: librnp.declare(
+ "rnp_op_sign_cleartext_create",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t.ptr,
+ rnp_ffi_t,
+ rnp_input_t,
+ rnp_output_t
+ ),
+
+ rnp_op_sign_detached_create: librnp.declare(
+ "rnp_op_sign_detached_create",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t.ptr,
+ rnp_ffi_t,
+ rnp_input_t,
+ rnp_output_t
+ ),
+
+ rnp_op_encrypt_add_recipient: librnp.declare(
+ "rnp_op_encrypt_add_recipient",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ rnp_key_handle_t
+ ),
+
+ rnp_op_encrypt_add_signature: librnp.declare(
+ "rnp_op_encrypt_add_signature",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ rnp_key_handle_t,
+ rnp_op_sign_signature_t.ptr
+ ),
+
+ rnp_op_sign_add_signature: librnp.declare(
+ "rnp_op_sign_add_signature",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t,
+ rnp_key_handle_t,
+ rnp_op_sign_signature_t.ptr
+ ),
+
+ rnp_op_encrypt_set_armor: librnp.declare(
+ "rnp_op_encrypt_set_armor",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ ctypes.bool
+ ),
+
+ rnp_op_sign_set_armor: librnp.declare(
+ "rnp_op_sign_set_armor",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t,
+ ctypes.bool
+ ),
+
+ rnp_op_encrypt_set_hash: librnp.declare(
+ "rnp_op_encrypt_set_hash",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_sign_set_hash: librnp.declare(
+ "rnp_op_sign_set_hash",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_encrypt_set_cipher: librnp.declare(
+ "rnp_op_encrypt_set_cipher",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_sign_execute: librnp.declare(
+ "rnp_op_sign_execute",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t
+ ),
+
+ rnp_op_sign_destroy: librnp.declare(
+ "rnp_op_sign_destroy",
+ abi,
+ rnp_result_t,
+ rnp_op_sign_t
+ ),
+
+ rnp_op_encrypt_execute: librnp.declare(
+ "rnp_op_encrypt_execute",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t
+ ),
+
+ rnp_op_encrypt_destroy: librnp.declare(
+ "rnp_op_encrypt_destroy",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t
+ ),
+
+ rnp_key_export: librnp.declare(
+ "rnp_key_export",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ rnp_output_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_key_export_revocation: librnp.declare(
+ "rnp_key_export_revocation",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ rnp_output_t,
+ ctypes.uint32_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_output_to_armor: librnp.declare(
+ "rnp_output_to_armor",
+ abi,
+ rnp_result_t,
+ rnp_output_t,
+ rnp_output_t.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_output_finish: librnp.declare(
+ "rnp_output_finish",
+ abi,
+ rnp_result_t,
+ rnp_output_t
+ ),
+
+ rnp_op_verify_create: librnp.declare(
+ "rnp_op_verify_create",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t.ptr,
+ rnp_ffi_t,
+ rnp_input_t,
+ rnp_output_t
+ ),
+
+ rnp_op_verify_detached_create: librnp.declare(
+ "rnp_op_verify_detached_create",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t.ptr,
+ rnp_ffi_t,
+ rnp_input_t,
+ rnp_input_t
+ ),
+
+ rnp_op_verify_execute: librnp.declare(
+ "rnp_op_verify_execute",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t
+ ),
+
+ rnp_op_verify_destroy: librnp.declare(
+ "rnp_op_verify_destroy",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t
+ ),
+
+ rnp_op_verify_get_signature_count: librnp.declare(
+ "rnp_op_verify_get_signature_count",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_op_verify_get_signature_at: librnp.declare(
+ "rnp_op_verify_get_signature_at",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.size_t,
+ rnp_op_verify_signature_t.ptr
+ ),
+
+ rnp_op_verify_signature_get_handle: librnp.declare(
+ "rnp_op_verify_signature_get_handle",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_signature_t,
+ rnp_signature_handle_t.ptr
+ ),
+
+ rnp_op_verify_signature_get_status: librnp.declare(
+ "rnp_op_verify_signature_get_status",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_signature_t
+ ),
+
+ rnp_op_verify_signature_get_key: librnp.declare(
+ "rnp_op_verify_signature_get_key",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_signature_t,
+ rnp_key_handle_t.ptr
+ ),
+
+ rnp_op_verify_signature_get_times: librnp.declare(
+ "rnp_op_verify_signature_get_times",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_signature_t,
+ ctypes.uint32_t.ptr,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_uid_get_signature_count: librnp.declare(
+ "rnp_uid_get_signature_count",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_uid_get_signature_at: librnp.declare(
+ "rnp_uid_get_signature_at",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t,
+ ctypes.size_t,
+ rnp_signature_handle_t.ptr
+ ),
+
+ rnp_key_get_signature_count: librnp.declare(
+ "rnp_key_get_signature_count",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_key_get_signature_at: librnp.declare(
+ "rnp_key_get_signature_at",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.size_t,
+ rnp_signature_handle_t.ptr
+ ),
+
+ rnp_signature_get_hash_alg: librnp.declare(
+ "rnp_signature_get_hash_alg",
+ abi,
+ rnp_result_t,
+ rnp_signature_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_signature_get_creation: librnp.declare(
+ "rnp_signature_get_creation",
+ abi,
+ rnp_result_t,
+ rnp_signature_handle_t,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_signature_get_keyid: librnp.declare(
+ "rnp_signature_get_keyid",
+ abi,
+ rnp_result_t,
+ rnp_signature_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_signature_get_signer: librnp.declare(
+ "rnp_signature_get_signer",
+ abi,
+ rnp_result_t,
+ rnp_signature_handle_t,
+ rnp_key_handle_t.ptr
+ ),
+
+ rnp_signature_handle_destroy: librnp.declare(
+ "rnp_signature_handle_destroy",
+ abi,
+ rnp_result_t,
+ rnp_signature_handle_t
+ ),
+
+ rnp_enarmor: librnp.declare(
+ "rnp_enarmor",
+ abi,
+ rnp_result_t,
+ rnp_input_t,
+ rnp_output_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_verify_get_protection_info: librnp.declare(
+ "rnp_op_verify_get_protection_info",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.char.ptr.ptr,
+ ctypes.char.ptr.ptr,
+ ctypes.bool.ptr
+ ),
+
+ rnp_op_verify_get_recipient_count: librnp.declare(
+ "rnp_op_verify_get_recipient_count",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_op_verify_get_used_recipient: librnp.declare(
+ "rnp_op_verify_get_used_recipient",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ rnp_recipient_handle_t.ptr
+ ),
+
+ rnp_op_verify_get_recipient_at: librnp.declare(
+ "rnp_op_verify_get_recipient_at",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.size_t,
+ rnp_recipient_handle_t.ptr
+ ),
+
+ rnp_recipient_get_keyid: librnp.declare(
+ "rnp_recipient_get_keyid",
+ abi,
+ rnp_result_t,
+ rnp_recipient_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_recipient_get_alg: librnp.declare(
+ "rnp_recipient_get_alg",
+ abi,
+ rnp_result_t,
+ rnp_recipient_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_op_verify_get_symenc_count: librnp.declare(
+ "rnp_op_verify_get_symenc_count",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.size_t.ptr
+ ),
+
+ rnp_op_verify_get_used_symenc: librnp.declare(
+ "rnp_op_verify_get_used_symenc",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ rnp_symenc_handle_t.ptr
+ ),
+
+ rnp_op_verify_get_symenc_at: librnp.declare(
+ "rnp_op_verify_get_symenc_at",
+ abi,
+ rnp_result_t,
+ rnp_op_verify_t,
+ ctypes.size_t,
+ rnp_symenc_handle_t.ptr
+ ),
+
+ rnp_symenc_get_cipher: librnp.declare(
+ "rnp_symenc_get_cipher",
+ abi,
+ rnp_result_t,
+ rnp_symenc_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_symenc_get_aead_alg: librnp.declare(
+ "rnp_symenc_get_aead_alg",
+ abi,
+ rnp_result_t,
+ rnp_symenc_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_symenc_get_hash_alg: librnp.declare(
+ "rnp_symenc_get_hash_alg",
+ abi,
+ rnp_result_t,
+ rnp_symenc_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_symenc_get_s2k_type: librnp.declare(
+ "rnp_symenc_get_s2k_type",
+ abi,
+ rnp_result_t,
+ rnp_symenc_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_symenc_get_s2k_iterations: librnp.declare(
+ "rnp_symenc_get_s2k_iterations",
+ abi,
+ rnp_result_t,
+ rnp_symenc_handle_t,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_key_set_expiration: librnp.declare(
+ "rnp_key_set_expiration",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_key_revoke: librnp.declare(
+ "rnp_key_revoke",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.char.ptr
+ ),
+
+ rnp_key_export_autocrypt: librnp.declare(
+ "rnp_key_export_autocrypt",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr,
+ rnp_output_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_key_valid_till: librnp.declare(
+ "rnp_key_valid_till",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_key_valid_till64: librnp.declare(
+ "rnp_key_valid_till64",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.uint64_t.ptr
+ ),
+
+ rnp_uid_is_valid: librnp.declare(
+ "rnp_uid_is_valid",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_uid_is_primary: librnp.declare(
+ "rnp_uid_is_primary",
+ abi,
+ rnp_result_t,
+ rnp_uid_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_signature_is_valid: librnp.declare(
+ "rnp_signature_is_valid",
+ abi,
+ rnp_result_t,
+ rnp_signature_handle_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_key_get_protection_type: librnp.declare(
+ "rnp_key_get_protection_type",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_output_armor_set_line_length: librnp.declare(
+ "rnp_output_armor_set_line_length",
+ abi,
+ rnp_result_t,
+ rnp_output_t,
+ ctypes.size_t
+ ),
+
+ rnp_key_25519_bits_tweaked: librnp.declare(
+ "rnp_key_25519_bits_tweaked",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.bool.ptr
+ ),
+
+ rnp_key_25519_bits_tweak: librnp.declare(
+ "rnp_key_25519_bits_tweak",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t
+ ),
+
+ rnp_key_get_curve: librnp.declare(
+ "rnp_key_get_curve",
+ abi,
+ rnp_result_t,
+ rnp_key_handle_t,
+ ctypes.char.ptr.ptr
+ ),
+
+ rnp_get_security_rule: librnp.declare(
+ "rnp_get_security_rule",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.uint64_t,
+ ctypes.uint32_t.ptr,
+ ctypes.uint64_t.ptr,
+ ctypes.uint32_t.ptr
+ ),
+
+ rnp_add_security_rule: librnp.declare(
+ "rnp_add_security_rule",
+ abi,
+ rnp_result_t,
+ rnp_ffi_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.uint32_t,
+ ctypes.uint64_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_op_encrypt_set_aead: librnp.declare(
+ "rnp_op_encrypt_set_aead",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ ctypes.char.ptr
+ ),
+
+ rnp_op_encrypt_set_flags: librnp.declare(
+ "rnp_op_encrypt_set_flags",
+ abi,
+ rnp_result_t,
+ rnp_op_encrypt_t,
+ ctypes.uint32_t
+ ),
+
+ rnp_result_t,
+ rnp_ffi_t,
+ rnp_password_cb_t,
+ rnp_input_t,
+ rnp_output_t,
+ rnp_key_handle_t,
+ rnp_uid_handle_t,
+ rnp_identifier_iterator_t,
+ rnp_op_generate_t,
+ rnp_op_encrypt_t,
+ rnp_op_sign_t,
+ rnp_op_sign_signature_t,
+ rnp_op_verify_t,
+ rnp_op_verify_signature_t,
+ rnp_signature_handle_t,
+ rnp_recipient_handle_t,
+ rnp_symenc_handle_t,
+
+ RNP_LOAD_SAVE_PUBLIC_KEYS: 1,
+ RNP_LOAD_SAVE_SECRET_KEYS: 2,
+ RNP_LOAD_SAVE_PERMISSIVE: 256,
+
+ RNP_KEY_REMOVE_PUBLIC: 1,
+ RNP_KEY_REMOVE_SECRET: 2,
+ RNP_KEY_REMOVE_SUBKEYS: 4,
+
+ RNP_KEY_EXPORT_ARMORED: 1,
+ RNP_KEY_EXPORT_PUBLIC: 2,
+ RNP_KEY_EXPORT_SECRET: 4,
+ RNP_KEY_EXPORT_SUBKEYS: 8,
+
+ RNP_KEY_SIGNATURE_NON_SELF_SIG: 4,
+
+ RNP_SUCCESS: 0x00000000,
+
+ RNP_FEATURE_SYMM_ALG: "symmetric algorithm",
+ RNP_FEATURE_HASH_ALG: "hash algorithm",
+ RNP_FEATURE_PK_ALG: "public key algorithm",
+ RNP_ALGNAME_MD5: "MD5",
+ RNP_ALGNAME_SHA1: "SHA1",
+ RNP_ALGNAME_SM2: "SM2",
+ RNP_ALGNAME_SM3: "SM3",
+ RNP_ALGNAME_SM4: "SM4",
+
+ RNP_SECURITY_OVERRIDE: 1,
+ RNP_SECURITY_VERIFY_KEY: 2,
+ RNP_SECURITY_VERIFY_DATA: 4,
+ RNP_SECURITY_REMOVE_ALL: 65536,
+
+ RNP_SECURITY_PROHIBITED: 0,
+ RNP_SECURITY_INSECURE: 1,
+ RNP_SECURITY_DEFAULT: 2,
+
+ RNP_ENCRYPT_NOWRAP: 1,
+
+ /* Common error codes */
+ RNP_ERROR_GENERIC: 0x10000000, // 268435456
+ RNP_ERROR_BAD_FORMAT: 0x10000001, // 268435457
+ RNP_ERROR_BAD_PARAMETERS: 0x10000002, // 268435458
+ RNP_ERROR_NOT_IMPLEMENTED: 0x10000003, // 268435459
+ RNP_ERROR_NOT_SUPPORTED: 0x10000004, // 268435460
+ RNP_ERROR_OUT_OF_MEMORY: 0x10000005, // 268435461
+ RNP_ERROR_SHORT_BUFFER: 0x10000006, // 268435462
+ RNP_ERROR_NULL_POINTER: 0x10000007, // 268435463
+
+ /* Storage */
+ RNP_ERROR_ACCESS: 0x11000000, // 285212672
+ RNP_ERROR_READ: 0x11000001, // 285212673
+ RNP_ERROR_WRITE: 0x11000002, // 285212674
+
+ /* Crypto */
+ RNP_ERROR_BAD_STATE: 0x12000000, // 301989888
+ RNP_ERROR_MAC_INVALID: 0x12000001, // 301989889
+ RNP_ERROR_SIGNATURE_INVALID: 0x12000002, // 301989890
+ RNP_ERROR_KEY_GENERATION: 0x12000003, // 301989891
+ RNP_ERROR_BAD_PASSWORD: 0x12000004, // 301989892
+ RNP_ERROR_KEY_NOT_FOUND: 0x12000005, // 301989893
+ RNP_ERROR_NO_SUITABLE_KEY: 0x12000006, // 301989894
+ RNP_ERROR_DECRYPT_FAILED: 0x12000007, // 301989895
+ RNP_ERROR_RNG: 0x12000008, // 301989896
+ RNP_ERROR_SIGNING_FAILED: 0x12000009, // 301989897
+ RNP_ERROR_NO_SIGNATURES_FOUND: 0x1200000a, // 301989898
+
+ RNP_ERROR_SIGNATURE_EXPIRED: 0x1200000b, // 301989899
+
+ /* Parsing */
+ RNP_ERROR_NOT_ENOUGH_DATA: 0x13000000, // 318767104
+ RNP_ERROR_UNKNOWN_TAG: 0x13000001, // 318767105
+ RNP_ERROR_PACKET_NOT_CONSUMED: 0x13000002, // 318767106
+ RNP_ERROR_NO_USERID: 0x13000003, // 318767107
+ RNP_ERROR_EOF: 0x13000004, // 318767108
+ };
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/armor.jsm b/comm/mail/extensions/openpgp/content/modules/armor.jsm
new file mode 100644
index 0000000000..023a68158c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/armor.jsm
@@ -0,0 +1,367 @@
+/*
+ * 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 = ["EnigmailArmor"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+// Locates STRing in TEXT occurring only at the beginning of a line
+function indexOfArmorDelimiter(text, str, offset) {
+ let currentOffset = offset;
+
+ while (currentOffset < text.length) {
+ let loc = text.indexOf(str, currentOffset);
+
+ if (loc === -1 || loc === 0 || text.charAt(loc - 1) == "\n") {
+ return loc;
+ }
+
+ currentOffset = loc + str.length;
+ }
+
+ return -1;
+}
+
+function searchBlankLine(str, then) {
+ var offset = str.search(/\n\s*\r?\n/);
+ if (offset === -1) {
+ return "";
+ }
+ return then(offset);
+}
+
+function indexOfNewline(str, off, then) {
+ var offset = str.indexOf("\n", off);
+ if (offset === -1) {
+ return "";
+ }
+ return then(offset);
+}
+
+var EnigmailArmor = {
+ /**
+ * Locates offsets bracketing PGP armored block in text,
+ * starting from given offset, and returns block type string.
+ *
+ * @param text: String - ASCII armored text
+ * @param offset: Number - offset to start looking for block
+ * @param indentStr: String - prefix that is used for all lines (such as "> ")
+ * @param beginIndexObj: Object - o.value will contain offset of first character of block
+ * @param endIndexObj: Object - o.value will contain offset of last character of block (newline)
+ * @param indentStrObj: Object - o.value will contain indent of 1st line
+ *
+ * @returns String - type of block found (e.g. MESSAGE, PUBLIC KEY)
+ * If no block is found, an empty String is returned;
+ */
+ locateArmoredBlock(
+ text,
+ offset,
+ indentStr,
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "armor.jsm: Enigmail.locateArmoredBlock: " +
+ offset +
+ ", '" +
+ indentStr +
+ "'\n"
+ );
+
+ beginIndexObj.value = -1;
+ endIndexObj.value = -1;
+
+ var beginIndex = indexOfArmorDelimiter(
+ text,
+ indentStr + "-----BEGIN PGP ",
+ offset
+ );
+
+ if (beginIndex == -1) {
+ var blockStart = text.indexOf("-----BEGIN PGP ");
+ if (blockStart >= 0) {
+ var indentStart = text.search(/\n.*-----BEGIN PGP /) + 1;
+ indentStrObj.value = text.substring(indentStart, blockStart);
+ indentStr = indentStrObj.value;
+ beginIndex = indexOfArmorDelimiter(
+ text,
+ indentStr + "-----BEGIN PGP ",
+ offset
+ );
+ }
+ }
+
+ if (beginIndex == -1) {
+ return "";
+ }
+
+ // Locate newline at end of armor header
+ offset = text.indexOf("\n", beginIndex);
+
+ if (offset == -1) {
+ return "";
+ }
+
+ var endIndex = indexOfArmorDelimiter(
+ text,
+ indentStr + "-----END PGP ",
+ offset
+ );
+
+ if (endIndex == -1) {
+ return "";
+ }
+
+ // Locate newline at end of PGP block
+ endIndex = text.indexOf("\n", endIndex);
+
+ if (endIndex == -1) {
+ // No terminating newline
+ endIndex = text.length - 1;
+ }
+
+ var blockHeader = text.substr(beginIndex, offset - beginIndex + 1);
+
+ let escapedIndentStr = indentStr.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ var blockRegex = new RegExp(
+ "^" + escapedIndentStr + "-----BEGIN PGP (.{1,30})-----\\s*\\r?\\n"
+ );
+
+ var matches = blockHeader.match(blockRegex);
+
+ var blockType = "";
+ if (matches && matches.length > 1) {
+ blockType = matches[1];
+ lazy.EnigmailLog.DEBUG(
+ "armor.jsm: Enigmail.locateArmoredBlock: blockType=" + blockType + "\n"
+ );
+ }
+
+ if (blockType == "UNVERIFIED MESSAGE") {
+ // Skip any unverified message block
+ return EnigmailArmor.locateArmoredBlock(
+ text,
+ endIndex + 1,
+ indentStr,
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ );
+ }
+
+ beginIndexObj.value = beginIndex;
+ endIndexObj.value = endIndex;
+
+ return blockType;
+ },
+
+ /**
+ * locateArmoredBlocks returns an array of ASCII Armor block positions
+ *
+ * @param text: String - text containing ASCII armored block(s)
+ *
+ * @returns Array of objects with the following structure:
+ * obj.begin: Number
+ * obj.end: Number
+ * obj.indent: String
+ * obj.blocktype: String
+ *
+ * if no block was found, an empty array is returned
+ */
+ locateArmoredBlocks(text) {
+ var beginObj = {};
+ var endObj = {};
+ var indentStrObj = {};
+ var blocks = [];
+ var i = 0;
+ var b;
+
+ while (
+ (b = EnigmailArmor.locateArmoredBlock(
+ text,
+ i,
+ "",
+ beginObj,
+ endObj,
+ indentStrObj
+ )) !== ""
+ ) {
+ blocks.push({
+ begin: beginObj.value,
+ end: endObj.value,
+ indent: indentStrObj.value ? indentStrObj.value : "",
+ blocktype: b,
+ });
+
+ i = endObj.value;
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "armor.jsm: locateArmorBlocks: Found " + blocks.length + " Blocks\n"
+ );
+ return blocks;
+ },
+
+ extractSignaturePart(signatureBlock, part) {
+ lazy.EnigmailLog.DEBUG(
+ "armor.jsm: Enigmail.extractSignaturePart: part=" + part + "\n"
+ );
+
+ return searchBlankLine(signatureBlock, function (offset) {
+ return indexOfNewline(signatureBlock, offset + 1, function (offset) {
+ var beginIndex = signatureBlock.indexOf(
+ "-----BEGIN PGP SIGNATURE-----",
+ offset + 1
+ );
+ if (beginIndex == -1) {
+ return "";
+ }
+
+ if (part === lazy.EnigmailConstants.SIGNATURE_TEXT) {
+ return signatureBlock
+ .substr(offset + 1, beginIndex - offset - 1)
+ .replace(/^- -/, "-")
+ .replace(/\n- -/g, "\n-")
+ .replace(/\r- -/g, "\r-");
+ }
+
+ return indexOfNewline(signatureBlock, beginIndex, function (offset) {
+ var endIndex = signatureBlock.indexOf(
+ "-----END PGP SIGNATURE-----",
+ offset
+ );
+ if (endIndex == -1) {
+ return "";
+ }
+
+ var signBlock = signatureBlock.substr(offset, endIndex - offset);
+
+ return searchBlankLine(signBlock, function (armorIndex) {
+ if (part == lazy.EnigmailConstants.SIGNATURE_HEADERS) {
+ return signBlock.substr(1, armorIndex);
+ }
+
+ return indexOfNewline(
+ signBlock,
+ armorIndex + 1,
+ function (armorIndex) {
+ if (part == lazy.EnigmailConstants.SIGNATURE_ARMOR) {
+ return signBlock
+ .substr(armorIndex, endIndex - armorIndex)
+ .replace(/\s*/g, "");
+ }
+ return "";
+ }
+ );
+ });
+ });
+ });
+ });
+ },
+
+ /**
+ * Remove all headers from an OpenPGP Armored message and replace them
+ * with a set of new headers.
+ *
+ * @param armorText: String - ASCII armored message
+ * @param headers: Object - key/value pairs of new headers to insert
+ *
+ * @returns String - new armored message
+ */
+ replaceArmorHeaders(armorText, headers) {
+ let text = armorText.replace(/\r\n/g, "\n");
+ let i = text.search(/\n/);
+
+ if (i < 0) {
+ return armorText;
+ }
+ let m = text.substr(0, i + 1);
+
+ for (let j in headers) {
+ m += j + ": " + headers[j] + "\n";
+ }
+
+ i = text.search(/\n\n/);
+ if (i < 0) {
+ return armorText;
+ }
+ m += text.substr(i + 1);
+
+ return m;
+ },
+
+ /**
+ * Get a list of all headers found in an armor message
+ *
+ * @param text String - ASCII armored message
+ *
+ * @returns Object: key/value pairs of headers. All keys are in lowercase.
+ */
+ getArmorHeaders(text) {
+ let headers = {};
+ let b = this.locateArmoredBlocks(text);
+
+ if (b.length === 0) {
+ return headers;
+ }
+
+ let msg = text.substr(b[0].begin);
+
+ // Escape regex chars.
+ let indent = b[0].indent.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ let lx = new RegExp("\\n" + indent + "\\r?\\n");
+ let hdrEnd = msg.search(lx);
+ if (hdrEnd < 0) {
+ return headers;
+ }
+
+ let lines = msg.substr(0, hdrEnd).split(/\r?\n/);
+
+ let rx = new RegExp("^" + b[0].indent + "([^: ]+)(: )(.*)");
+ // skip 1st line (ARMOR-line)
+ for (let i = 1; i < lines.length; i++) {
+ let m = lines[i].match(rx);
+ if (m && m.length >= 4) {
+ headers[m[1].toLowerCase()] = m[3];
+ }
+ }
+
+ return headers;
+ },
+
+ /**
+ * Split armored blocks into an array of strings
+ */
+ splitArmoredBlocks(keyBlockStr) {
+ let myRe = /-----BEGIN PGP (PUBLIC|PRIVATE) KEY BLOCK-----/g;
+ let myArray;
+ let retArr = [];
+ let startIndex = -1;
+ while ((myArray = myRe.exec(keyBlockStr)) !== null) {
+ if (startIndex >= 0) {
+ let s = keyBlockStr.substring(startIndex, myArray.index);
+ retArr.push(s);
+ }
+ startIndex = myArray.index;
+ }
+
+ retArr.push(keyBlockStr.substring(startIndex));
+
+ return retArr;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/constants.jsm b/comm/mail/extensions/openpgp/content/modules/constants.jsm
new file mode 100644
index 0000000000..f04f249536
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/constants.jsm
@@ -0,0 +1,183 @@
+/*
+ * 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 = ["EnigmailConstants"];
+
+var EnigmailConstants = {
+ POSSIBLE_PGPMIME: -2081,
+
+ // possible values for
+ // - encryptByRule, signByRules, pgpmimeByRules
+ // - encryptForced, signForced, pgpmimeForced (except CONFLICT)
+ // NOTE:
+ // - values 0/1/2 are used with this fixed semantics in the persistent rules
+ // - see also enigmailEncryptionDlg.xhtml
+ ENIG_FORCE_SMIME: 3,
+ ENIG_AUTO_ALWAYS: 22,
+ ENIG_CONFLICT: 99,
+
+ ENIG_FINAL_UNDEF: -1,
+ ENIG_FINAL_NO: 0,
+ ENIG_FINAL_YES: 1,
+ ENIG_FINAL_FORCENO: 10,
+ ENIG_FINAL_FORCEYES: 11,
+ ENIG_FINAL_SMIME: 97, // use S/MIME (automatically chosen)
+ ENIG_FINAL_FORCESMIME: 98, // use S/MIME (forced by user)
+ ENIG_FINAL_CONFLICT: 99,
+
+ MIME_HANDLER_UNDEF: 0,
+ MIME_HANDLER_SMIME: 1,
+ MIME_HANDLER_PGPMIME: 2,
+
+ ICONTYPE_INFO: 1,
+ ICONTYPE_QUESTION: 2,
+ ICONTYPE_ALERT: 3,
+ ICONTYPE_ERROR: 4,
+
+ FILTER_MOVE_DECRYPT: "enigmail@enigmail.net#filterActionMoveDecrypt",
+ FILTER_COPY_DECRYPT: "enigmail@enigmail.net#filterActionCopyDecrypt",
+ FILTER_ENCRYPT: "enigmail@enigmail.net#filterActionEncrypt",
+ FILTER_TERM_PGP_ENCRYPTED: "enigmail@enigmail.net#filterTermPGPEncrypted",
+
+ /* taken over from old nsIEnigmail */
+
+ /* Cleartext signature parts */
+ SIGNATURE_TEXT: 1,
+ SIGNATURE_HEADERS: 2,
+ SIGNATURE_ARMOR: 3,
+
+ /* User interaction flags */
+ UI_INTERACTIVE: 0x01,
+ UI_ALLOW_KEY_IMPORT: 0x02,
+ UI_UNVERIFIED_ENC_OK: 0x04,
+ UI_PGP_MIME: 0x08,
+ UI_TEST: 0x10,
+ UI_RESTORE_STRICTLY_MIME: 0x20,
+ UI_IGNORE_MDC_ERROR: 0x40, // force decryption, even if we got an MDC error
+
+ /* Send message flags */
+ SEND_SIGNED: 0x0001, // 1
+ SEND_ENCRYPTED: 0x0002, // 2
+ SEND_DEFAULT: 0x0004, // 4
+ SEND_LATER: 0x0008, // 8
+ SEND_WITH_CHECK: 0x0010, // 16
+ SEND_ALWAYS_TRUST: 0x0020, // 32
+ SEND_ENCRYPT_TO_SELF: 0x0040, // 64
+ SEND_PGP_MIME: 0x0080, // 128
+ SEND_TEST: 0x0100, // 256
+ SAVE_MESSAGE: 0x0200, // 512
+ SEND_STRIP_WHITESPACE: 0x0400, // 1024
+ SEND_ATTACHMENT: 0x0800, // 2048
+ ENCRYPT_SUBJECT: 0x1000, // 4096
+ SEND_VERBATIM: 0x2000, // 8192
+ SEND_TWO_MIME_LAYERS: 0x4000, // 16384
+ SEND_SENDER_KEY_EXTERNAL: 0x8000, // 32768
+
+ /* Status flags */
+ GOOD_SIGNATURE: 0x00000001,
+ BAD_SIGNATURE: 0x00000002,
+ UNCERTAIN_SIGNATURE: 0x00000004,
+ EXPIRED_SIGNATURE: 0x00000008,
+ EXPIRED_KEY_SIGNATURE: 0x00000010,
+ EXPIRED_KEY: 0x00000020,
+ REVOKED_KEY: 0x00000040,
+ NO_PUBKEY: 0x00000080,
+ NO_SECKEY: 0x00000100,
+ IMPORTED_KEY: 0x00000200,
+ INVALID_RECIPIENT: 0x00000400,
+ MISSING_PASSPHRASE: 0x00000800,
+ BAD_PASSPHRASE: 0x00001000,
+ BAD_ARMOR: 0x00002000,
+ NODATA: 0x00004000,
+ DECRYPTION_INCOMPLETE: 0x00008000,
+ DECRYPTION_FAILED: 0x00010000,
+ DECRYPTION_OKAY: 0x00020000,
+ MISSING_MDC: 0x00040000,
+ TRUSTED_IDENTITY: 0x00080000,
+ PGP_MIME_SIGNED: 0x00100000,
+ PGP_MIME_ENCRYPTED: 0x00200000,
+ DISPLAY_MESSAGE: 0x00400000,
+ INLINE_KEY: 0x00800000,
+ PARTIALLY_PGP: 0x01000000,
+ PHOTO_AVAILABLE: 0x02000000,
+ OVERFLOWED: 0x04000000,
+ CARDCTRL: 0x08000000,
+ SC_OP_FAILURE: 0x10000000,
+ UNKNOWN_ALGO: 0x20000000,
+ SIG_CREATED: 0x40000000,
+ END_ENCRYPTION: 0x80000000,
+
+ /* Extended status flags */
+ EXT_SELF_IDENTITY: 0x00000001,
+
+ /* UI message status flags */
+ MSG_SIG_NONE: 0,
+ MSG_SIG_VALID_SELF: 1,
+ MSG_SIG_VALID_KEY_VERIFIED: 2,
+ MSG_SIG_VALID_KEY_UNVERIFIED: 3,
+ MSG_SIG_UNCERTAIN_KEY_UNAVAILABLE: 4,
+ MSG_SIG_UNCERTAIN_UID_MISMATCH: 5,
+ MSG_SIG_UNCERTAIN_KEY_NOT_ACCEPTED: 6,
+ MSG_SIG_INVALID: 7,
+ MSG_SIG_INVALID_KEY_REJECTED: 8,
+
+ MSG_ENC_NONE: 0,
+ MSG_ENC_OK: 1,
+ MSG_ENC_FAILURE: 2,
+ MSG_ENC_NO_SECRET_KEY: 3,
+
+ /*** key handling functions ***/
+
+ EXTRACT_SECRET_KEY: 0x01,
+
+ /* Keyserver Action Flags */
+ SEARCH_KEY: 1,
+ DOWNLOAD_KEY: 2,
+ UPLOAD_KEY: 3,
+ REFRESH_KEY: 4,
+ UPLOAD_WKD: 6,
+ GET_CONFIRMATION_LINK: 7,
+ DOWNLOAD_KEY_NO_IMPORT: 8,
+
+ /* attachment handling */
+
+ /* per-recipient rules */
+ AC_RULE_PREFIX: "autocrypt://",
+
+ CARD_PIN_CHANGE: 1,
+ CARD_PIN_UNBLOCK: 2,
+ CARD_ADMIN_PIN_CHANGE: 3,
+
+ /* Keyserver error codes (in keyserver.jsm) */
+ KEYSERVER_ERR_ABORTED: 1,
+ KEYSERVER_ERR_SERVER_ERROR: 2,
+ KEYSERVER_ERR_SECURITY_ERROR: 3,
+ KEYSERVER_ERR_CERTIFICATE_ERROR: 4,
+ KEYSERVER_ERR_SERVER_UNAVAILABLE: 5,
+ KEYSERVER_ERR_IMPORT_ERROR: 6,
+ KEYSERVER_ERR_UNKNOWN: 7,
+
+ /* AutocryptSeup Setup Type */
+ AUTOSETUP_NOT_INITIALIZED: 0,
+ AUTOSETUP_AC_SETUP_MSG: 1,
+ AUTOSETUP_AC_HEADER: 2,
+ AUTOSETUP_PEP_HEADER: 3,
+ AUTOSETUP_ENCRYPTED_MSG: 4,
+ AUTOSETUP_NO_HEADER: 5,
+ AUTOSETUP_NO_ACCOUNT: 6,
+
+ /* Bootstrapped Addon constants */
+ APP_STARTUP: 1, // The application is starting up.
+ APP_SHUTDOWN: 2, // The application is shutting down.
+ ADDON_ENABLE: 3, // The add-on is being enabled.
+ ADDON_DISABLE: 4, // The add-on is being disabled. (Also sent during uninstallation)
+ ADDON_INSTALL: 5, // The add-on is being installed.
+ ADDON_UNINSTALL: 6, // The add-on is being uninstalled.
+ ADDON_UPGRADE: 7, // The add-on is being upgraded.
+ ADDON_DOWNGRADE: 8, // The add-on is being downgraded.
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/core.jsm b/comm/mail/extensions/openpgp/content/modules/core.jsm
new file mode 100644
index 0000000000..a18d07415f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/core.jsm
@@ -0,0 +1,189 @@
+/*
+ * 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";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailMimeEncrypt: "chrome://openpgp/content/modules/mimeEncrypt.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+ EnigmailPgpmimeHander: "chrome://openpgp/content/modules/pgpmimeHandler.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+});
+
+var EXPORTED_SYMBOLS = ["EnigmailCore"];
+
+var gEnigmailService = null; // Global Enigmail Service
+
+var EnigmailCore = {
+ /**
+ * Create a new instance of Enigmail, or return the already existing one
+ */
+ createInstance() {
+ if (!gEnigmailService) {
+ gEnigmailService = new Enigmail();
+ }
+ return gEnigmailService;
+ },
+
+ async startup(reason) {
+ initializeLogDirectory();
+
+ lazy.EnigmailLog.DEBUG("core.jsm: startup()\n");
+
+ await lazy.PgpSqliteDb2.checkDatabaseStructure();
+
+ this.factories = [];
+
+ lazy.EnigmailVerify.registerPGPMimeHandler();
+ //EnigmailWksMimeHandler.registerContentTypeHandler();
+ //EnigmailFiltersWrapper.onStartup();
+
+ lazy.EnigmailMimeEncrypt.startup(reason);
+ //EnigmailOverlays.startup();
+ this.factories.push(new Factory(lazy.EnigmailMimeEncrypt.Handler));
+ },
+
+ shutdown(reason) {
+ if (this.factories) {
+ for (let fct of this.factories) {
+ fct.unregister();
+ }
+ }
+
+ //EnigmailFiltersWrapper.onShutdown();
+ lazy.EnigmailVerify.unregisterPGPMimeHandler();
+
+ lazy.EnigmailLog.onShutdown();
+
+ lazy.EnigmailLog.setLogLevel(3);
+ gEnigmailService = null;
+ },
+
+ /**
+ * get and or initialize the Enigmail service,
+ * including the handling for upgrading old preferences to new versions
+ *
+ * @win: - nsIWindow: parent window (optional)
+ * @startingPreferences - Boolean: true - called while switching to new preferences
+ * (to avoid re-check for preferences)
+ * @returns {Promise<Enigmail|null>}
+ */
+ async getService(win, startingPreferences) {
+ // Lazy initialization of Enigmail JS component (for efficiency)
+
+ if (gEnigmailService) {
+ return gEnigmailService.initialized ? gEnigmailService : null;
+ }
+
+ try {
+ this.createInstance();
+ return gEnigmailService.getService(win, startingPreferences);
+ } catch (ex) {
+ return null;
+ }
+ },
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// Enigmail encryption/decryption service
+///////////////////////////////////////////////////////////////////////////////
+
+function initializeLogDirectory() {
+ let dir = Services.prefs.getCharPref("temp.openpgp.logDirectory", "");
+ if (!dir) {
+ return;
+ }
+
+ lazy.EnigmailLog.setLogLevel(5);
+ lazy.EnigmailLog.setLogDirectory(dir);
+ lazy.EnigmailLog.DEBUG(
+ "core.jsm: Logging debug output to " + dir + "/enigdbug.txt\n"
+ );
+}
+
+function Enigmail() {
+ this.wrappedJSObject = this;
+}
+
+Enigmail.prototype = {
+ initialized: false,
+ initializationAttempted: false,
+
+ initialize(domWindow) {
+ this.initializationAttempted = true;
+
+ lazy.EnigmailLog.DEBUG("core.jsm: Enigmail.initialize: START\n");
+
+ if (this.initialized) {
+ return;
+ }
+
+ this.initialized = true;
+
+ lazy.EnigmailLog.DEBUG("core.jsm: Enigmail.initialize: END\n");
+ },
+
+ reinitialize() {
+ lazy.EnigmailLog.DEBUG("core.jsm: Enigmail.reinitialize:\n");
+ this.initialized = false;
+ this.initializationAttempted = true;
+
+ this.initialized = true;
+ },
+
+ async getService(win, startingPreferences) {
+ if (!win) {
+ win = lazy.EnigmailWindows.getBestParentWin();
+ }
+
+ lazy.EnigmailLog.DEBUG("core.jsm: svc = " + this + "\n");
+
+ if (!this.initialized) {
+ // Initialize enigmail
+ this.initialize(win);
+ }
+ await EnigmailCore.startup(0);
+ lazy.EnigmailPgpmimeHander.startup(0);
+ return this.initialized ? this : null;
+ },
+}; // Enigmail.prototype
+
+class Factory {
+ constructor(component) {
+ this.component = component;
+ this.register();
+ Object.freeze(this);
+ }
+
+ createInstance(iid) {
+ return new this.component();
+ }
+
+ register() {
+ Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar)
+ .registerFactory(
+ this.component.prototype.classID,
+ this.component.prototype.classDescription,
+ this.component.prototype.contractID,
+ this
+ );
+ }
+
+ unregister() {
+ Components.manager
+ .QueryInterface(Ci.nsIComponentRegistrar)
+ .unregisterFactory(this.component.prototype.classID, this);
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/cryptoAPI.jsm b/comm/mail/extensions/openpgp/content/modules/cryptoAPI.jsm
new file mode 100644
index 0000000000..b722d4ee7d
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/cryptoAPI.jsm
@@ -0,0 +1,32 @@
+/*
+ * 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";
+
+var EXPORTED_SYMBOLS = ["EnigmailCryptoAPI", "EnigmailGnuPGAPI"];
+
+var gCurrentApi = null;
+var gGnuPGApi = null;
+
+function EnigmailCryptoAPI() {
+ if (!gCurrentApi) {
+ const { getRNPAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI/RNPCryptoAPI.jsm"
+ );
+ gCurrentApi = getRNPAPI();
+ }
+ return gCurrentApi;
+}
+
+function EnigmailGnuPGAPI() {
+ if (!gGnuPGApi) {
+ const { getGnuPGAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI/GnuPGCryptoAPI.jsm"
+ );
+ gGnuPGApi = getGnuPGAPI();
+ }
+ return gGnuPGApi;
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/cryptoAPI/GnuPGCryptoAPI.jsm b/comm/mail/extensions/openpgp/content/modules/cryptoAPI/GnuPGCryptoAPI.jsm
new file mode 100644
index 0000000000..475108292d
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/cryptoAPI/GnuPGCryptoAPI.jsm
@@ -0,0 +1,238 @@
+/*
+ * 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";
+
+var EXPORTED_SYMBOLS = ["getGnuPGAPI"];
+
+Services.scriptloader.loadSubScript(
+ "chrome://openpgp/content/modules/cryptoAPI/interface.js",
+ null,
+ "UTF-8"
+); /* global CryptoAPI */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+/**
+ * GnuPG implementation of CryptoAPI
+ */
+
+class GnuPGCryptoAPI extends CryptoAPI {
+ constructor() {
+ super();
+ this.api_name = "GnuPG";
+ }
+
+ /**
+ * Get the list of all knwn keys (including their secret keys)
+ *
+ * @param {Array of String} onlyKeys: [optional] only load data for specified key IDs
+ *
+ * @returns {Promise<Array of Object>}
+ */
+ async getKeys(onlyKeys = null) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Obtain signatures for a given set of key IDs.
+ *
+ * @param {string} keyId: space-separated list of key IDs
+ * @param {boolean} ignoreUnknownUid: if true, filter out unknown signer's UIDs
+ *
+ * @returns {Promise<Array of Object>} - see extractSignatures()
+ */
+ async getKeySignatures(keyId, ignoreUnknownUid = false) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: getKeySignatures: ${keyId}\n`);
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Obtain signatures for a given key.
+ *
+ * @param {KeyObj} keyObj: the signatures of this key will be returned
+ * @param {boolean} ignoreUnknownUid: if true, filter out unknown signer's UIDs
+ *
+ * @returns {Promise<Array of Object>} - see extractSignatures()
+ */
+ async getKeyObjSignatures(keyObj, ignoreUnknownUid = false) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Export the minimum key for the public key object:
+ * public key, primary 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 specific creation Dates
+ *
+ * @returns {Promise<object>}:
+ * - exitCode (0 = success)
+ * - errorMsg (if exitCode != 0)
+ * - keyData: BASE64-encded string of key data
+ */
+ async getMinimalPubKey(fpr, email, subkeyDates) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: getMinimalPubKey: ${fpr}\n`);
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Export secret key(s) to a file
+ *
+ * @param {string} keyId Specification by fingerprint or keyID
+ * @param {boolean} minimalKey - if true, reduce key to minimum required
+ *
+ * @returns {object}:
+ * - {Number} exitCode: result code (0: OK)
+ * - {String} keyData: ASCII armored key data material
+ * - {String} errorMsg: error message in case exitCode !== 0
+ */
+
+ async extractSecretKey(keyId, minimalKey) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ *
+ * @param {byte} byteData - The encrypted data
+ *
+ * @returns {String or null} - the name of the attached file
+ */
+
+ async getFileName(byteData) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: getFileName()\n`);
+ throw new Error("Not implemented");
+ }
+
+ /**
+ *
+ * @param {Path} filePath - The signed file
+ * @param {Path} sigPath - The signature to verify
+ *
+ * @returns {Promise<string>} - A message from the verification.
+ *
+ * Use Promise.catch to handle failed verifications.
+ * The message will be an error message in this case.
+ */
+
+ async verifyAttachment(filePath, sigPath) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: verifyAttachment()\n`);
+ throw new Error("Not implemented");
+ }
+
+ /**
+ *
+ * @param {Bytes} encrypted The encrypted data
+ *
+ * @returns {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 decryptAttachment(encrypted) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: decryptAttachment()\n`);
+ throw new Error("Not implemented");
+ }
+
+ /**
+ *
+ * @param {string} encrypted - The encrypted data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: decrypt()\n`);
+ throw new Error("Not implemented");
+ }
+
+ /**
+ *
+ * @param {string} encrypted - The encrypted data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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 decryptMime(encrypted, options) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: decryptMime()\n`);
+
+ // write something to gpg such that the process doesn't get stuck
+ if (encrypted.length === 0) {
+ encrypted = "NO DATA\n";
+ }
+
+ options.noOutput = false;
+ options.verifyOnly = false;
+ options.uiFlags = lazy.EnigmailConstants.UI_PGP_MIME;
+
+ return this.decrypt(encrypted, options);
+ }
+
+ /**
+ *
+ * @param {string} signed - The signed data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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 verifyMime(signed, options) {
+ lazy.EnigmailLog.DEBUG(`gnupg.js: verifyMime()\n`);
+
+ options.noOutput = true;
+ options.verifyOnly = true;
+ options.uiFlags = lazy.EnigmailConstants.UI_PGP_MIME;
+
+ return this.decrypt(signed, options);
+ }
+
+ async getKeyListFromKeyBlockAPI(keyBlockStr) {
+ throw new Error("Not implemented");
+ }
+
+ async genKey(userId, keyType, keySize, expiryTime, passphrase) {
+ throw new Error("GnuPG genKey() not implemented");
+ }
+
+ async deleteKey(keyFingerprint, deleteSecret) {
+ return null;
+ }
+
+ async encryptAndOrSign(plaintext, args, resultStatus) {
+ return null;
+ }
+}
+
+function getGnuPGAPI() {
+ return new GnuPGCryptoAPI();
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/cryptoAPI/RNPCryptoAPI.jsm b/comm/mail/extensions/openpgp/content/modules/cryptoAPI/RNPCryptoAPI.jsm
new file mode 100644
index 0000000000..6b03bf3c6f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/cryptoAPI/RNPCryptoAPI.jsm
@@ -0,0 +1,282 @@
+/*
+ * 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";
+
+var EXPORTED_SYMBOLS = ["getRNPAPI"];
+
+const { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+
+Services.scriptloader.loadSubScript(
+ "chrome://openpgp/content/modules/cryptoAPI/interface.js",
+ null,
+ "UTF-8"
+); /* global CryptoAPI */
+
+const { EnigmailLog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/log.jsm"
+);
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+
+/**
+ * RNP implementation of CryptoAPI
+ */
+class RNPCryptoAPI extends CryptoAPI {
+ constructor() {
+ super();
+ this.api_name = "RNP";
+ }
+
+ /**
+ * Get the list of all knwn keys (including their secret keys)
+ *
+ * @param {Array of String} onlyKeys: [optional] only load data for specified key IDs
+ *
+ * @returns {Promise<Array of Object>}
+ */
+ async getKeys(onlyKeys = null) {
+ return RNP.getKeys(onlyKeys);
+ }
+
+ /**
+ * Obtain signatures for a given set of key IDs.
+ *
+ * @param {string} keyId: space-separated list of key IDs
+ * @param {boolean} ignoreUnknownUid: if true, filter out unknown signer's UIDs
+ *
+ * @returns {Promise<Array of Object>} - see extractSignatures()
+ */
+ async getKeySignatures(keyId, ignoreUnknownUid = false) {
+ return RNP.getKeySignatures(keyId, ignoreUnknownUid);
+ }
+
+ /**
+ * Obtain signatures for a given key.
+ *
+ * @param {KeyObj} keyObj: the signatures of this key will be returned
+ * @param {boolean} ignoreUnknownUid: if true, filter out unknown signer's UIDs
+ *
+ * @returns {Promise<Array of Object>} - see extractSignatures()
+ */
+ async getKeyObjSignatures(keyId, ignoreUnknownUid = false) {
+ return RNP.getKeyObjSignatures(keyId, ignoreUnknownUid);
+ }
+
+ /**
+ * Export the minimum key for the public key object:
+ * public key, primary 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 specific creation Dates
+ *
+ * @returns {Promise<object>}:
+ * - exitCode (0 = success)
+ * - errorMsg (if exitCode != 0)
+ * - keyData: BASE64-encded string of key data
+ */
+ async getMinimalPubKey(fpr, email, subkeyDates) {
+ throw new Error("Not implemented");
+ }
+
+ async importPubkeyBlockAutoAcceptAPI(
+ win,
+ keyBlock,
+ acceptance,
+ permissive,
+ limitedFPRs = []
+ ) {
+ let res = await RNP.importPubkeyBlockAutoAcceptImpl(
+ win,
+ keyBlock,
+ acceptance,
+ permissive,
+ limitedFPRs
+ );
+ return res;
+ }
+
+ async importRevBlockAPI(data) {
+ return RNP.importRevImpl(data);
+ }
+
+ /**
+ * Export secret key(s) to a file
+ *
+ * @param {string} keyId Specification by fingerprint or keyID
+ * @param {boolean} minimalKey - if true, reduce key to minimum required
+ *
+ * @returns {object}:
+ * - {Number} exitCode: result code (0: OK)
+ * - {String} keyData: ASCII armored key data material
+ * - {String} errorMsg: error message in case exitCode !== 0
+ */
+
+ async extractSecretKey(keyId, minimalKey) {
+ throw new Error("extractSecretKey not implemented");
+ }
+
+ /**
+ *
+ * @param {byte} byteData - The encrypted data
+ *
+ * @returns {String or null} - the name of the attached file
+ */
+
+ async getFileName(byteData) {
+ throw new Error("getFileName not implemented");
+ }
+
+ /**
+ *
+ * @param {Path} filePath - The signed file
+ * @param {Path} sigPath - The signature to verify
+ *
+ * @returns {Promise<string>} - A message from the verification.
+ *
+ * Use Promise.catch to handle failed verifications.
+ * The message will be an error message in this case.
+ */
+
+ async verifyAttachment(filePath, sigPath) {
+ throw new Error("verifyAttachment not implemented");
+ }
+
+ /**
+ *
+ * @param {Bytes} encrypted The encrypted data
+ *
+ * @returns {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 decryptAttachment(encrypted) {
+ let options = {};
+ options.fromAddr = "";
+ options.msgDate = null;
+ return RNP.decrypt(encrypted, options);
+ }
+
+ /**
+ *
+ * @param {string} encrypted - The encrypted data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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.
+ * XXX: it's not... ^^^ This should be changed to always reject
+ * by throwing an Error (subclass?) for failures to decrypt.
+ */
+
+ async decrypt(encrypted, options) {
+ EnigmailLog.DEBUG(`rnp-cryptoAPI.js: decrypt()\n`);
+
+ return RNP.decrypt(encrypted, options);
+ }
+
+ /**
+ *
+ * @param {string} encrypted - The encrypted data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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 decryptMime(encrypted, options) {
+ EnigmailLog.DEBUG(`rnp-cryptoAPI.js: decryptMime()\n`);
+
+ // write something to gpg such that the process doesn't get stuck
+ if (encrypted.length === 0) {
+ encrypted = "NO DATA\n";
+ }
+
+ options.noOutput = false;
+ options.verifyOnly = false;
+ options.uiFlags = EnigmailConstants.UI_PGP_MIME;
+
+ return this.decrypt(encrypted, options);
+ }
+
+ /**
+ *
+ * @param {string} signed - The signed data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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 verifyMime(signed, options) {
+ EnigmailLog.DEBUG(`rnp-cryptoAPI.js: verifyMime()\n`);
+
+ //options.noOutput = true;
+ //options.verifyOnly = true;
+ //options.uiFlags = EnigmailConstants.UI_PGP_MIME;
+
+ if (!options.mimeSignatureData) {
+ throw new Error("inline verify not yet implemented");
+ }
+ return RNP.verifyDetached(signed, options);
+ }
+
+ async getKeyListFromKeyBlockAPI(
+ keyBlockStr,
+ pubkey,
+ seckey,
+ permissive,
+ withPubKey
+ ) {
+ return RNP.getKeyListFromKeyBlockImpl(
+ keyBlockStr,
+ pubkey,
+ seckey,
+ permissive,
+ withPubKey
+ );
+ }
+
+ async genKey(userId, keyType, keySize, expiryTime, passphrase) {
+ let id = RNP.genKey(userId, keyType, keySize, expiryTime, passphrase);
+ await RNP.saveKeyRings();
+ return id;
+ }
+
+ async deleteKey(keyFingerprint, deleteSecret) {
+ return RNP.deleteKey(keyFingerprint, deleteSecret);
+ }
+
+ async encryptAndOrSign(plaintext, args, resultStatus) {
+ return RNP.encryptAndOrSign(plaintext, args, resultStatus);
+ }
+
+ async unlockAndGetNewRevocation(id, pass) {
+ return RNP.unlockAndGetNewRevocation(id, pass);
+ }
+
+ async getPublicKey(id) {
+ return RNP.getPublicKey(id);
+ }
+}
+
+function getRNPAPI() {
+ return new RNPCryptoAPI();
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/cryptoAPI/interface.js b/comm/mail/extensions/openpgp/content/modules/cryptoAPI/interface.js
new file mode 100644
index 0000000000..eb2419a2e1
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/cryptoAPI/interface.js
@@ -0,0 +1,288 @@
+/*
+ * 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";
+
+/**
+ * CryptoAPI - abstract interface
+ */
+
+var inspector;
+
+class CryptoAPI {
+ constructor() {
+ this.api_name = "null";
+ }
+
+ get apiName() {
+ return this.api_name;
+ }
+
+ /**
+ * Synchronize a promise: wait synchonously until a promise has completed and return
+ * the value that the promise returned.
+ *
+ * @param {Promise} promise: the promise to wait for
+ *
+ * @returns {Variant} whatever the promise returns
+ */
+ sync(promise) {
+ if (!inspector) {
+ inspector = Cc["@mozilla.org/jsinspector;1"].createInstance(
+ Ci.nsIJSInspector
+ );
+ }
+
+ let res = null;
+ promise
+ .then(gotResult => {
+ res = gotResult;
+ inspector.exitNestedEventLoop();
+ })
+ .catch(gotResult => {
+ console.log("CryptoAPI.sync() failed result: %o", gotResult);
+ if (gotResult instanceof Error) {
+ inspector.exitNestedEventLoop();
+ throw gotResult;
+ }
+
+ res = gotResult;
+ inspector.exitNestedEventLoop();
+ });
+
+ inspector.enterNestedEventLoop(0);
+ return res;
+ }
+
+ /**
+ * Obtain signatures for a given set of key IDs.
+ *
+ * @param {string} keyId: space-separated list of key IDs
+ * @param {boolean} ignoreUnknownUid: if true, filter out unknown signer's UIDs
+ *
+ * @returns {Promise<Array of Object>} - see extractSignatures()
+ */
+ async getKeySignatures(keyId, ignoreUnknownUid = false) {
+ return null;
+ }
+
+ /**
+ * Obtain signatures for a given key.
+ *
+ * @param {KeyObj} keyObj: the signatures of this key will be returned
+ * @param {boolean} ignoreUnknownUid: if true, filter out unknown signer's UIDs
+ *
+ * @returns {Promise<Array of Object>} - see extractSignatures()
+ */
+ async getKeyObjSignatures(keyObj, ignoreUnknownUid = false) {
+ return null;
+ }
+
+ /**
+ * 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
+ *
+ * @returns {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 the list of all known keys (including their secret keys)
+ *
+ * @param {Array of String} onlyKeys: [optional] only load data for specified key IDs
+ *
+ * @returns {Promise<Array of Object>}
+ */
+ async getKeys(onlyKeys = null) {
+ return [];
+ }
+
+ async importPubkeyBlockAutoAccept(keyBlock) {
+ return null;
+ }
+
+ // return bool success
+ async importRevBlockAPI(data) {
+ return null;
+ }
+
+ /**
+ * Export secret key(s) to a file
+ *
+ * @param {string} keyId Specification by fingerprint or keyID
+ * @param {boolean} minimalKey - if true, reduce key to minimum required
+ *
+ * @returns {object}:
+ * - {Number} exitCode: result code (0: OK)
+ * - {String} keyData: ASCII armored key data material
+ * - {String} errorMsg: error message in case exitCode !== 0
+ */
+
+ async extractSecretKey(keyId, minimalKey) {
+ return null;
+ }
+
+ /**
+ * Determine the file name from OpenPGP data.
+ *
+ * @param {byte} byteData - The encrypted data
+ *
+ * @returns {string} - the name of the attached file
+ */
+
+ async getFileName(byteData) {
+ return null;
+ }
+
+ /**
+ * Verify the detached signature of an attachment (or in other words,
+ * check the signature of a file, given the file and the signature).
+ *
+ * @param {Path} filePath - The signed file
+ * @param {Path} sigPath - The signature to verify
+ *
+ * @returns {Promise<string>} - A message from the verification.
+ *
+ * Use Promise.catch to handle failed verifications.
+ * The message will be an error message in this case.
+ */
+
+ async verifyAttachment(filePath, sigPath) {
+ return null;
+ }
+
+ /**
+ * Decrypt an attachment.
+ *
+ * @param {Bytes} encrypted The encrypted data
+ *
+ * @returns {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 decryptAttachment(encrypted) {
+ return null;
+ }
+
+ /**
+ * Generic function to decrypt and/or verify an OpenPGP message.
+ *
+ * @param {string} encrypted - The encrypted data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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
+ *
+ * @returns {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 decryptMime(encrypted, options) {
+ return null;
+ }
+
+ /**
+ * Verify a PGP/MIME-signed message
+ *
+ * @param {string} signed - The signed data
+ * @param {object} options - Decryption options
+ *
+ * @returns {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 verifyMime(signed, options) {
+ return null;
+ }
+
+ /**
+ * Get details (key ID, UID) of the data contained in a OpenPGP key block
+ *
+ * @param {string} keyBlockStr - String: the contents of one or more public keys
+ *
+ * @returns {Promise<Array>}: array of objects with the following structure:
+ * - id (key ID)
+ * - fpr
+ * - name (the UID of the key)
+ */
+
+ async getKeyListFromKeyBlockAPI(keyBlockStr) {
+ return null;
+ }
+
+ /**
+ * Create a new private key pair, including appropriate sub key pair,
+ * and store the new keys in the default keyrings.
+ *
+ * @param {string} userId - User ID string, with name and email.
+ * @param {string} keyType - "RSA" or "ECC".
+ * ECC uses EDDSA and ECDH/Curve25519.
+ * @param {number} keySize - RSA key size. Ignored for ECC.
+ * @param {number} expiryTime The number of days the key will remain valid
+ * (after the creation date).
+ * Set to zero for no expiration.
+ * @param {string} passphrase The passphrase to protect the new key.
+ * Set to null to use an empty passphrase.
+ *
+ * @returns {Promise<string>} - The new KeyID
+ */
+
+ async genKey(userId, keyType, keySize, expiryTime, passphrase) {
+ return null;
+ }
+
+ async deleteKey(keyFingerprint, deleteSecret) {
+ return null;
+ }
+
+ async encryptAndOrSign(plaintext, args, resultStatus) {
+ return null;
+ }
+
+ async unlockAndGetNewRevocation(id, pass) {
+ return null;
+ }
+
+ async getPublicKey(id) {
+ return null;
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/data.jsm b/comm/mail/extensions/openpgp/content/modules/data.jsm
new file mode 100644
index 0000000000..0dd5cf451f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/data.jsm
@@ -0,0 +1,156 @@
+/*
+ * 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 = ["EnigmailData"];
+
+const SCRIPTABLEUNICODECONVERTER_CONTRACTID =
+ "@mozilla.org/intl/scriptableunicodeconverter";
+
+const HEX_TABLE = "0123456789abcdef";
+
+function converter(charset) {
+ let unicodeConv = Cc[SCRIPTABLEUNICODECONVERTER_CONTRACTID].getService(
+ Ci.nsIScriptableUnicodeConverter
+ );
+ unicodeConv.charset = charset || "utf-8";
+ return unicodeConv;
+}
+
+var EnigmailData = {
+ getUnicodeData(data) {
+ if (!data) {
+ throw new Error("EnigmailData.getUnicodeData invalid parameter");
+ }
+ // convert output to Unicode
+ var tmpStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ tmpStream.setData(data, data.length);
+ var inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ inStream.init(tmpStream);
+ return inStream.read(tmpStream.available());
+ },
+
+ decodeQuotedPrintable(str) {
+ return unescape(
+ str.replace(/%/g, "=25").replace(new RegExp("=", "g"), "%")
+ );
+ },
+
+ decodeBase64(str) {
+ return atob(str.replace(/[\s\r\n]*/g, ""));
+ },
+
+ /***
+ * Encode a string in base64, with a max. line length of 72 characters
+ */
+ encodeBase64(str) {
+ return btoa(str).replace(/(.{72})/g, "$1\r\n");
+ },
+
+ convertToUnicode(text, charset) {
+ if (!text || (charset && charset.toLowerCase() == "iso-8859-1")) {
+ return text;
+ }
+
+ // Encode plaintext
+ let buffer = Uint8Array.from(text, c => c.charCodeAt(0));
+ return new TextDecoder(charset).decode(buffer);
+ },
+
+ convertFromUnicode(text, charset) {
+ if (!text) {
+ return "";
+ }
+
+ let conv = converter(charset);
+ let result = conv.ConvertFromUnicode(text);
+ result += conv.Finish();
+ return result;
+ },
+
+ convertGpgToUnicode(text) {
+ if (typeof text === "string") {
+ text = text.replace(/\\x3a/gi, "\\e3A");
+ var a = text.search(/\\x[0-9a-fA-F]{2}/);
+ while (a >= 0) {
+ var ch = unescape("%" + text.substr(a + 2, 2));
+ var r = new RegExp("\\" + text.substr(a, 4));
+ text = text.replace(r, ch);
+
+ a = text.search(/\\x[0-9a-fA-F]{2}/);
+ }
+
+ text = EnigmailData.convertToUnicode(text, "utf-8").replace(
+ /\\e3A/g,
+ ":"
+ );
+ }
+
+ return text;
+ },
+
+ pack(value, bytes) {
+ let str = "";
+ let mask = 0xff;
+ for (let j = 0; j < bytes; j++) {
+ str = String.fromCharCode((value & mask) >> (j * 8)) + str;
+ mask <<= 8;
+ }
+
+ return str;
+ },
+
+ unpack(str) {
+ let len = str.length;
+ let value = 0;
+
+ for (let j = 0; j < len; j++) {
+ value <<= 8;
+ value |= str.charCodeAt(j);
+ }
+
+ return value;
+ },
+
+ bytesToHex(str) {
+ let len = str.length;
+
+ let hex = "";
+ for (let j = 0; j < len; j++) {
+ let charCode = str.charCodeAt(j);
+ hex +=
+ HEX_TABLE.charAt((charCode & 0xf0) >> 4) +
+ HEX_TABLE.charAt(charCode & 0x0f);
+ }
+
+ return hex;
+ },
+
+ /**
+ * Convert an ArrayBuffer (or Uint8Array) object into a string
+ */
+ arrayBufferToString(buffer) {
+ const MAXLEN = 102400;
+
+ let uArr = new Uint8Array(buffer);
+ let ret = "";
+ let len = buffer.byteLength;
+
+ for (let j = 0; j < Math.floor(len / MAXLEN) + 1; j++) {
+ ret += String.fromCharCode.apply(
+ null,
+ uArr.subarray(j * MAXLEN, (j + 1) * MAXLEN)
+ );
+ }
+
+ return ret;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/decryption.jsm b/comm/mail/extensions/openpgp/content/modules/decryption.jsm
new file mode 100644
index 0000000000..c7f6188eeb
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/decryption.jsm
@@ -0,0 +1,639 @@
+/*
+ * 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/.
+ */
+
+/* eslint-disable complexity */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["EnigmailDecryption"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm",
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+function statusObjectFrom(
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+) {
+ return {
+ signature: signatureObj,
+ exitCode: exitCodeObj,
+ statusFlags: statusFlagsObj,
+ keyId: keyIdObj,
+ userId: userIdObj,
+ sigDetails: sigDetailsObj,
+ message: errorMsgObj,
+ blockSeparation: blockSeparationObj,
+ encToDetails: encToDetailsObj,
+ };
+}
+
+function newStatusObject() {
+ return statusObjectFrom(
+ {
+ value: "",
+ },
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {}
+ );
+}
+
+var EnigmailDecryption = {
+ isReady(win) {
+ // this used to return false while generating a key. still necessary?
+ return lazy.EnigmailCore.getService(win);
+ },
+
+ getFromAddr(win) {
+ var fromAddr;
+ if (win?.gMessage) {
+ fromAddr = win.gMessage.author;
+ }
+ if (fromAddr) {
+ try {
+ fromAddr = lazy.EnigmailFuncs.stripEmail(fromAddr);
+ if (fromAddr.search(/[a-zA-Z0-9]@.*[\(\)]/) >= 0) {
+ fromAddr = false;
+ }
+ } catch (ex) {
+ fromAddr = false;
+ }
+ }
+
+ return fromAddr;
+ },
+
+ getMsgDate(win) {
+ // Sometimes the "dateInSeconds" attribute is missing.
+ // "date" appears to be available more reliably, and it appears
+ // to be in microseconds (1/1000000 second). Convert
+ // to milliseconds (1/1000 of a second) for conversion to Date.
+ if (win?.gMessage) {
+ return new Date(win.gMessage.date / 1000);
+ }
+ return null;
+ },
+
+ /**
+ * Decrypts a PGP ciphertext and returns the the plaintext
+ *
+ *in @parent a window object
+ *in @uiFlags see flag options in EnigmailConstants, UI_INTERACTIVE, UI_ALLOW_KEY_IMPORT
+ *in @cipherText a string containing a PGP Block
+ *out @signatureObj
+ *out @exitCodeObj contains the exit code
+ *out @statusFlagsObj see status flags in nslEnigmail.idl, GOOD_SIGNATURE, BAD_SIGNATURE
+ *out @keyIdObj holds the key id
+ *out @userIdObj holds the user id
+ *out @sigDetailsObj
+ *out @errorMsgObj error string
+ *out @blockSeparationObj
+ *out @encToDetailsObj returns in details, which keys the message was encrypted for (ENC_TO entries)
+ *
+ * @returns string plaintext ("" if error)
+ *
+ */
+ decryptMessage(
+ parent,
+ uiFlags,
+ cipherText,
+ msgDate,
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "decryption.jsm: decryptMessage(" +
+ cipherText.length +
+ " bytes, " +
+ uiFlags +
+ ")\n"
+ );
+
+ if (!cipherText) {
+ return "";
+ }
+
+ //var interactive = uiFlags & EnigmailConstants.UI_INTERACTIVE;
+ var allowImport = false; // uiFlags & EnigmailConstants.UI_ALLOW_KEY_IMPORT;
+ var unverifiedEncryptedOK =
+ uiFlags & lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK;
+ var oldSignature = signatureObj.value;
+
+ lazy.EnigmailLog.DEBUG(
+ "decryption.jsm: decryptMessage: oldSignature=" + oldSignature + "\n"
+ );
+
+ signatureObj.value = "";
+ exitCodeObj.value = -1;
+ statusFlagsObj.value = 0;
+ statusFlagsObj.ext = 0;
+ keyIdObj.value = "";
+ userIdObj.value = "";
+ errorMsgObj.value = "";
+
+ var beginIndexObj = {};
+ var endIndexObj = {};
+ var indentStrObj = {};
+ var blockType = lazy.EnigmailArmor.locateArmoredBlock(
+ cipherText,
+ 0,
+ "",
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ );
+ if (!blockType || blockType == "SIGNATURE") {
+ // return without displaying a message
+ return "";
+ }
+
+ var publicKey = blockType == "PUBLIC KEY BLOCK";
+
+ var verifyOnly = blockType == "SIGNED MESSAGE";
+ var isEncrypted = blockType == "MESSAGE";
+
+ if (verifyOnly) {
+ statusFlagsObj.value |= lazy.EnigmailConstants.PGP_MIME_SIGNED;
+ }
+ if (isEncrypted) {
+ statusFlagsObj.value |= lazy.EnigmailConstants.PGP_MIME_ENCRYPTED;
+ }
+
+ var pgpBlock = cipherText.substr(
+ beginIndexObj.value,
+ endIndexObj.value - beginIndexObj.value + 1
+ );
+
+ if (indentStrObj.value) {
+ // Escape regex chars.
+ indentStrObj.value = indentStrObj.value.replace(
+ /[.*+\-?^${}()|[\]\\]/g,
+ "\\$&"
+ );
+ var indentRegexp = new RegExp("^" + indentStrObj.value, "gm");
+ pgpBlock = pgpBlock.replace(indentRegexp, "");
+ if (indentStrObj.value.substr(-1) == " ") {
+ var indentRegexpStr = "^" + indentStrObj.value.replace(/ $/m, "$");
+ indentRegexp = new RegExp(indentRegexpStr, "gm");
+ pgpBlock = pgpBlock.replace(indentRegexp, "");
+ }
+ }
+
+ // HACK to better support messages from Outlook: if there are empty lines, drop them
+ if (pgpBlock.search(/MESSAGE-----\r?\n\r?\nVersion/) >= 0) {
+ lazy.EnigmailLog.DEBUG(
+ "decryption.jsm: decryptMessage: apply Outlook empty line workaround\n"
+ );
+ pgpBlock = pgpBlock.replace(/\r?\n\r?\n/g, "\n");
+ }
+
+ var tail = cipherText.substr(
+ endIndexObj.value + 1,
+ cipherText.length - endIndexObj.value - 1
+ );
+
+ if (publicKey) {
+ // TODO: import key into our scratch area for new, unknown keys
+ if (!allowImport) {
+ errorMsgObj.value = lazy.l10n.formatValueSync("key-in-message-body");
+ statusFlagsObj.value |= lazy.EnigmailConstants.DISPLAY_MESSAGE;
+ statusFlagsObj.value |= lazy.EnigmailConstants.INLINE_KEY;
+
+ return "";
+ }
+
+ // Import public key
+ exitCodeObj.value = lazy.EnigmailKeyRing.importKey(
+ parent,
+ true,
+ pgpBlock,
+ false,
+ "",
+ errorMsgObj,
+ {}, // importedKeysObj
+ false,
+ [],
+ false // don't use prompt for permissive
+ );
+ if (exitCodeObj.value === 0) {
+ statusFlagsObj.value |= lazy.EnigmailConstants.IMPORTED_KEY;
+ }
+ return "";
+ }
+
+ var newSignature = "";
+
+ if (verifyOnly) {
+ newSignature = lazy.EnigmailArmor.extractSignaturePart(
+ pgpBlock,
+ lazy.EnigmailConstants.SIGNATURE_ARMOR
+ );
+ if (oldSignature && newSignature != oldSignature) {
+ lazy.EnigmailLog.ERROR(
+ "enigmail.js: Enigmail.decryptMessage: Error - signature mismatch " +
+ newSignature +
+ "\n"
+ );
+ errorMsgObj.value = lazy.l10n.formatValueSync("sig-mismatch");
+ statusFlagsObj.value |= lazy.EnigmailConstants.DISPLAY_MESSAGE;
+
+ return "";
+ }
+ }
+
+ if (!lazy.EnigmailCore.getService(parent)) {
+ statusFlagsObj.value |= lazy.EnigmailConstants.DISPLAY_MESSAGE;
+ throw new Error("decryption.jsm: decryptMessage: not yet initialized");
+ //return "";
+ }
+
+ /*
+ if (EnigmailKeyRing.isGeneratingKey()) {
+ errorMsgObj.value = "Error - key generation not yet completed";
+ statusFlagsObj.value |= EnigmailConstants.DISPLAY_MESSAGE;
+ return "";
+ }
+ */
+
+ // limit output to 100 times message size to avoid DoS attack
+ var maxOutput = pgpBlock.length * 100;
+ let options = {
+ fromAddr: EnigmailDecryption.getFromAddr(parent),
+ verifyOnly,
+ noOutput: false,
+ maxOutputLength: maxOutput,
+ uiFlags,
+ msgDate,
+ };
+ const cApi = lazy.EnigmailCryptoAPI();
+ let result = cApi.sync(cApi.decrypt(pgpBlock, options));
+ lazy.EnigmailLog.DEBUG(
+ "decryption.jsm: decryptMessage: decryption finished\n"
+ );
+ if (!result) {
+ console.debug("EnigmailCryptoAPI.decrypt() failed with empty result");
+ return "";
+ }
+
+ let plainText = this.getPlaintextFromDecryptResult(result);
+ exitCodeObj.value = result.exitCode;
+ statusFlagsObj.value = result.statusFlags;
+ errorMsgObj.value = result.errorMsg;
+
+ userIdObj.value = result.userId;
+ keyIdObj.value = result.keyId;
+ sigDetailsObj.value = result.sigDetails;
+ if (encToDetailsObj) {
+ encToDetailsObj.value = result.encToDetails;
+ }
+ blockSeparationObj.value = result.blockSeparation;
+
+ if (tail.search(/\S/) >= 0) {
+ statusFlagsObj.value |= lazy.EnigmailConstants.PARTIALLY_PGP;
+ }
+
+ if (exitCodeObj.value === 0) {
+ // Normal return
+
+ let doubleDashSeparator = Services.prefs.getBoolPref(
+ "doubleDashSeparator",
+ false
+ );
+
+ if (doubleDashSeparator && plainText.search(/(\r|\n)-- +(\r|\n)/) < 0) {
+ // Workaround for MsgCompose stripping trailing spaces from sig separator
+ plainText = plainText.replace(/(\r|\n)--(\r|\n)/, "$1-- $2");
+ }
+
+ statusFlagsObj.value |= lazy.EnigmailConstants.DISPLAY_MESSAGE;
+
+ if (verifyOnly && indentStrObj.value) {
+ plainText = plainText.replace(/^/gm, indentStrObj.value);
+ }
+
+ return EnigmailDecryption.inlineInnerVerification(
+ parent,
+ uiFlags,
+ plainText,
+ statusObjectFrom(
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ )
+ );
+ }
+
+ var pubKeyId = keyIdObj.value;
+
+ if (statusFlagsObj.value & lazy.EnigmailConstants.BAD_SIGNATURE) {
+ if (verifyOnly && indentStrObj.value) {
+ // Probably replied message that could not be verified
+ errorMsgObj.value =
+ lazy.l10n.formatValueSync("unverified-reply") +
+ "\n\n" +
+ errorMsgObj.value;
+ return "";
+ }
+
+ // Return bad signature (for checking later)
+ signatureObj.value = newSignature;
+ } else if (
+ pubKeyId &&
+ statusFlagsObj.value & lazy.EnigmailConstants.UNCERTAIN_SIGNATURE
+ ) {
+ // TODO: import into scratch area
+ /*
+ var innerKeyBlock;
+ if (verifyOnly) {
+ // Search for indented public key block in signed message
+ var innerBlockType = EnigmailArmor.locateArmoredBlock(
+ pgpBlock,
+ 0,
+ "- ",
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ );
+ if (innerBlockType == "PUBLIC KEY BLOCK") {
+ innerKeyBlock = pgpBlock.substr(
+ beginIndexObj.value,
+ endIndexObj.value - beginIndexObj.value + 1
+ );
+
+ innerKeyBlock = innerKeyBlock.replace(/- -----/g, "-----");
+
+ statusFlagsObj.value |= EnigmailConstants.INLINE_KEY;
+ EnigmailLog.DEBUG(
+ "decryption.jsm: decryptMessage: innerKeyBlock found\n"
+ );
+ }
+ }
+
+ var importedKey = false;
+
+ if (innerKeyBlock) {
+ var importErrorMsgObj = {};
+ var exitStatus = EnigmailKeyRing.importKey(
+ parent,
+ true,
+ innerKeyBlock,
+ false,
+ pubKeyId,
+ importErrorMsgObj
+ );
+
+ importedKey = exitStatus === 0;
+
+ if (exitStatus > 0) {
+ l10n.formatValue("cant-import").then(value => {
+ EnigmailDialog.alert(
+ parent,
+ value + "\n" + importErrorMsgObj.value
+ );
+ });
+ }
+ }
+
+ if (importedKey) {
+ // Recursive call; note that EnigmailConstants.UI_ALLOW_KEY_IMPORT is unset
+ // to break the recursion
+ var uiFlagsDeep = interactive ? EnigmailConstants.UI_INTERACTIVE : 0;
+ signatureObj.value = "";
+ return EnigmailDecryption.decryptMessage(
+ parent,
+ uiFlagsDeep,
+ pgpBlock,
+ null, // date
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj
+ );
+ }
+ */
+
+ if (plainText && !unverifiedEncryptedOK) {
+ // Append original PGP block to unverified message
+ plainText =
+ "-----BEGIN PGP UNVERIFIED MESSAGE-----\r\n" +
+ plainText +
+ "-----END PGP UNVERIFIED MESSAGE-----\r\n\r\n" +
+ pgpBlock;
+ }
+ }
+
+ return verifyOnly ? "" : plainText;
+ },
+
+ inlineInnerVerification(parent, uiFlags, text, statusObject) {
+ lazy.EnigmailLog.DEBUG("decryption.jsm: inlineInnerVerification()\n");
+
+ if (text && text.indexOf("-----BEGIN PGP SIGNED MESSAGE-----") === 0) {
+ var status = newStatusObject();
+ var newText = EnigmailDecryption.decryptMessage(
+ parent,
+ uiFlags,
+ text,
+ null, // date
+ status.signature,
+ status.exitCode,
+ status.statusFlags,
+ status.keyId,
+ status.userId,
+ status.sigDetails,
+ status.message,
+ status.blockSeparation,
+ status.encToDetails
+ );
+ if (status.exitCode.value === 0) {
+ text = newText;
+ // merge status into status object:
+ statusObject.statusFlags.value =
+ statusObject.statusFlags.value | status.statusFlags.value;
+ statusObject.keyId.value = status.keyId.value;
+ statusObject.userId.value = status.userId.value;
+ statusObject.sigDetails.value = status.sigDetails.value;
+ statusObject.message.value = status.message.value;
+ // we don't merge encToDetails
+ }
+ }
+
+ return text;
+ },
+
+ isDecryptFailureResult(result) {
+ if (result.statusFlags & lazy.EnigmailConstants.MISSING_MDC) {
+ console.log("bad message, missing MDC");
+ } else if (result.statusFlags & lazy.EnigmailConstants.DECRYPTION_FAILED) {
+ console.log("cannot decrypt message");
+ } else if (result.decryptedData) {
+ return false;
+ }
+ return true;
+ },
+
+ getPlaintextFromDecryptResult(result) {
+ if (this.isDecryptFailureResult(result)) {
+ return "";
+ }
+ return lazy.EnigmailData.getUnicodeData(result.decryptedData);
+ },
+
+ async decryptAttachment(
+ parent,
+ outFile,
+ displayName,
+ byteData,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "decryption.jsm: decryptAttachment(parent=" +
+ parent +
+ ", outFileName=" +
+ outFile.path +
+ ")\n"
+ );
+
+ let attachmentHead = byteData.substr(0, 200);
+ if (attachmentHead.match(/-----BEGIN PGP \w{5,10} KEY BLOCK-----/)) {
+ // attachment appears to be a PGP key file
+
+ if (
+ lazy.EnigmailDialog.confirmDlg(
+ parent,
+ lazy.l10n.formatValueSync("attachment-pgp-key", {
+ name: displayName,
+ }),
+ lazy.l10n.formatValueSync("key-man-button-import"),
+ lazy.l10n.formatValueSync("dlg-button-view")
+ )
+ ) {
+ let preview = await lazy.EnigmailKey.getKeyListFromKeyBlock(
+ byteData,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ exitCodeObj.keyList = preview;
+ if (preview && errorMsgObj.value === "") {
+ if (preview.length > 0) {
+ let confirmImport = false;
+ let outParam = {};
+ confirmImport = lazy.EnigmailDialog.confirmPubkeyImport(
+ parent,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ exitCodeObj.value = lazy.EnigmailKeyRing.importKey(
+ parent,
+ false,
+ byteData,
+ false,
+ "",
+ errorMsgObj,
+ null,
+ false,
+ [],
+ false, // don't use prompt for permissive
+ outParam.acceptance
+ );
+ statusFlagsObj.value = lazy.EnigmailConstants.IMPORTED_KEY;
+ } else {
+ exitCodeObj.value = 0;
+ statusFlagsObj.value = lazy.EnigmailConstants.DISPLAY_MESSAGE;
+ }
+ }
+ } else {
+ console.debug(
+ "Failed to obtain key list from key block in decrypted attachment. " +
+ errorMsgObj.value
+ );
+ }
+ } else {
+ exitCodeObj.value = 0;
+ statusFlagsObj.value = lazy.EnigmailConstants.DISPLAY_MESSAGE;
+ }
+ statusFlagsObj.ext = 0;
+ return true;
+ }
+
+ const cApi = lazy.EnigmailCryptoAPI();
+ let result = await cApi.decryptAttachment(byteData);
+ if (!result) {
+ console.debug(
+ "EnigmailCryptoAPI.decryptAttachment() failed with empty result"
+ );
+ return false;
+ }
+
+ exitCodeObj.value = result.exitCode;
+ statusFlagsObj.value = result.statusFlags;
+ errorMsgObj.value = result.errorMsg;
+
+ if (!this.isDecryptFailureResult(result)) {
+ await IOUtils.write(
+ outFile.path,
+ lazy.MailStringUtils.byteStringToUint8Array(result.decryptedData)
+ );
+ return true;
+ }
+
+ return false;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/dialog.jsm b/comm/mail/extensions/openpgp/content/modules/dialog.jsm
new file mode 100644
index 0000000000..a97db6094f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/dialog.jsm
@@ -0,0 +1,481 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailDialog"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var EnigmailDialog = {
+ /***
+ * Confirmation dialog with OK / Cancel buttons (both customizable)
+ *
+ * @win: nsIWindow - parent window to display modal dialog; can be null
+ * @mesg: String - message text
+ * @okLabel: String - OPTIONAL label for OK button
+ * @cancelLabel: String - OPTIONAL label for cancel button
+ *
+ * @return: Boolean - true: OK pressed / false: Cancel or ESC pressed
+ */
+ confirmDlg(win, mesg, okLabel, cancelLabel) {
+ let buttonPressed = EnigmailDialog.msgBox(
+ win,
+ {
+ msgtext: mesg,
+ button1: okLabel ? okLabel : lazy.l10n.formatValueSync("dlg-button-ok"),
+ cancelButton: cancelLabel
+ ? cancelLabel
+ : lazy.l10n.formatValueSync("dlg-button-cancel"),
+ iconType: lazy.EnigmailConstants.ICONTYPE_QUESTION,
+ dialogTitle: lazy.l10n.formatValueSync("enig-confirm"),
+ },
+ null
+ );
+
+ return buttonPressed === 0;
+ },
+
+ /**
+ * Displays an alert dialog.
+ *
+ * @win: nsIWindow - parent window to display modal dialog; can be null
+ * @mesg: String - message text
+ *
+ * no return value
+ */
+ alert(win, mesg) {
+ EnigmailDialog.msgBox(
+ win,
+ {
+ msgtext: mesg,
+ button1: lazy.l10n.formatValueSync("dlg-button-close"),
+ iconType: lazy.EnigmailConstants.ICONTYPE_ALERT,
+ dialogTitle: lazy.l10n.formatValueSync("enig-alert"),
+ },
+ null
+ );
+ },
+
+ /**
+ * Displays an information dialog.
+ *
+ * @win: nsIWindow - parent window to display modal dialog; can be null
+ * @mesg: String - message text
+ *
+ * no return value
+ */
+ info(win, mesg) {
+ EnigmailDialog.msgBox(
+ win,
+ {
+ msgtext: mesg,
+ button1: lazy.l10n.formatValueSync("dlg-button-close"),
+ iconType: lazy.EnigmailConstants.ICONTYPE_INFO,
+ dialogTitle: lazy.l10n.formatValueSync("enig-info"),
+ },
+ null
+ );
+ },
+
+ /**
+ * Displays a message box with 1-3 optional buttons.
+ *
+ * @win: nsIWindow - parent window to display modal dialog; can be null
+ * @argsObj: Object:
+ * - msgtext: String - message text
+ * - dialogTitle: String - title of the dialog
+ * - checkboxLabel: String - if not null, display checkbox with text; the
+ * checkbox state is returned in checkedObj.value
+ * - iconType: Number - Icon type: 1=Message / 2=Question / 3=Alert / 4=Error
+ *
+ * - buttonX: String - Button label (button 1-3) [button1 = "accept" button]
+ * use "&" to indicate access key
+ * - cancelButton String - Label for cancel button
+ * use "buttonType:label" or ":buttonType" to indicate special button types
+ * (buttonType is one of cancel, help, extra1, extra2)
+ * if no button is provided, OK will be displayed
+ *
+ * @checkedObj: Object - holding the checkbox value
+ *
+ * @return: 0-2: button Number pressed
+ * -1: cancel button, ESC or close window button pressed
+ *
+ */
+ msgBox(win, argsObj, checkedObj) {
+ var result = {
+ value: -1,
+ checked: false,
+ };
+
+ if (!win) {
+ win = lazy.EnigmailWindows.getBestParentWin();
+ }
+
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailMsgBox.xhtml",
+ "",
+ "chrome,dialog,modal,centerscreen,resizable",
+ argsObj,
+ result
+ );
+
+ if (argsObj.checkboxLabel) {
+ checkedObj.value = result.checked;
+ }
+ return result.value;
+ },
+
+ /**
+ * Display an alert message with an OK button and a checkbox to hide
+ * the message in the future.
+ * In case the checkbox was pressed in the past, the dialog is skipped
+ *
+ * @win: nsIWindow - the parent window to hold the modal dialog
+ * @mesg: String - the localized message to display
+ * @prefText: String - the name of the Enigmail preference to read/store the
+ * the future display status
+ */
+ alertPref(win, mesg, prefText) {
+ let prefValue = Services.prefs.getBoolPref("temp.openpgp." + prefText);
+ if (prefValue) {
+ let checkBoxObj = {
+ value: false,
+ };
+
+ let buttonPressed = EnigmailDialog.msgBox(
+ win,
+ {
+ msgtext: mesg,
+ dialogTitle: lazy.l10n.formatValueSync("enig-info"),
+ iconType: lazy.EnigmailConstants.ICONTYPE_INFO,
+ checkboxLabel: lazy.l10n.formatValueSync("dlg-no-prompt"),
+ },
+ checkBoxObj
+ );
+
+ if (checkBoxObj.value && buttonPressed === 0) {
+ Services.prefs.setBoolPref(prefText, false);
+ }
+ }
+ },
+
+ /**
+ * Display an alert dialog together with the message "this dialog will be
+ * displayed |counter| more times".
+ * If |counter| is 0, the dialog is not displayed.
+ *
+ * @win: nsIWindow - the parent window to hold the modal dialog
+ * @countPrefName: String - the name of the Enigmail preference to read/store the
+ * the |counter| value
+ * @mesg: String - the localized message to display
+ *
+ */
+ alertCount(win, countPrefName, mesg) {
+ let alertCount = Services.prefs.getIntPref("temp.openpgp." + countPrefName);
+ if (alertCount <= 0) {
+ return;
+ }
+
+ alertCount--;
+ Services.prefs.setIntPref(countPrefName, alertCount);
+
+ if (alertCount > 0) {
+ mesg +=
+ "\n" +
+ lazy.l10n.formatValueSync("repeat-prefix", { count: alertCount }) +
+ " ";
+ mesg +=
+ alertCount == 1
+ ? lazy.l10n.formatValueSync("repeat-suffix-singular")
+ : lazy.l10n.formatValueSync("repeat-suffix-plural");
+ } else {
+ mesg += "\n" + lazy.l10n.formatValueSync("no-repeat");
+ }
+
+ EnigmailDialog.alert(win, mesg);
+ },
+
+ /**
+ * Display a confirmation dialog with OK / Cancel buttons (both customizable) and
+ * a checkbox to remember the selected choice.
+ *
+ *
+ * @param {nsIWindow} win - Parent window to display modal dialog; can be null
+ * @param {mesg} - Mssage text
+ * @param {pref} - Full name of preference to read/store the future display status.
+ *
+ * @param {string} [okLabel] - Label for Ok button.
+ * @param {string} [cancelLabel] - Label for Cancel button.
+ *
+ * @returns {integer} 1: Ok pressed / 0: Cancel pressed / -1: ESC pressed
+ *
+ * If the dialog is not displayed:
+ * - if @prefText is type Boolean: return 1
+ * - if @prefText is type Number: return the last choice of the user
+ */
+ confirmBoolPref(win, mesg, pref, okLabel, cancelLabel) {
+ var prefValue = Services.prefs.getBoolPref(pref);
+ // boolean: "do not show this dialog anymore" (and return default)
+ switch (prefValue) {
+ case true: {
+ // display
+ let checkBoxObj = {
+ value: false,
+ };
+ let buttonPressed = EnigmailDialog.msgBox(
+ win,
+ {
+ msgtext: mesg,
+ button1: okLabel
+ ? okLabel
+ : lazy.l10n.formatValueSync("dlg-button-ok"),
+ cancelButton: cancelLabel
+ ? cancelLabel
+ : lazy.l10n.formatValueSync("dlg-button-cancel"),
+ checkboxLabel: lazy.l10n.formatValueSync("dlg-no-prompt"),
+ iconType: lazy.EnigmailConstants.ICONTYPE_QUESTION,
+ dialogTitle: lazy.l10n.formatValueSync("enig-confirm"),
+ },
+ checkBoxObj
+ );
+
+ if (checkBoxObj.value) {
+ Services.prefs.setBoolPref(pref, false);
+ }
+ return buttonPressed === 0 ? 1 : 0;
+ }
+ case false: // don't display
+ return 1;
+ default:
+ return -1;
+ }
+ },
+
+ confirmIntPref(win, mesg, pref, okLabel, cancelLabel) {
+ let prefValue = Services.prefs.getIntPref(pref);
+ // number: remember user's choice
+ switch (prefValue) {
+ case 0: {
+ // not set
+ let checkBoxObj = {
+ value: false,
+ };
+ let buttonPressed = EnigmailDialog.msgBox(
+ win,
+ {
+ msgtext: mesg,
+ button1: okLabel
+ ? okLabel
+ : lazy.l10n.formatValueSync("dlg-button-ok"),
+ cancelButton: cancelLabel
+ ? cancelLabel
+ : lazy.l10n.formatValueSync("dlg-button-cancel"),
+ checkboxLabel: lazy.l10n.formatValueSync("dlg-keep-setting"),
+ iconType: lazy.EnigmailConstants.ICONTYPE_QUESTION,
+ dialogTitle: lazy.l10n.formatValueSync("enig-confirm"),
+ },
+ checkBoxObj
+ );
+
+ if (checkBoxObj.value) {
+ Services.prefs.setIntPref(pref, buttonPressed === 0 ? 1 : 0);
+ }
+ return buttonPressed === 0 ? 1 : 0;
+ }
+ case 1: // yes
+ return 1;
+ case 2: // no
+ return 0;
+ }
+ return -1;
+ },
+
+ /**
+ * Display a "open file" or "save file" dialog
+ *
+ * win: nsIWindow - parent window
+ * title: String - window title
+ * displayDir: String - optional: directory to be displayed
+ * save: Boolean - true = Save file / false = Open file
+ * multiple: Boolean - true = Select multiple files / false = Select single file
+ * defaultExtension: String - optional: extension for the type of files to work with, e.g. "asc"
+ * defaultName: String - optional: filename, incl. extension, that should be suggested to
+ * the user as default, e.g. "keys.asc"
+ * filterPairs: Array - optional: [title, extension], e.g. ["Pictures", "*.jpg; *.png"]
+ *
+ * return value: nsIFile object, or array of nsIFile objects,
+ * representing the file(s) to load or save
+ */
+ filePicker(
+ win,
+ title,
+ displayDir,
+ save,
+ multiple,
+ defaultExtension,
+ defaultName,
+ filterPairs
+ ) {
+ let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance();
+ filePicker = filePicker.QueryInterface(Ci.nsIFilePicker);
+
+ let open = multiple
+ ? Ci.nsIFilePicker.modeOpenMultiple
+ : Ci.nsIFilePicker.modeOpen;
+ let mode = save ? Ci.nsIFilePicker.modeSave : open;
+
+ filePicker.init(win, title, mode);
+ if (displayDir) {
+ var localFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+
+ try {
+ localFile.initWithPath(displayDir);
+ filePicker.displayDirectory = localFile;
+ } catch (ex) {}
+ }
+
+ if (defaultExtension) {
+ filePicker.defaultExtension = defaultExtension;
+ }
+
+ if (defaultName) {
+ filePicker.defaultString = defaultName;
+ }
+
+ let nfilters = 0;
+ if (filterPairs && filterPairs.length) {
+ nfilters = filterPairs.length / 2;
+ }
+
+ for (let index = 0; index < nfilters; index++) {
+ filePicker.appendFilter(
+ filterPairs[2 * index],
+ filterPairs[2 * index + 1]
+ );
+ }
+
+ filePicker.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ let inspector = Cc["@mozilla.org/jsinspector;1"].createInstance(
+ Ci.nsIJSInspector
+ );
+ let files = [];
+ filePicker.open(res => {
+ if (
+ res != Ci.nsIFilePicker.returnOK &&
+ res != Ci.nsIFilePicker.returnReplace
+ ) {
+ inspector.exitNestedEventLoop();
+ return;
+ }
+
+ // Loop through multiple selected files only if the dialog was triggered
+ // to open files and the `multiple` boolean variable is true.
+ if (!save && multiple) {
+ for (let file of filePicker.files) {
+ // XXX: for some reason QI is needed on Mac.
+ files.push(file.QueryInterface(Ci.nsIFile));
+ }
+ } else {
+ files.push(filePicker.file);
+ }
+
+ inspector.exitNestedEventLoop();
+ });
+
+ inspector.enterNestedEventLoop(0); // wait for async process to terminate
+
+ return multiple ? files : files[0];
+ },
+
+ /**
+ * Displays a dialog with success/failure information after importing
+ * keys.
+ *
+ * @param win: nsIWindow - parent window to display modal dialog; can be null
+ * @param keyList: Array of String - imported keyIDs
+ *
+ * @return: 0-2: button Number pressed
+ * -1: ESC or close window button pressed
+ *
+ */
+ keyImportDlg(win, keyList) {
+ var result = {
+ value: -1,
+ checked: false,
+ };
+
+ if (!win) {
+ win = lazy.EnigmailWindows.getBestParentWin();
+ }
+
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailKeyImportInfo.xhtml",
+ "",
+ "chrome,dialog,modal,centerscreen,resizable",
+ {
+ keyList,
+ },
+ result
+ );
+
+ return result.value;
+ },
+ /**
+ * return a pre-initialized prompt service
+ */
+ getPromptSvc() {
+ return Services.prompt;
+ },
+
+ /**
+ * Asks user to confirm the import of the given public keys.
+ * User is allowed to automatically accept new/undecided keys.
+ *
+ * @param {nsIDOMWindow} parentWindow - Parent window.
+ * @param {object[]} keyPreview - Key details. See EnigmailKey.getKeyListFromKeyBlock().
+ * @param {EnigmailKeyObj[]} - Array of key objects.
+ * @param {object} outputParams - Out parameters.
+ * @param {string} outputParams.acceptance contains the decision. If confirmed.
+ * @returns {boolean} true if user confirms import
+ *
+ */
+ confirmPubkeyImport(parentWindow, keyPreview, outputParams) {
+ let args = {
+ keys: keyPreview,
+ confirmed: false,
+ acceptance: "",
+ };
+
+ parentWindow.browsingContext.topChromeWindow.openDialog(
+ "chrome://openpgp/content/ui/confirmPubkeyImport.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable",
+ args
+ );
+
+ if (args.confirmed && outputParams) {
+ outputParams.acceptance = args.acceptance;
+ }
+ return args.confirmed;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/encryption.jsm b/comm/mail/extensions/openpgp/content/modules/encryption.jsm
new file mode 100644
index 0000000000..b02336bb91
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/encryption.jsm
@@ -0,0 +1,564 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailEncryption"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+const gMimeHashAlgorithms = [
+ null,
+ "sha1",
+ "ripemd160",
+ "sha256",
+ "sha384",
+ "sha512",
+ "sha224",
+ "md5",
+];
+
+const ENC_TYPE_MSG = 0;
+const ENC_TYPE_ATTACH_BINARY = 1;
+
+var EnigmailEncryption = {
+ // return object on success, null on failure
+ getCryptParams(
+ fromMailAddr,
+ toMailAddr,
+ bccMailAddr,
+ hashAlgorithm,
+ sendFlags,
+ isAscii,
+ errorMsgObj,
+ logFileObj
+ ) {
+ let result = {};
+ result.sender = "";
+ result.sign = false;
+ result.signatureHash = "";
+ result.sigTypeClear = false;
+ result.sigTypeDetached = false;
+ result.encrypt = false;
+ result.encryptToSender = false;
+ result.armor = false;
+ result.senderKeyIsExternal = false;
+
+ lazy.EnigmailLog.DEBUG(
+ "encryption.jsm: getCryptParams: hashAlgorithm=" + hashAlgorithm + "\n"
+ );
+
+ try {
+ fromMailAddr = lazy.EnigmailFuncs.stripEmail(fromMailAddr);
+ toMailAddr = lazy.EnigmailFuncs.stripEmail(toMailAddr);
+ bccMailAddr = lazy.EnigmailFuncs.stripEmail(bccMailAddr);
+ } catch (ex) {
+ errorMsgObj.value = lazy.l10n.formatValueSync("invalid-email");
+ return null;
+ }
+
+ var signMsg = sendFlags & lazy.EnigmailConstants.SEND_SIGNED;
+ var encryptMsg = sendFlags & lazy.EnigmailConstants.SEND_ENCRYPTED;
+ var usePgpMime = sendFlags & lazy.EnigmailConstants.SEND_PGP_MIME;
+
+ if (sendFlags & lazy.EnigmailConstants.SEND_SENDER_KEY_EXTERNAL) {
+ result.senderKeyIsExternal = true;
+ }
+
+ // Some day we might need to look at flag SEND_TWO_MIME_LAYERS here,
+ // to decide which detached signature flag needs to be passed on
+ // to the RNP or GPGME layers. However, today those layers can
+ // derive their necessary behavior from being asked to do combined
+ // or single encryption/signing. This is because today we always
+ // create signed messages using the detached signature, and we never
+ // need the OpenPGP signature encoding that includes the message
+ // except when combining GPG signing with RNP encryption.
+
+ var detachedSig =
+ (usePgpMime || sendFlags & lazy.EnigmailConstants.SEND_ATTACHMENT) &&
+ signMsg &&
+ !encryptMsg;
+
+ result.to = toMailAddr.split(/\s*,\s*/);
+ result.bcc = bccMailAddr.split(/\s*,\s*/);
+ result.aliasKeys = new Map();
+
+ if (result.to.length == 1 && result.to[0].length == 0) {
+ result.to.splice(0, 1); // remove the single empty entry
+ }
+
+ if (result.bcc.length == 1 && result.bcc[0].length == 0) {
+ result.bcc.splice(0, 1); // remove the single empty entry
+ }
+
+ if (/^0x[0-9a-f]+$/i.test(fromMailAddr)) {
+ result.sender = fromMailAddr;
+ } else {
+ result.sender = "<" + fromMailAddr + ">";
+ }
+ result.sender = result.sender.replace(/(["'`])/g, "\\$1");
+
+ if (signMsg && hashAlgorithm) {
+ result.signatureHash = hashAlgorithm;
+ }
+
+ if (encryptMsg) {
+ if (isAscii != ENC_TYPE_ATTACH_BINARY) {
+ result.armor = true;
+ }
+ result.encrypt = true;
+
+ if (signMsg) {
+ result.sign = true;
+ }
+
+ if (
+ sendFlags & lazy.EnigmailConstants.SEND_ENCRYPT_TO_SELF &&
+ fromMailAddr
+ ) {
+ result.encryptToSender = true;
+ }
+
+ let recipArrays = ["to", "bcc"];
+ for (let recipArray of recipArrays) {
+ let kMax = recipArray == "to" ? result.to.length : result.bcc.length;
+ for (let k = 0; k < kMax; k++) {
+ let email = recipArray == "to" ? result.to[k] : result.bcc[k];
+ if (!email) {
+ continue;
+ }
+ email = email.toLowerCase();
+ if (/^0x[0-9a-f]+$/i.test(email)) {
+ throw new Error(`Recipient should not be a key ID: ${email}`);
+ }
+ if (recipArray == "to") {
+ result.to[k] = "<" + email + ">";
+ } else {
+ result.bcc[k] = "<" + email + ">";
+ }
+
+ let aliasKeyList = lazy.EnigmailKeyRing.getAliasKeyList(email);
+ if (aliasKeyList) {
+ // We have an alias definition.
+
+ let aliasKeys = lazy.EnigmailKeyRing.getAliasKeys(aliasKeyList);
+ if (!aliasKeys.length) {
+ // An empty result means there was a failure obtaining the
+ // defined keys, this happens if at least one key is missing
+ // or unusable.
+ // We don't allow composing an email that involves a
+ // bad alias definition, return null to signal that
+ // sending should be aborted.
+ errorMsgObj.value = "bad alias definition for " + email;
+ return null;
+ }
+
+ result.aliasKeys.set(email, aliasKeys);
+ }
+ }
+ }
+ } else if (detachedSig) {
+ result.sigTypeDetached = true;
+ result.sign = true;
+
+ if (isAscii != ENC_TYPE_ATTACH_BINARY) {
+ result.armor = true;
+ }
+ } else if (signMsg) {
+ result.sigTypeClear = true;
+ result.sign = true;
+ }
+
+ return result;
+ },
+
+ /**
+ * Determine why a given key cannot be used for signing.
+ *
+ * @param {string} keyId - key ID
+ *
+ * @returns {string} The reason(s) as message to display to the user, or
+ * an empty string in case the key is valid.
+ */
+ determineInvSignReason(keyId) {
+ lazy.EnigmailLog.DEBUG(
+ "errorHandling.jsm: determineInvSignReason: keyId: " + keyId + "\n"
+ );
+
+ let key = lazy.EnigmailKeyRing.getKeyById(keyId);
+ if (!key) {
+ return lazy.l10n.formatValueSync("key-error-key-id-not-found", {
+ keySpec: keyId,
+ });
+ }
+ let r = key.getSigningValidity();
+ if (!r.keyValid) {
+ return r.reason;
+ }
+
+ return "";
+ },
+
+ /**
+ * Determine why a given key cannot be used for encryption.
+ *
+ * @param {string} keyId - key ID
+ *
+ * @returns {string} The reason(s) as message to display to the user, or
+ * an empty string in case the key is valid.
+ */
+ determineInvRcptReason(keyId) {
+ lazy.EnigmailLog.DEBUG(
+ "errorHandling.jsm: determineInvRcptReason: keyId: " + keyId + "\n"
+ );
+
+ let key = lazy.EnigmailKeyRing.getKeyById(keyId);
+ if (!key) {
+ return lazy.l10n.formatValueSync("key-error-key-id-not-found", {
+ keySpec: keyId,
+ });
+ }
+ let r = key.getEncryptionValidity(false);
+ if (!r.keyValid) {
+ return r.reason;
+ }
+
+ return "";
+ },
+
+ /**
+ * Determine if the sender key ID or user ID can be used for signing and/or
+ * encryption
+ *
+ * @param {integer} sendFlags - The send Flags; need to contain SEND_SIGNED and/or SEND_ENCRYPTED
+ * @param {string} fromKeyId - The sender key ID
+ *
+ * @returns {object} object
+ * - keyId: String - the found key ID, or null if fromMailAddr is not valid
+ * - errorMsg: String - the error message if key not valid, or null if key is valid
+ */
+ async determineOwnKeyUsability(sendFlags, fromKeyId, isExternalGnuPG) {
+ lazy.EnigmailLog.DEBUG(
+ "encryption.jsm: determineOwnKeyUsability: sendFlags=" +
+ sendFlags +
+ ", sender=" +
+ fromKeyId +
+ "\n"
+ );
+
+ let foundKey = null;
+ let ret = {
+ errorMsg: null,
+ };
+
+ if (!fromKeyId) {
+ return ret;
+ }
+
+ let sign = !!(sendFlags & lazy.EnigmailConstants.SEND_SIGNED);
+ let encrypt = !!(sendFlags & lazy.EnigmailConstants.SEND_ENCRYPTED);
+
+ if (/^(0x)?[0-9a-f]+$/i.test(fromKeyId)) {
+ // key ID specified
+ foundKey = lazy.EnigmailKeyRing.getKeyById(fromKeyId);
+ }
+
+ // even for isExternalGnuPG we require that the public key is available
+ if (!foundKey) {
+ ret.errorMsg = this.determineInvSignReason(fromKeyId);
+ return ret;
+ }
+
+ if (!isExternalGnuPG && foundKey.secretAvailable) {
+ let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(
+ foundKey.fpr
+ );
+ if (!isPersonal) {
+ ret.errorMsg = lazy.l10n.formatValueSync(
+ "key-error-not-accepted-as-personal",
+ {
+ keySpec: fromKeyId,
+ }
+ );
+ return ret;
+ }
+ }
+
+ let canSign = false;
+ let canEncrypt = false;
+
+ if (isExternalGnuPG) {
+ canSign = true;
+ } else if (sign && foundKey) {
+ let v = foundKey.getSigningValidity();
+ if (v.keyValid) {
+ canSign = true;
+ } else {
+ // If we already have a reason for the key not being valid,
+ // use that as error message.
+ ret.errorMsg = v.reason;
+ }
+ }
+
+ if (encrypt && foundKey) {
+ let v;
+ if (lazy.EnigmailKeyRing.isSubkeyId(fromKeyId)) {
+ // If the configured own key ID points to a subkey, check
+ // specifically that this subkey is a valid encryption key.
+
+ let id = fromKeyId.replace(/^0x/, "");
+ v = foundKey.getEncryptionValidity(false, null, id);
+ } else {
+ // Use parameter "false", because for isExternalGnuPG we cannot
+ // confirm that the user has the secret key.
+ // And for users of internal encryption code, we don't need to
+ // check that here either, public key is sufficient for encryption.
+ v = foundKey.getEncryptionValidity(false);
+ }
+
+ if (v.keyValid) {
+ canEncrypt = true;
+ } else {
+ // If we already have a reason for the key not being valid,
+ // use that as error message.
+ ret.errorMsg = v.reason;
+ }
+ }
+
+ if (sign && !canSign) {
+ if (!ret.errorMsg) {
+ // Only if we don't have an error message yet.
+ ret.errorMsg = this.determineInvSignReason(fromKeyId);
+ }
+ } else if (encrypt && !canEncrypt) {
+ if (!ret.errorMsg) {
+ // Only if we don't have an error message yet.
+ ret.errorMsg = this.determineInvRcptReason(fromKeyId);
+ }
+ }
+
+ return ret;
+ },
+
+ // return 0 on success, non-zero on failure
+ encryptMessageStart(
+ win,
+ uiFlags,
+ fromMailAddr,
+ toMailAddr,
+ bccMailAddr,
+ hashAlgorithm,
+ sendFlags,
+ listener,
+ statusFlagsObj,
+ errorMsgObj
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "encryption.jsm: encryptMessageStart: uiFlags=" +
+ uiFlags +
+ ", from " +
+ fromMailAddr +
+ " to " +
+ toMailAddr +
+ ", hashAlgorithm=" +
+ hashAlgorithm +
+ " (" +
+ lazy.EnigmailData.bytesToHex(lazy.EnigmailData.pack(sendFlags, 4)) +
+ ")\n"
+ );
+
+ // This code used to call determineOwnKeyUsability, and return on
+ // failure. But now determineOwnKeyUsability is an async function,
+ // and calling it from here with await results in a deadlock.
+ // Instead we perform this check in Enigmail.msg.prepareSendMsg.
+
+ var hashAlgo =
+ gMimeHashAlgorithms[
+ Services.prefs.getIntPref("temp.openpgp.mimeHashAlgorithm")
+ ];
+
+ if (hashAlgorithm) {
+ hashAlgo = hashAlgorithm;
+ }
+
+ errorMsgObj.value = "";
+
+ if (!sendFlags) {
+ lazy.EnigmailLog.DEBUG(
+ "encryption.jsm: encryptMessageStart: NO ENCRYPTION!\n"
+ );
+ errorMsgObj.value = lazy.l10n.formatValueSync("not-required");
+ return 0;
+ }
+
+ if (!lazy.EnigmailCore.getService(win)) {
+ throw new Error(
+ "encryption.jsm: encryptMessageStart: not yet initialized"
+ );
+ }
+
+ let logFileObj = {};
+
+ let encryptArgs = EnigmailEncryption.getCryptParams(
+ fromMailAddr,
+ toMailAddr,
+ bccMailAddr,
+ hashAlgo,
+ sendFlags,
+ ENC_TYPE_MSG,
+ errorMsgObj,
+ logFileObj
+ );
+
+ if (!encryptArgs) {
+ return -1;
+ }
+
+ if (!listener) {
+ throw new Error("unexpected no listener");
+ }
+
+ let resultStatus = {};
+ const cApi = lazy.EnigmailCryptoAPI();
+ let encrypted = cApi.sync(
+ cApi.encryptAndOrSign(
+ listener.getInputForCrypto(),
+ encryptArgs,
+ resultStatus
+ )
+ );
+
+ if (resultStatus.exitCode) {
+ if (resultStatus.errorMsg.length) {
+ lazy.EnigmailDialog.alert(win, resultStatus.errorMsg);
+ }
+ } else if (encrypted) {
+ listener.addCryptoOutput(encrypted);
+ }
+
+ if (resultStatus.exitCode === 0 && !listener.getCryptoOutputLength()) {
+ resultStatus.exitCode = -1;
+ }
+ return resultStatus.exitCode;
+ },
+
+ encryptMessage(
+ parent,
+ uiFlags,
+ plainText,
+ fromMailAddr,
+ toMailAddr,
+ bccMailAddr,
+ sendFlags,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "enigmail.js: Enigmail.encryptMessage: " +
+ plainText.length +
+ " bytes from " +
+ fromMailAddr +
+ " to " +
+ toMailAddr +
+ " (" +
+ sendFlags +
+ ")\n"
+ );
+ throw new Error("Not implemented");
+
+ /*
+ exitCodeObj.value = -1;
+ statusFlagsObj.value = 0;
+ errorMsgObj.value = "";
+
+ if (!plainText) {
+ EnigmailLog.DEBUG("enigmail.js: Enigmail.encryptMessage: NO ENCRYPTION!\n");
+ exitCodeObj.value = 0;
+ EnigmailLog.DEBUG(" <=== encryptMessage()\n");
+ return plainText;
+ }
+
+ var defaultSend = sendFlags & EnigmailConstants.SEND_DEFAULT;
+ var signMsg = sendFlags & EnigmailConstants.SEND_SIGNED;
+ var encryptMsg = sendFlags & EnigmailConstants.SEND_ENCRYPTED;
+
+ if (encryptMsg) {
+ // First convert all linebreaks to newlines
+ plainText = plainText.replace(/\r\n/g, "\n");
+ plainText = plainText.replace(/\r/g, "\n");
+
+ // we need all data in CRLF according to RFC 4880
+ plainText = plainText.replace(/\n/g, "\r\n");
+ }
+
+ var listener = EnigmailExecution.newSimpleListener(
+ function _stdin(pipe) {
+ pipe.write(plainText);
+ pipe.close();
+ },
+ function _done(exitCode) {});
+
+
+ var proc = EnigmailEncryption.encryptMessageStart(parent, uiFlags,
+ fromMailAddr, toMailAddr, bccMailAddr,
+ null, sendFlags,
+ listener, statusFlagsObj, errorMsgObj);
+ if (!proc) {
+ exitCodeObj.value = -1;
+ EnigmailLog.DEBUG(" <=== encryptMessage()\n");
+ return "";
+ }
+
+ // Wait for child pipes to close
+ proc.wait();
+
+ var retStatusObj = {};
+ exitCodeObj.value = EnigmailEncryption.encryptMessageEnd(fromMailAddr, EnigmailData.getUnicodeData(listener.stderrData), listener.exitCode,
+ uiFlags, sendFlags,
+ listener.stdoutData.length,
+ retStatusObj);
+
+ statusFlagsObj.value = retStatusObj.statusFlags;
+ statusFlagsObj.statusMsg = retStatusObj.statusMsg;
+ errorMsgObj.value = retStatusObj.errorMsg;
+
+
+ if ((exitCodeObj.value === 0) && listener.stdoutData.length === 0)
+ exitCodeObj.value = -1;
+
+ if (exitCodeObj.value === 0) {
+ // Normal return
+ EnigmailLog.DEBUG(" <=== encryptMessage()\n");
+ return EnigmailData.getUnicodeData(listener.stdoutData);
+ }
+
+ // Error processing
+ EnigmailLog.DEBUG("enigmail.js: Enigmail.encryptMessage: command execution exit code: " + exitCodeObj.value + "\n");
+ return "";
+ */
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/filters.jsm b/comm/mail/extensions/openpgp/content/modules/filters.jsm
new file mode 100644
index 0000000000..09abd448e5
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/filters.jsm
@@ -0,0 +1,598 @@
+/*
+ * 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 = ["EnigmailFilters"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+
+ EnigmailPersistentCrypto:
+ "chrome://openpgp/content/modules/persistentCrypto.jsm",
+
+ EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ jsmime: "resource:///modules/jsmime.jsm",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+let l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+var gNewMailListenerInitiated = false;
+
+/**
+ * filter action for creating a decrypted version of the mail and
+ * deleting the original mail at the same time
+ */
+
+const filterActionMoveDecrypt = {
+ async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: filterActionMoveDecrypt: Move to: " + aActionValue + "\n"
+ );
+
+ for (let msgHdr of aMsgHdrs) {
+ await lazy.EnigmailPersistentCrypto.cryptMessage(
+ msgHdr,
+ aActionValue,
+ true,
+ null
+ );
+ }
+ },
+
+ isValidForType(type, scope) {
+ return true;
+ },
+
+ validateActionValue(value, folder, type) {
+ l10n.formatValue("filter-decrypt-move-warn-experimental").then(value => {
+ lazy.EnigmailDialog.alert(null, value);
+ });
+
+ if (value === "") {
+ return l10n.formatValueSync("filter-folder-required");
+ }
+
+ return null;
+ },
+};
+
+/**
+ * filter action for creating a decrypted copy of the mail, leaving the original
+ * message untouched
+ */
+const filterActionCopyDecrypt = {
+ async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: filterActionCopyDecrypt: Copy to: " + aActionValue + "\n"
+ );
+
+ for (let msgHdr of aMsgHdrs) {
+ await lazy.EnigmailPersistentCrypto.cryptMessage(
+ msgHdr,
+ aActionValue,
+ false,
+ null
+ );
+ }
+ },
+
+ isValidForType(type, scope) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: filterActionCopyDecrypt.isValidForType(" + type + ")\n"
+ );
+
+ let r = true;
+ return r;
+ },
+
+ validateActionValue(value, folder, type) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: filterActionCopyDecrypt.validateActionValue(" +
+ value +
+ ")\n"
+ );
+
+ if (value === "") {
+ return l10n.formatValueSync("filter-folder-required");
+ }
+
+ return null;
+ },
+};
+
+/**
+ * filter action for to encrypt a mail to a specific key
+ */
+const filterActionEncrypt = {
+ async applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ // Ensure KeyRing is loaded.
+ if (aMsgWindow) {
+ lazy.EnigmailCore.getService(aMsgWindow.domWindow);
+ } else {
+ lazy.EnigmailCore.getService();
+ }
+ lazy.EnigmailKeyRing.getAllKeys();
+
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: filterActionEncrypt: Encrypt to: " + aActionValue + "\n"
+ );
+ let keyObj = lazy.EnigmailKeyRing.getKeyById(aActionValue);
+
+ if (keyObj === null) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: failed to find key by id: " + aActionValue + "\n"
+ );
+ let keyId = lazy.EnigmailKeyRing.getValidKeyForRecipient(aActionValue);
+ if (keyId) {
+ keyObj = lazy.EnigmailKeyRing.getKeyById(keyId);
+ }
+ }
+
+ if (keyObj === null && aListener) {
+ lazy.EnigmailLog.DEBUG("filters.jsm: no valid key - aborting\n");
+
+ aListener.OnStartCopy();
+ aListener.OnStopCopy(1);
+
+ return;
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: key to encrypt to: " +
+ JSON.stringify(keyObj) +
+ ", userId: " +
+ keyObj.userId +
+ "\n"
+ );
+
+ // Maybe skip messages here if they are already encrypted to
+ // the target key? There might be some use case for unconditionally
+ // encrypting here. E.g. to use the local preferences and remove all
+ // other recipients.
+ // Also not encrypting to already encrypted messages would make the
+ // behavior less transparent as it's not obvious.
+
+ for (let msgHdr of aMsgHdrs) {
+ await lazy.EnigmailPersistentCrypto.cryptMessage(
+ msgHdr,
+ null /* same folder */,
+ true /* move */,
+ keyObj /* target key */
+ );
+ }
+ },
+
+ isValidForType(type, scope) {
+ return true;
+ },
+
+ validateActionValue(value, folder, type) {
+ // Initialize KeyRing. Ugly as it blocks the GUI but
+ // we need it.
+ lazy.EnigmailCore.getService();
+ lazy.EnigmailKeyRing.getAllKeys();
+
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: validateActionValue: Encrypt to: " + value + "\n"
+ );
+ if (value === "") {
+ return l10n.formatValueSync("filter-key-required");
+ }
+
+ let keyObj = lazy.EnigmailKeyRing.getKeyById(value);
+
+ if (keyObj === null) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: failed to find key by id. Looking for uid.\n"
+ );
+ let keyId = lazy.EnigmailKeyRing.getValidKeyForRecipient(value);
+ if (keyId) {
+ keyObj = lazy.EnigmailKeyRing.getKeyById(keyId);
+ }
+ }
+
+ if (keyObj === null) {
+ return l10n.formatValueSync("filter-key-not-found", {
+ desc: value,
+ });
+ }
+
+ if (!keyObj.secretAvailable) {
+ // We warn but we allow it. There might be use cases where
+ // thunderbird + enigmail is used as a gateway filter with
+ // the secret not available on one machine and the decryption
+ // is intended to happen on different systems.
+ l10n
+ .formatValue("filter-warn-key-not-secret", {
+ desc: value,
+ })
+ .then(value => {
+ lazy.EnigmailDialog.alert(null, value);
+ });
+ }
+
+ return null;
+ },
+};
+
+function isPGPEncrypted(data) {
+ // We only check the first mime subpart for application/pgp-encrypted.
+ // If it is text/plain or text/html we look into that for the
+ // message marker.
+ // If there are no subparts we just look in the body.
+ //
+ // This intentionally does not match more complex cases
+ // with sub parts being encrypted etc. as auto processing
+ // these kinds of mails will be error prone and better not
+ // done through a filter
+
+ var mimeTree = lazy.EnigmailMime.getMimeTree(data, true);
+ if (!mimeTree.subParts.length) {
+ // No subParts. Check for PGP Marker in Body
+ return mimeTree.body.includes("-----BEGIN PGP MESSAGE-----");
+ }
+
+ // Check the type of the first subpart.
+ var firstPart = mimeTree.subParts[0];
+ var ct = firstPart.fullContentType;
+ if (typeof ct == "string") {
+ ct = ct.replace(/[\r\n]/g, " ");
+ // Proper PGP/MIME ?
+ if (ct.search(/application\/pgp-encrypted/i) >= 0) {
+ return true;
+ }
+ // Look into text/plain pgp messages and text/html messages.
+ if (ct.search(/text\/plain/i) >= 0 || ct.search(/text\/html/i) >= 0) {
+ return firstPart.body.includes("-----BEGIN PGP MESSAGE-----");
+ }
+ }
+ return false;
+}
+
+/**
+ * filter term for OpenPGP Encrypted mail
+ */
+const filterTermPGPEncrypted = {
+ id: EnigmailConstants.FILTER_TERM_PGP_ENCRYPTED,
+ name: l10n.formatValueSync("filter-term-pgpencrypted-label"),
+ needsBody: true,
+ match(aMsgHdr, searchValue, searchOp) {
+ var folder = aMsgHdr.folder;
+ var stream = folder.getMsgInputStream(aMsgHdr, {});
+
+ var messageSize = folder.hasMsgOffline(aMsgHdr.messageKey)
+ ? aMsgHdr.offlineMessageSize
+ : aMsgHdr.messageSize;
+ var data;
+ try {
+ data = lazy.NetUtil.readInputStreamToString(stream, messageSize);
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: filterTermPGPEncrypted: failed to get data.\n"
+ );
+ // If we don't know better to return false.
+ stream.close();
+ return false;
+ }
+
+ var isPGP = isPGPEncrypted(data);
+
+ stream.close();
+
+ return (
+ (searchOp == Ci.nsMsgSearchOp.Is && isPGP) ||
+ (searchOp == Ci.nsMsgSearchOp.Isnt && !isPGP)
+ );
+ },
+
+ getEnabled(scope, op) {
+ return true;
+ },
+
+ getAvailable(scope, op) {
+ return true;
+ },
+
+ getAvailableOperators(scope, length) {
+ length.value = 2;
+ return [Ci.nsMsgSearchOp.Is, Ci.nsMsgSearchOp.Isnt];
+ },
+};
+
+function initNewMailListener() {
+ lazy.EnigmailLog.DEBUG("filters.jsm: initNewMailListener()\n");
+
+ if (!gNewMailListenerInitiated) {
+ let notificationService = Cc[
+ "@mozilla.org/messenger/msgnotificationservice;1"
+ ].getService(Ci.nsIMsgFolderNotificationService);
+ notificationService.addListener(
+ newMailListener,
+ notificationService.msgAdded
+ );
+ }
+ gNewMailListenerInitiated = true;
+}
+
+function shutdownNewMailListener() {
+ lazy.EnigmailLog.DEBUG("filters.jsm: shutdownNewMailListener()\n");
+
+ if (gNewMailListenerInitiated) {
+ let notificationService = Cc[
+ "@mozilla.org/messenger/msgnotificationservice;1"
+ ].getService(Ci.nsIMsgFolderNotificationService);
+ notificationService.removeListener(newMailListener);
+ gNewMailListenerInitiated = false;
+ }
+}
+
+function getIdentityForSender(senderEmail, msgServer) {
+ let identities = MailServices.accounts.getIdentitiesForServer(msgServer);
+ return identities.find(
+ id => id.email.toLowerCase() === senderEmail.toLowerCase()
+ );
+}
+
+var consumerList = [];
+
+function JsmimeEmitter(requireBody) {
+ this.requireBody = requireBody;
+ this.mimeTree = {
+ partNum: "",
+ headers: null,
+ body: "",
+ parent: null,
+ subParts: [],
+ };
+ this.stack = [];
+ this.currPartNum = "";
+}
+
+JsmimeEmitter.prototype = {
+ createPartObj(partNum, headers, parent) {
+ return {
+ partNum,
+ headers,
+ body: "",
+ parent,
+ subParts: [],
+ };
+ },
+
+ getMimeTree() {
+ return this.mimeTree.subParts[0];
+ },
+
+ /** JSMime API */
+ startMessage() {
+ this.currentPart = this.mimeTree;
+ },
+ endMessage() {},
+
+ startPart(partNum, headers) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: JsmimeEmitter.startPart: partNum=" + partNum + "\n"
+ );
+ //this.stack.push(partNum);
+ let newPart = this.createPartObj(partNum, headers, this.currentPart);
+
+ if (partNum.indexOf(this.currPartNum) === 0) {
+ // found sub-part
+ this.currentPart.subParts.push(newPart);
+ } else {
+ // found same or higher level
+ this.currentPart.subParts.push(newPart);
+ }
+ this.currPartNum = partNum;
+ this.currentPart = newPart;
+ },
+
+ endPart(partNum) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: JsmimeEmitter.startPart: partNum=" + partNum + "\n"
+ );
+ this.currentPart = this.currentPart.parent;
+ },
+
+ deliverPartData(partNum, data) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: JsmimeEmitter.deliverPartData: partNum=" + partNum + "\n"
+ );
+ if (this.requireBody) {
+ if (typeof data === "string") {
+ this.currentPart.body += data;
+ } else {
+ this.currentPart.body += lazy.EnigmailData.arrayBufferToString(data);
+ }
+ }
+ },
+};
+
+function processIncomingMail(url, requireBody, aMsgHdr) {
+ lazy.EnigmailLog.DEBUG("filters.jsm: processIncomingMail()\n");
+
+ let inputStream = lazy.EnigmailStreams.newStringStreamListener(msgData => {
+ let opt = {
+ strformat: "unicode",
+ bodyformat: "decode",
+ };
+
+ try {
+ let e = new JsmimeEmitter(requireBody);
+ let p = new lazy.jsmime.MimeParser(e, opt);
+ p.deliverData(msgData);
+
+ for (let c of consumerList) {
+ try {
+ c.consumeMessage(e.getMimeTree(), msgData, aMsgHdr);
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: processIncomingMail: exception: " +
+ ex.toString() +
+ "\n"
+ );
+ }
+ }
+ } catch (ex) {}
+ });
+
+ try {
+ let channel = lazy.EnigmailStreams.createChannel(url);
+ channel.asyncOpen(inputStream, null);
+ } catch (e) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: processIncomingMail: open stream exception " +
+ e.toString() +
+ "\n"
+ );
+ }
+}
+
+function getRequireMessageProcessing(aMsgHdr) {
+ let isInbox =
+ aMsgHdr.folder.getFlag(Ci.nsMsgFolderFlags.CheckNew) ||
+ aMsgHdr.folder.getFlag(Ci.nsMsgFolderFlags.Inbox);
+ let requireBody = false;
+ let inboxOnly = true;
+ let selfSentOnly = false;
+ let processReadMail = false;
+
+ for (let c of consumerList) {
+ if (!c.incomingMailOnly) {
+ inboxOnly = false;
+ }
+ if (!c.unreadOnly) {
+ processReadMail = true;
+ }
+ if (!c.headersOnly) {
+ requireBody = true;
+ }
+ if (c.selfSentOnly) {
+ selfSentOnly = true;
+ }
+ }
+
+ if (!processReadMail && aMsgHdr.isRead) {
+ return null;
+ }
+ if (inboxOnly && !isInbox) {
+ return null;
+ }
+ if (selfSentOnly) {
+ let sender = lazy.EnigmailFuncs.parseEmails(aMsgHdr.author, true);
+ let id = null;
+ if (sender && sender[0]) {
+ id = getIdentityForSender(sender[0].email, aMsgHdr.folder.server);
+ }
+
+ if (!id) {
+ return null;
+ }
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: getRequireMessageProcessing: author: " + aMsgHdr.author + "\n"
+ );
+
+ let u = lazy.EnigmailFuncs.getUrlFromUriSpec(
+ aMsgHdr.folder.getUriForMsg(aMsgHdr)
+ );
+
+ if (!u) {
+ return null;
+ }
+
+ let op = u.spec.indexOf("?") > 0 ? "&" : "?";
+ let url = u.spec + op + "header=enigmailFilter";
+
+ return {
+ url,
+ requireBody,
+ };
+}
+
+const newMailListener = {
+ msgAdded(aMsgHdr) {
+ lazy.EnigmailLog.DEBUG(
+ "filters.jsm: newMailListener.msgAdded() - got new mail in " +
+ aMsgHdr.folder.prettiestName +
+ "\n"
+ );
+
+ if (consumerList.length === 0) {
+ return;
+ }
+
+ let ret = getRequireMessageProcessing(aMsgHdr);
+ if (ret) {
+ processIncomingMail(ret.url, ret.requireBody, aMsgHdr);
+ }
+ },
+};
+
+/**
+ messageStructure - Object:
+ - partNum: String - MIME part number
+ - headers: Object(nsIStructuredHeaders) - MIME part headers
+ - body: String or typedarray - the body part
+ - parent: Object(messageStructure) - link to the parent part
+ - subParts: Array of Object(messageStructure) - array of the sub-parts
+ */
+
+var EnigmailFilters = {
+ onStartup() {
+ let filterService = Cc[
+ "@mozilla.org/messenger/services/filters;1"
+ ].getService(Ci.nsIMsgFilterService);
+ filterService.addCustomTerm(filterTermPGPEncrypted);
+ initNewMailListener();
+ },
+
+ onShutdown() {
+ shutdownNewMailListener();
+ },
+
+ /**
+ * add a new consumer to listen to new mails
+ *
+ * @param consumer - Object
+ * - headersOnly: Boolean - needs full message body? [FUTURE]
+ * - incomingMailOnly: Boolean - only work on folder(s) that obtain new mail
+ * (Inbox and folders that listen to new mail)
+ * - unreadOnly: Boolean - only process unread mails
+ * - selfSentOnly: Boolean - only process mails with sender Email == Account Email
+ * - consumeMessage: function(messageStructure, rawMessageData, nsIMsgHdr)
+ */
+ addNewMailConsumer(consumer) {
+ lazy.EnigmailLog.DEBUG("filters.jsm: addNewMailConsumer()\n");
+ consumerList.push(consumer);
+ },
+
+ removeNewMailConsumer(consumer) {},
+
+ moveDecrypt: filterActionMoveDecrypt,
+ copyDecrypt: filterActionCopyDecrypt,
+ encrypt: filterActionEncrypt,
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/filtersWrapper.jsm b/comm/mail/extensions/openpgp/content/modules/filtersWrapper.jsm
new file mode 100644
index 0000000000..a43fb29e87
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/filtersWrapper.jsm
@@ -0,0 +1,186 @@
+/*
+ * 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 = ["EnigmailFiltersWrapper"];
+
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+
+var gEnigmailFilters = null;
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+/**
+ * filter action for creating a decrypted version of the mail and
+ * deleting the original mail at the same time
+ */
+const filterActionMoveDecrypt = {
+ id: EnigmailConstants.FILTER_MOVE_DECRYPT,
+ name: l10n.formatValueSync("filter-decrypt-move-label"),
+ value: "movemessage",
+ applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ if (gEnigmailFilters) {
+ gEnigmailFilters.moveDecrypt.applyAction(
+ aMsgHdrs,
+ aActionValue,
+ aListener,
+ aType,
+ aMsgWindow
+ );
+ } else {
+ aListener.OnStartCopy();
+ aListener.OnStopCopy(0);
+ }
+ },
+
+ isValidForType(type, scope) {
+ return gEnigmailFilters
+ ? gEnigmailFilters.moveDecrypt.isValidForType(type, scope)
+ : false;
+ },
+
+ validateActionValue(value, folder, type) {
+ if (gEnigmailFilters) {
+ return gEnigmailFilters.moveDecrypt.validateActionValue(
+ value,
+ folder,
+ type
+ );
+ }
+ return null;
+ },
+
+ allowDuplicates: false,
+ isAsync: true,
+ needsBody: true,
+};
+
+/**
+ * filter action for creating a decrypted copy of the mail, leaving the original
+ * message untouched
+ */
+const filterActionCopyDecrypt = {
+ id: EnigmailConstants.FILTER_COPY_DECRYPT,
+ name: l10n.formatValueSync("filter-decrypt-copy-label"),
+ value: "copymessage",
+ applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ if (gEnigmailFilters) {
+ gEnigmailFilters.copyDecrypt.applyAction(
+ aMsgHdrs,
+ aActionValue,
+ aListener,
+ aType,
+ aMsgWindow
+ );
+ } else {
+ aListener.OnStartCopy();
+ aListener.OnStopCopy(0);
+ }
+ },
+
+ isValidForType(type, scope) {
+ return gEnigmailFilters
+ ? gEnigmailFilters.copyDecrypt.isValidForType(type, scope)
+ : false;
+ },
+
+ validateActionValue(value, folder, type) {
+ if (gEnigmailFilters) {
+ return gEnigmailFilters.copyDecrypt.validateActionValue(
+ value,
+ folder,
+ type
+ );
+ }
+ return null;
+ },
+
+ allowDuplicates: false,
+ isAsync: true,
+ needsBody: true,
+};
+
+/**
+ * filter action for to encrypt a mail to a specific key
+ */
+const filterActionEncrypt = {
+ id: EnigmailConstants.FILTER_ENCRYPT,
+ name: l10n.formatValueSync("filter-encrypt-label"),
+ value: "encryptto",
+ applyAction(aMsgHdrs, aActionValue, aListener, aType, aMsgWindow) {
+ if (gEnigmailFilters) {
+ gEnigmailFilters.encrypt.applyAction(
+ aMsgHdrs,
+ aActionValue,
+ aListener,
+ aType,
+ aMsgWindow
+ );
+ } else {
+ aListener.OnStartCopy();
+ aListener.OnStopCopy(0);
+ }
+ },
+
+ isValidForType(type, scope) {
+ return gEnigmailFilters ? gEnigmailFilters.encrypt.isValidForType() : false;
+ },
+
+ validateActionValue(value, folder, type) {
+ if (gEnigmailFilters) {
+ return gEnigmailFilters.encrypt.validateActionValue(value, folder, type);
+ }
+ return null;
+ },
+
+ allowDuplicates: false,
+ isAsync: true,
+ needsBody: true,
+};
+
+/**
+ * Add a custom filter action. If the filter already exists, do nothing
+ * (for example, if addon is disabled and re-enabled)
+ *
+ * @param filterObj - nsIMsgFilterCustomAction
+ */
+function addFilterIfNotExists(filterObj) {
+ let filterService = Cc[
+ "@mozilla.org/messenger/services/filters;1"
+ ].getService(Ci.nsIMsgFilterService);
+
+ let foundFilter = null;
+ try {
+ foundFilter = filterService.getCustomAction(filterObj.id);
+ } catch (ex) {}
+
+ if (!foundFilter) {
+ filterService.addCustomAction(filterObj);
+ }
+}
+
+var EnigmailFiltersWrapper = {
+ onStartup() {
+ let { EnigmailFilters } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/filters.jsm"
+ );
+ gEnigmailFilters = EnigmailFilters;
+
+ addFilterIfNotExists(filterActionMoveDecrypt);
+ addFilterIfNotExists(filterActionCopyDecrypt);
+ addFilterIfNotExists(filterActionEncrypt);
+
+ gEnigmailFilters.onStartup();
+ },
+
+ onShutdown() {
+ gEnigmailFilters.onShutdown();
+ gEnigmailFilters = null;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm b/comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm
new file mode 100644
index 0000000000..753041cf1c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm
@@ -0,0 +1,433 @@
+/*
+ * 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 = ["EnigmailFixExchangeMsg"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm",
+ EnigmailPersistentCrypto:
+ "chrome://openpgp/content/modules/persistentCrypto.jsm",
+});
+
+var EnigmailFixExchangeMsg = {
+ /*
+ * Fix a broken message from MS-Exchange and replace it with the original message
+ *
+ * @param {nsIMsgDBHdr} hdr - Header of the message to fix (= pointer to message)
+ * @param {string} brokenByApp - Type of app that created the message. Currently one of
+ * exchange, iPGMail
+ * @param {string} [destFolderUri] optional destination Folder URI
+ *
+ * @return {nsMsgKey} upon success, the promise returns the messageKey
+ */
+ async fixExchangeMessage(hdr, brokenByApp, destFolderUri = null) {
+ let msgUriSpec = hdr.folder.getUriForMsg(hdr);
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: fixExchangeMessage: msgUriSpec: " + msgUriSpec + "\n"
+ );
+
+ this.hdr = hdr;
+ this.brokenByApp = brokenByApp;
+ this.destFolderUri = destFolderUri;
+
+ this.msgSvc = MailServices.messageServiceFromURI(msgUriSpec);
+
+ let fixedMsgData = await this.getMessageBody();
+
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: fixExchangeMessage: got fixedMsgData\n"
+ );
+ this.ensureExpectedStructure(fixedMsgData);
+ return lazy.EnigmailPersistentCrypto.copyMessageToFolder(
+ this.hdr,
+ this.destFolderUri,
+ true,
+ fixedMsgData,
+ null
+ );
+ },
+
+ getMessageBody() {
+ lazy.EnigmailLog.DEBUG("fixExchangeMsg.jsm: getMessageBody:\n");
+
+ var self = this;
+
+ return new Promise(function (resolve, reject) {
+ let url = lazy.EnigmailFuncs.getUrlFromUriSpec(
+ self.hdr.folder.getUriForMsg(self.hdr)
+ );
+
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getting data from URL " + url + "\n"
+ );
+
+ let s = lazy.EnigmailStreams.newStringStreamListener(function (data) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: analyzeDecryptedData: got " +
+ data.length +
+ " bytes\n"
+ );
+
+ if (lazy.EnigmailLog.getLogLevel() > 5) {
+ lazy.EnigmailLog.DEBUG(
+ "*** start data ***\n'" + data + "'\n***end data***\n"
+ );
+ }
+
+ let [good, errorCode, msg] = self.getRepairedMessage(data);
+
+ if (!good) {
+ reject(errorCode);
+ } else {
+ resolve(msg);
+ }
+ });
+
+ try {
+ let channel = lazy.EnigmailStreams.createChannel(url);
+ channel.asyncOpen(s, null);
+ } catch (e) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getMessageBody: exception " + e + "\n"
+ );
+ }
+ });
+ },
+
+ getRepairedMessage(data) {
+ this.determineCreatorApp(data);
+
+ let hdrEnd = data.search(/\r?\n\r?\n/);
+
+ if (hdrEnd <= 0) {
+ // cannot find end of header data
+ return [false, 0, ""];
+ }
+
+ let hdrLines = data.substr(0, hdrEnd).split(/\r?\n/);
+ let hdrObj = this.getFixedHeaderData(hdrLines);
+
+ if (hdrObj.headers.length === 0 || hdrObj.boundary.length === 0) {
+ return [false, 1, ""];
+ }
+
+ let boundary = hdrObj.boundary;
+ let body;
+
+ switch (this.brokenByApp) {
+ case "exchange":
+ body = this.getCorrectedExchangeBodyData(
+ data.substr(hdrEnd + 2),
+ boundary
+ );
+ break;
+ case "iPGMail":
+ body = this.getCorrectediPGMailBodyData(
+ data.substr(hdrEnd + 2),
+ boundary
+ );
+ break;
+ default:
+ lazy.EnigmailLog.ERROR(
+ "fixExchangeMsg.jsm: getRepairedMessage: unknown appType " +
+ this.brokenByApp +
+ "\n"
+ );
+ return [false, 99, ""];
+ }
+
+ if (body) {
+ return [true, 0, hdrObj.headers + "\r\n" + body];
+ }
+ return [false, 22, ""];
+ },
+
+ determineCreatorApp(msgData) {
+ // perform extra testing if iPGMail is assumed
+ if (this.brokenByApp === "exchange") {
+ return;
+ }
+
+ let msgTree = lazy.EnigmailMime.getMimeTree(msgData, false);
+
+ try {
+ let isIPGMail =
+ msgTree.subParts.length === 3 &&
+ (msgTree.subParts[0].headers.get("content-type").type.toLowerCase() ===
+ "text/plain" ||
+ msgTree.subParts[0].headers.get("content-type").type.toLowerCase() ===
+ "multipart/alternative") &&
+ msgTree.subParts[1].headers.get("content-type").type.toLowerCase() ===
+ "application/pgp-encrypted" &&
+ msgTree.subParts[2].headers.get("content-type").type.toLowerCase() ===
+ "text/plain";
+
+ if (!isIPGMail) {
+ this.brokenByApp = "exchange";
+ }
+ } catch (x) {}
+ },
+
+ /**
+ * repair header data, such that they are working for PGP/MIME
+ *
+ * @return: object: {
+ * headers: String - all headers ready for appending to message
+ * boundary: String - MIME part boundary (incl. surrounding "" or '')
+ * }
+ */
+ getFixedHeaderData(hdrLines) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getFixedHeaderData: hdrLines[]:'" +
+ hdrLines.length +
+ "'\n"
+ );
+ let r = {
+ headers: "",
+ boundary: "",
+ };
+
+ for (let i = 0; i < hdrLines.length; i++) {
+ if (hdrLines[i].search(/^content-type:/i) >= 0) {
+ // Join the rest of the content type lines together.
+ // See RFC 2425, section 5.8.1
+ let contentTypeLine = hdrLines[i];
+ i++;
+ while (i < hdrLines.length) {
+ let endOfCTL = false;
+ // Does the line start with a space or a tab, followed by something else?
+ if (hdrLines[i].search(/^[ \t]+?/) === 0) {
+ contentTypeLine += hdrLines[i];
+ i++;
+ if (i == hdrLines.length) {
+ endOfCTL = true;
+ }
+ } else {
+ endOfCTL = true;
+ }
+ if (endOfCTL) {
+ // we got the complete content-type header
+ contentTypeLine = contentTypeLine.replace(/[\r\n]/g, "");
+ let h = lazy.EnigmailFuncs.getHeaderData(contentTypeLine);
+ r.boundary = h.boundary || "";
+ break;
+ }
+ }
+ } else {
+ r.headers += hdrLines[i] + "\r\n";
+ }
+ }
+
+ r.boundary = r.boundary.replace(/^(['"])(.*)(\1)$/, "$2");
+
+ r.headers +=
+ "Content-Type: multipart/encrypted;\r\n" +
+ ' protocol="application/pgp-encrypted";\r\n' +
+ ' boundary="' +
+ r.boundary +
+ '"\r\n' +
+ "X-Enigmail-Info: Fixed broken PGP/MIME message\r\n";
+
+ return r;
+ },
+
+ /**
+ * Get corrected body for MS-Exchange messages
+ */
+ getCorrectedExchangeBodyData(bodyData, boundary) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: boundary='" +
+ boundary +
+ "'\n"
+ );
+ // Escape regex chars in the boundary.
+ boundary = boundary.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ let boundRx = new RegExp("^--" + boundary, "gm");
+ let match = boundRx.exec(bodyData);
+
+ if (match.index < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: did not find index of mime type to skip\n"
+ );
+ return null;
+ }
+
+ let skipStart = match.index;
+ // found first instance -- that's the message part to ignore
+ match = boundRx.exec(bodyData);
+ if (match.index <= 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: did not find boundary of PGP/MIME version identification\n"
+ );
+ return null;
+ }
+
+ let versionIdent = match.index;
+
+ if (
+ bodyData
+ .substring(skipStart, versionIdent)
+ .search(
+ /^content-type:[ \t]*(text\/(plain|html)|multipart\/alternative)/im
+ ) < 0
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: first MIME part is not content-type text/plain or text/html\n"
+ );
+ return null;
+ }
+
+ match = boundRx.exec(bodyData);
+ if (match.index < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: did not find boundary of PGP/MIME encrypted data\n"
+ );
+ return null;
+ }
+
+ let encData = match.index;
+ let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ mimeHdr.initialize(bodyData.substring(versionIdent, encData));
+ let ct = mimeHdr.extractHeader("content-type", false);
+
+ if (!ct || ct.search(/application\/pgp-encrypted/i) < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: wrong content-type of version-identification\n"
+ );
+ lazy.EnigmailLog.DEBUG(" ct = '" + ct + "'\n");
+ return null;
+ }
+
+ mimeHdr.initialize(bodyData.substr(encData, 5000));
+ ct = mimeHdr.extractHeader("content-type", false);
+ if (!ct || ct.search(/application\/octet-stream/i) < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectedExchangeBodyData: wrong content-type of PGP/MIME data\n"
+ );
+ lazy.EnigmailLog.DEBUG(" ct = '" + ct + "'\n");
+ return null;
+ }
+
+ return bodyData.substr(versionIdent);
+ },
+
+ /**
+ * Get corrected body for iPGMail messages
+ */
+ getCorrectediPGMailBodyData(bodyData, boundary) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: boundary='" +
+ boundary +
+ "'\n"
+ );
+ // Escape regex chars.
+ boundary = boundary.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ let boundRx = new RegExp("^--" + boundary, "gm");
+ let match = boundRx.exec(bodyData);
+
+ if (match.index < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: did not find index of mime type to skip\n"
+ );
+ return null;
+ }
+
+ // found first instance -- that's the message part to ignore
+ match = boundRx.exec(bodyData);
+ if (match.index <= 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: did not find boundary of text/plain msg part\n"
+ );
+ return null;
+ }
+
+ let encData = match.index;
+
+ match = boundRx.exec(bodyData);
+ if (match.index < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: did not find end boundary of PGP/MIME encrypted data\n"
+ );
+ return null;
+ }
+
+ let mimeHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+
+ mimeHdr.initialize(bodyData.substr(encData, 5000));
+ let ct = mimeHdr.extractHeader("content-type", false);
+ if (!ct || ct.search(/application\/pgp-encrypted/i) < 0) {
+ lazy.EnigmailLog.DEBUG(
+ "fixExchangeMsg.jsm: getCorrectediPGMailBodyData: wrong content-type of PGP/MIME data\n"
+ );
+ lazy.EnigmailLog.DEBUG(" ct = '" + ct + "'\n");
+ return null;
+ }
+
+ return (
+ "--" +
+ boundary +
+ "\r\n" +
+ "Content-Type: application/pgp-encrypted\r\n" +
+ "Content-Description: PGP/MIME version identification\r\n\r\n" +
+ "Version: 1\r\n\r\n" +
+ bodyData
+ .substring(encData, match.index)
+ .replace(
+ /^Content-Type: +application\/pgp-encrypted/im,
+ "Content-Type: application/octet-stream"
+ ) +
+ "--" +
+ boundary +
+ "--\r\n"
+ );
+ },
+
+ ensureExpectedStructure(msgData) {
+ let msgTree = lazy.EnigmailMime.getMimeTree(msgData, true);
+
+ // check message structure
+ let ok =
+ msgTree.headers.get("content-type").type.toLowerCase() ===
+ "multipart/encrypted" &&
+ msgTree.headers.get("content-type").get("protocol").toLowerCase() ===
+ "application/pgp-encrypted" &&
+ msgTree.subParts.length === 2 &&
+ msgTree.subParts[0].headers.get("content-type").type.toLowerCase() ===
+ "application/pgp-encrypted" &&
+ msgTree.subParts[1].headers.get("content-type").type.toLowerCase() ===
+ "application/octet-stream";
+
+ if (ok) {
+ // check for existence of PGP Armor
+ let body = msgTree.subParts[1].body;
+ let p0 = body.search(/^-----BEGIN PGP MESSAGE-----$/m);
+ let p1 = body.search(/^-----END PGP MESSAGE-----$/m);
+
+ ok = p0 >= 0 && p1 > p0 + 32;
+ }
+ if (!ok) {
+ throw new Error("unexpected MIME structure");
+ }
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/funcs.jsm b/comm/mail/extensions/openpgp/content/modules/funcs.jsm
new file mode 100644
index 0000000000..469b71e71c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/funcs.jsm
@@ -0,0 +1,561 @@
+/*
+ * 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/.
+ */
+
+/*
+ * Common Enigmail crypto-related GUI functionality
+ */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["EnigmailFuncs"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+var gTxtConverter = null;
+
+var EnigmailFuncs = {
+ /**
+ * get a list of plain email addresses without name or surrounding <>
+ *
+ * @param mailAddrs |string| - address-list encdoded in Unicode as specified in RFC 2822, 3.4
+ * separated by , or ;
+ *
+ * @returns |string| - list of pure email addresses separated by ","
+ */
+ stripEmail(mailAddresses) {
+ // EnigmailLog.DEBUG("funcs.jsm: stripEmail(): mailAddresses=" + mailAddresses + "\n");
+
+ const SIMPLE = "[^<>,]+"; // RegExp for a simple email address (e.g. a@b.c)
+ const COMPLEX = "[^<>,]*<[^<>, ]+>"; // RegExp for an address containing <...> (e.g. Name <a@b.c>)
+ const MatchAddr = new RegExp(
+ "^(" + SIMPLE + "|" + COMPLEX + ")(," + SIMPLE + "|," + COMPLEX + ")*$"
+ );
+
+ let mailAddrs = mailAddresses;
+
+ let qStart, qEnd;
+ while ((qStart = mailAddrs.indexOf('"')) >= 0) {
+ qEnd = mailAddrs.indexOf('"', qStart + 1);
+ if (qEnd < 0) {
+ lazy.EnigmailLog.ERROR(
+ "funcs.jsm: stripEmail: Unmatched quote in mail address: '" +
+ mailAddresses +
+ "'\n"
+ );
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ mailAddrs =
+ mailAddrs.substring(0, qStart) + mailAddrs.substring(qEnd + 1);
+ }
+
+ // replace any ";" by ","; remove leading/trailing ","
+ mailAddrs = mailAddrs
+ .replace(/[,;]+/g, ",")
+ .replace(/^,/, "")
+ .replace(/,$/, "");
+
+ if (mailAddrs.length === 0) {
+ return "";
+ }
+
+ // having two <..> <..> in one email, or things like <a@b.c,><d@e.f> is an error
+ if (mailAddrs.search(MatchAddr) < 0) {
+ lazy.EnigmailLog.ERROR(
+ "funcs.jsm: stripEmail: Invalid <..> brackets in mail address: '" +
+ mailAddresses +
+ "'\n"
+ );
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ // We know that the "," and the < > are at the right places, thus we can split by ","
+ let addrList = mailAddrs.split(/,/);
+
+ for (let i in addrList) {
+ // Extract pure e-mail address list (strip out anything before angle brackets and any whitespace)
+ addrList[i] = addrList[i]
+ .replace(/^([^<>]*<)([^<>]+)(>)$/, "$2")
+ .replace(/\s/g, "");
+ }
+
+ // remove repeated, trailing and leading "," (again, as there may be empty addresses)
+ mailAddrs = addrList
+ .join(",")
+ .replace(/,,/g, ",")
+ .replace(/^,/, "")
+ .replace(/,$/, "");
+
+ return mailAddrs;
+ },
+
+ /**
+ * get an array of email object (email, name) from an address string
+ *
+ * @param mailAddrs |string| - address-list as specified in RFC 2822, 3.4
+ * separated by ","; encoded according to RFC 2047
+ *
+ * @returns |array| of msgIAddressObject
+ */
+ parseEmails(mailAddrs, encoded = true) {
+ try {
+ let hdr = Cc["@mozilla.org/messenger/headerparser;1"].createInstance(
+ Ci.nsIMsgHeaderParser
+ );
+ if (encoded) {
+ return hdr.parseEncodedHeader(mailAddrs, "utf-8");
+ }
+ return hdr.parseDecodedHeader(mailAddrs);
+ } catch (ex) {}
+
+ return [];
+ },
+
+ /**
+ * Hide all menu entries and other XUL elements that are considered for
+ * advanced users. The XUL items must contain 'advanced="true"' or
+ * 'advanced="reverse"'.
+ *
+ * @obj: |object| - XUL tree element
+ * @attribute: |string| - attribute to set or remove (i.e. "hidden" or "collapsed")
+ * @dummy: |object| - anything
+ *
+ * no return value
+ */
+
+ collapseAdvanced(obj, attribute, dummy) {
+ lazy.EnigmailLog.DEBUG("funcs.jsm: collapseAdvanced:\n");
+
+ var advancedUser = Services.prefs.getBoolPref("temp.openpgp.advancedUser");
+
+ obj = obj.firstChild;
+ while (obj) {
+ if ("getAttribute" in obj) {
+ if (obj.getAttribute("advanced") == "true") {
+ if (advancedUser) {
+ obj.removeAttribute(attribute);
+ } else {
+ obj.setAttribute(attribute, "true");
+ }
+ } else if (obj.getAttribute("advanced") == "reverse") {
+ if (advancedUser) {
+ obj.setAttribute(attribute, "true");
+ } else {
+ obj.removeAttribute(attribute);
+ }
+ }
+ }
+
+ obj = obj.nextSibling;
+ }
+ },
+
+ /**
+ * this function tries to mimic the Thunderbird plaintext viewer
+ *
+ * @plainTxt - |string| containing the plain text data
+ *
+ * @ return HTML markup to display mssage
+ */
+
+ formatPlaintextMsg(plainTxt) {
+ if (!gTxtConverter) {
+ gTxtConverter = Cc["@mozilla.org/txttohtmlconv;1"].createInstance(
+ Ci.mozITXTToHTMLConv
+ );
+ }
+
+ var fontStyle = "";
+
+ // set the style stuff according to preferences
+
+ switch (Services.prefs.getIntPref("mail.quoted_style")) {
+ case 1:
+ fontStyle = "font-weight: bold; ";
+ break;
+ case 2:
+ fontStyle = "font-style: italic; ";
+ break;
+ case 3:
+ fontStyle = "font-weight: bold; font-style: italic; ";
+ break;
+ }
+
+ switch (Services.prefs.getIntPref("mail.quoted_size")) {
+ case 1:
+ fontStyle += "font-size: large; ";
+ break;
+ case 2:
+ fontStyle += "font-size: small; ";
+ break;
+ }
+
+ fontStyle +=
+ "color: " + Services.prefs.getCharPref("mail.citation_color") + ";";
+
+ var convFlags = Ci.mozITXTToHTMLConv.kURLs;
+ if (Services.prefs.getBoolPref("mail.display_glyph")) {
+ convFlags |= Ci.mozITXTToHTMLConv.kGlyphSubstitution;
+ }
+ if (Services.prefs.getBoolPref("mail.display_struct")) {
+ convFlags |= Ci.mozITXTToHTMLConv.kStructPhrase;
+ }
+
+ // start processing the message
+
+ plainTxt = plainTxt.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
+ var lines = plainTxt.split(/\n/);
+ var oldCiteLevel = 0;
+ var citeLevel = 0;
+ var preface = "";
+ var logLineStart = {
+ value: 0,
+ };
+ var isSignature = false;
+
+ for (var i = 0; i < lines.length; i++) {
+ preface = "";
+ oldCiteLevel = citeLevel;
+ if (lines[i].search(/^[> \t]*>$/) === 0) {
+ lines[i] += " ";
+ }
+
+ citeLevel = gTxtConverter.citeLevelTXT(lines[i], logLineStart);
+
+ if (citeLevel > oldCiteLevel) {
+ preface = "</pre>";
+ for (let j = 0; j < citeLevel - oldCiteLevel; j++) {
+ preface += '<blockquote type="cite" style="' + fontStyle + '">';
+ }
+ preface += '<pre wrap="">\n';
+ } else if (citeLevel < oldCiteLevel) {
+ preface = "</pre>";
+ for (let j = 0; j < oldCiteLevel - citeLevel; j++) {
+ preface += "</blockquote>";
+ }
+
+ preface += '<pre wrap="">\n';
+ }
+
+ if (logLineStart.value > 0) {
+ preface +=
+ '<span class="moz-txt-citetags">' +
+ gTxtConverter.scanTXT(
+ lines[i].substr(0, logLineStart.value),
+ convFlags
+ ) +
+ "</span>";
+ } else if (lines[i] == "-- ") {
+ preface += '<div class="moz-txt-sig">';
+ isSignature = true;
+ }
+ lines[i] =
+ preface +
+ gTxtConverter.scanTXT(lines[i].substr(logLineStart.value), convFlags);
+ }
+
+ var r =
+ '<pre wrap="">' +
+ lines.join("\n") +
+ (isSignature ? "</div>" : "") +
+ "</pre>";
+ //EnigmailLog.DEBUG("funcs.jsm: r='"+r+"'\n");
+ return r;
+ },
+
+ /**
+ * extract the data fields following a header.
+ * e.g. ContentType: xyz; Aa=b; cc=d
+ *
+ * @data: |string| containing a single header
+ *
+ * @returns |array| of |arrays| containing pairs of aa/b and cc/d
+ */
+ getHeaderData(data) {
+ lazy.EnigmailLog.DEBUG(
+ "funcs.jsm: getHeaderData: " + data.substr(0, 100) + "\n"
+ );
+ var a = data.split(/\n/);
+ var res = [];
+ for (let i = 0; i < a.length; i++) {
+ if (a[i].length === 0) {
+ break;
+ }
+ let b = a[i].split(/;/);
+
+ // extract "abc = xyz" tuples
+ for (let j = 0; j < b.length; j++) {
+ let m = b[j].match(/^(\s*)([^=\s;]+)(\s*)(=)(\s*)(.*)(\s*)$/);
+ if (m) {
+ // m[2]: identifier / m[6]: data
+ res[m[2].toLowerCase()] = m[6].replace(/\s*$/, "");
+ lazy.EnigmailLog.DEBUG(
+ "funcs.jsm: getHeaderData: " +
+ m[2].toLowerCase() +
+ " = " +
+ res[m[2].toLowerCase()] +
+ "\n"
+ );
+ }
+ }
+ if (i === 0 && !a[i].includes(";")) {
+ break;
+ }
+ if (i > 0 && a[i].search(/^\s/) < 0) {
+ break;
+ }
+ }
+ return res;
+ },
+
+ /***
+ * Get the text for the encrypted subject (either configured by user or default)
+ */
+ getProtectedSubjectText() {
+ return "...";
+ },
+
+ cloneObj(orig) {
+ let newObj;
+
+ if (typeof orig !== "object" || orig === null || orig === undefined) {
+ return orig;
+ }
+
+ if ("clone" in orig && typeof orig.clone === "function") {
+ return orig.clone();
+ }
+
+ if (Array.isArray(orig) && orig.length > 0) {
+ newObj = [];
+ for (let i in orig) {
+ if (typeof orig[i] === "object") {
+ newObj.push(this.cloneObj(orig[i]));
+ } else {
+ newObj.push(orig[i]);
+ }
+ }
+ } else {
+ newObj = {};
+ for (let i in orig) {
+ if (typeof orig[i] === "object") {
+ newObj[i] = this.cloneObj(orig[i]);
+ } else {
+ newObj[i] = orig[i];
+ }
+ }
+ }
+
+ return newObj;
+ },
+
+ /**
+ * Compare two MIME part numbers to determine which of the two is earlier in the tree
+ * MIME part numbers have the structure "x.y.z...", e.g 1, 1.2, 2.3.1.4.5.1.2
+ *
+ * @param mime1, mime2 - String the two mime part numbers to compare.
+ *
+ * @returns Number (one of -2, -1, 0, 1 , 2)
+ * - Negative number if mime1 is before mime2
+ * - Positive number if mime1 is after mime2
+ * - 0 if mime1 and mime2 are equal
+ * - if mime1 is a parent of mime2 the return value is -2
+ * - if mime2 is a parent of mime1 the return value is 2
+ *
+ * Throws an error if mime1 or mime2 do not comply to the required format
+ */
+ compareMimePartLevel(mime1, mime2) {
+ let s = new RegExp("^[0-9]+(\\.[0-9]+)*$");
+ if (mime1.search(s) < 0) {
+ throw new Error("Invalid mime1");
+ }
+ if (mime2.search(s) < 0) {
+ throw new Error("Invalid mime2");
+ }
+
+ let a1 = mime1.split(/\./);
+ let a2 = mime2.split(/\./);
+
+ for (let i = 0; i < Math.min(a1.length, a2.length); i++) {
+ if (Number(a1[i]) < Number(a2[i])) {
+ return -1;
+ }
+ if (Number(a1[i]) > Number(a2[i])) {
+ return 1;
+ }
+ }
+
+ if (a2.length > a1.length) {
+ return -2;
+ }
+ if (a2.length < a1.length) {
+ return 2;
+ }
+ return 0;
+ },
+
+ /**
+ * Get the nsIMsgAccount associated with a given nsIMsgIdentity
+ */
+ getAccountForIdentity(identity) {
+ for (let ac of MailServices.accounts.accounts) {
+ for (let id of ac.identities) {
+ if (id.key === identity.key) {
+ return ac;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Get the default identity of the default account
+ */
+ getDefaultIdentity() {
+ try {
+ let ac;
+ if (MailServices.accounts.defaultAccount) {
+ ac = MailServices.accounts.defaultAccount;
+ } else {
+ for (ac of MailServices.accounts.accounts) {
+ if (
+ ac.incomingServer.type === "imap" ||
+ ac.incomingServer.type === "pop3"
+ ) {
+ break;
+ }
+ }
+ }
+
+ if (ac.defaultIdentity) {
+ return ac.defaultIdentity;
+ }
+ return ac.identities[0];
+ } catch (x) {
+ return null;
+ }
+ },
+
+ /**
+ * Get a list of all own email addresses, taken from all identities
+ * and all reply-to addresses
+ */
+ getOwnEmailAddresses() {
+ let ownEmails = {};
+
+ // Determine all sorts of own email addresses
+ for (let id of MailServices.accounts.allIdentities) {
+ if (id.email && id.email.length > 0) {
+ ownEmails[id.email.toLowerCase()] = 1;
+ }
+ if (id.replyTo && id.replyTo.length > 0) {
+ try {
+ let replyEmails = this.stripEmail(id.replyTo)
+ .toLowerCase()
+ .split(/,/);
+ for (let j in replyEmails) {
+ ownEmails[replyEmails[j]] = 1;
+ }
+ } catch (ex) {}
+ }
+ }
+
+ return ownEmails;
+ },
+
+ /**
+ * Determine the distinct number of non-self recipients of a message.
+ * Only To: and Cc: fields are considered.
+ */
+ getNumberOfRecipients(msgCompField) {
+ let recipients = {},
+ ownEmails = this.getOwnEmailAddresses();
+
+ let allAddr = (
+ this.stripEmail(msgCompField.to) +
+ "," +
+ this.stripEmail(msgCompField.cc)
+ ).toLowerCase();
+ let emails = allAddr.split(/,+/);
+
+ for (let i = 0; i < emails.length; i++) {
+ let r = emails[i];
+ if (r && !(r in ownEmails)) {
+ recipients[r] = 1;
+ }
+ }
+
+ return recipients.length;
+ },
+
+ /**
+ * Get a mail URL from a uriSpec.
+ *
+ * @param {string} uriSpec - URL spec of the desired message.
+ *
+ * @returns {nsIURL|nsIMsgMailNewsUrl|null} The necko url.
+ */
+ getUrlFromUriSpec(uriSpec) {
+ try {
+ if (!uriSpec) {
+ return null;
+ }
+
+ let msgService = MailServices.messageServiceFromURI(uriSpec);
+ let url = msgService.getUrlForUri(uriSpec);
+
+ if (url.scheme == "file") {
+ return url;
+ }
+
+ return url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ } catch (ex) {
+ return null;
+ }
+ },
+
+ /**
+ * Test if the given string looks roughly like an email address,
+ * returns true or false.
+ */
+ stringLooksLikeEmailAddress(str) {
+ return /^[^ @]+@[^ @]+$/.test(str);
+ },
+
+ /**
+ * Extract an email address from the given string, using MailServices.
+ * However, be more strict, and avoid strings that appear to be
+ * invalid addresses.
+ *
+ * If more than one email address is found, only return the first.
+ *
+ * If we fail to extract an email address from the given string,
+ * because the given string doesn't conform to expectations,
+ * an empty string is returned.
+ */
+ getEmailFromUserID(uid) {
+ let addresses = MailServices.headerParser.makeFromDisplayAddress(uid);
+ if (
+ !addresses[0] ||
+ !EnigmailFuncs.stringLooksLikeEmailAddress(addresses[0].email)
+ ) {
+ console.debug("failed to extract email address from: " + uid);
+ return "";
+ }
+
+ return addresses[0].email.trim();
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/key.jsm b/comm/mail/extensions/openpgp/content/modules/key.jsm
new file mode 100644
index 0000000000..06f9779b0f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/key.jsm
@@ -0,0 +1,285 @@
+/*
+ * 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 = ["EnigmailKey"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var EnigmailKey = {
+ /**
+ * Format a key fingerprint
+ *
+ * @fingerprint |string| - unformatted OpenPGP fingerprint
+ *
+ * @returns |string| - formatted string
+ */
+ formatFpr(fingerprint) {
+ //EnigmailLog.DEBUG("key.jsm: EnigmailKey.formatFpr(" + fingerprint + ")\n");
+ // format key fingerprint
+ let r = "";
+ const fpr = fingerprint.match(
+ /(....)(....)(....)(....)(....)(....)(....)(....)(....)?(....)?/
+ );
+ if (fpr && fpr.length > 2) {
+ fpr.shift();
+ r = fpr.join(" ");
+ }
+
+ return r;
+ },
+
+ // Extract public key from Status Message
+ extractPubkey(statusMsg) {
+ const matchb = statusMsg.match(/(^|\n)NO_PUBKEY (\w{8})(\w{8})/);
+ if (matchb && matchb.length > 3) {
+ lazy.EnigmailLog.DEBUG(
+ "Enigmail.extractPubkey: NO_PUBKEY 0x" + matchb[3] + "\n"
+ );
+ return matchb[2] + matchb[3];
+ }
+ return null;
+ },
+
+ /**
+ * import a revocation certificate form a given keyblock string.
+ * Ask the user before importing the cert, and display an error
+ * message in case of failures.
+ */
+ importRevocationCert(keyId, keyBlockStr) {
+ let key = lazy.EnigmailKeyRing.getKeyById(keyId);
+
+ if (key) {
+ if (key.keyTrust === "r") {
+ // Key has already been revoked
+ lazy.l10n
+ .formatValue("revoke-key-already-revoked", {
+ keyId,
+ })
+ .then(value => {
+ lazy.EnigmailDialog.info(null, value);
+ });
+ } else {
+ let userId = key.userId + " - 0x" + key.keyId;
+ if (
+ !lazy.EnigmailDialog.confirmDlg(
+ null,
+ lazy.l10n.formatValueSync("revoke-key-question", { userId }),
+ lazy.l10n.formatValueSync("key-man-button-revoke-key")
+ )
+ ) {
+ return;
+ }
+
+ let errorMsgObj = {};
+ // TODO this will certainly not work yet, because RNP requires
+ // calling a different function for importing revocation
+ // signatures, see RNP.importRevImpl
+ if (
+ lazy.EnigmailKeyRing.importKey(
+ null,
+ false,
+ keyBlockStr,
+ false,
+ keyId,
+ errorMsgObj
+ ) > 0
+ ) {
+ lazy.EnigmailDialog.alert(null, errorMsgObj.value);
+ }
+ }
+ } else {
+ // Suitable key for revocation certificate is not present in keyring
+ lazy.l10n
+ .formatValue("revoke-key-not-present", {
+ keyId,
+ })
+ .then(value => {
+ lazy.EnigmailDialog.alert(null, value);
+ });
+ }
+ },
+
+ _keyListCache: new Map(),
+ _keyListCacheMaxEntries: 50,
+ _keyListCacheMaxKeySize: 30720,
+
+ /**
+ * Get details (key ID, UID) of the data contained in a OpenPGP key block
+ *
+ * @param {string} keyBlockStr - the contents of one or more public keys
+ * @param {object} errorMsgObj - obj.value will contain an error message in case of failures
+ * @param {boolean} interactive - if in interactive mode, may display dialogs (default: true)
+ * @param {boolean} pubkey - load public keys from the given block
+ * @param {boolean} seckey - load secret keys from the given block
+ *
+ * @returns {object[]} an array of objects with the following structure:
+ * - id (key ID)
+ * - fpr
+ * - name (the UID of the key)
+ * - state (one of "old" [existing key], "new" [new key], "invalid" [key cannot not be imported])
+ */
+ async getKeyListFromKeyBlock(
+ keyBlockStr,
+ errorMsgObj,
+ interactive,
+ pubkey,
+ seckey,
+ withPubKey = false
+ ) {
+ lazy.EnigmailLog.DEBUG("key.jsm: getKeyListFromKeyBlock\n");
+ errorMsgObj.value = "";
+
+ let cacheEntry = this._keyListCache.get(keyBlockStr);
+ if (cacheEntry) {
+ // Remove and re-insert to move entry to the end of insertion order,
+ // so we know which entry was used least recently.
+ this._keyListCache.delete(keyBlockStr);
+ this._keyListCache.set(keyBlockStr, cacheEntry);
+
+ if (cacheEntry.error) {
+ errorMsgObj.value = cacheEntry.error;
+ return null;
+ }
+ return cacheEntry.data;
+ }
+
+ // We primarily want to cache single keys that are found in email
+ // attachments. We shouldn't attempt to cache larger key blocks
+ // that are likely arriving from explicit import attempts.
+ let updateCache = keyBlockStr.length < this._keyListCacheMaxKeySize;
+
+ if (
+ updateCache &&
+ this._keyListCache.size >= this._keyListCacheMaxEntries
+ ) {
+ // Remove oldest entry, make room for new entry.
+ this._keyListCache.delete(this._keyListCache.keys().next().value);
+ }
+
+ const cApi = lazy.EnigmailCryptoAPI();
+ let keyList;
+ let key = {};
+ let blocks;
+ errorMsgObj.value = "";
+
+ try {
+ keyList = await cApi.getKeyListFromKeyBlockAPI(
+ keyBlockStr,
+ pubkey,
+ seckey,
+ true,
+ withPubKey
+ );
+ } catch (ex) {
+ errorMsgObj.value = ex.toString();
+ if (updateCache && !withPubKey) {
+ this._keyListCache.set(keyBlockStr, {
+ error: errorMsgObj.value,
+ data: null,
+ });
+ }
+ return null;
+ }
+
+ if (!keyList) {
+ if (updateCache) {
+ this._keyListCache.set(keyBlockStr, { error: undefined, data: null });
+ }
+ return null;
+ }
+
+ if (interactive && keyList.length === 1) {
+ // TODO: not yet tested
+ key = keyList[0];
+ if ("revoke" in key && !("name" in key)) {
+ if (updateCache) {
+ this._keyListCache.set(keyBlockStr, { error: undefined, data: [] });
+ }
+ this.importRevocationCert(key.id, blocks.join("\n"));
+ return [];
+ }
+ }
+
+ if (updateCache) {
+ this._keyListCache.set(keyBlockStr, { error: undefined, data: keyList });
+ }
+ return keyList;
+ },
+
+ /**
+ * Get details of a key block to import. Works identically as getKeyListFromKeyBlock();
+ * except that the input is a file instead of a string
+ *
+ * @param {nsIFile} file - The file to read.
+ * @param {object} errorMsgObj - Object; obj.value will contain error message.
+ *
+ * @returns {object[]} An array of objects; see getKeyListFromKeyBlock()
+ */
+ async getKeyListFromKeyFile(
+ file,
+ errorMsgObj,
+ pubkey,
+ seckey,
+ withPubKey = false
+ ) {
+ let data = await IOUtils.read(file.path);
+ let contents = lazy.MailStringUtils.uint8ArrayToByteString(data);
+ return this.getKeyListFromKeyBlock(
+ contents,
+ errorMsgObj,
+ true,
+ pubkey,
+ seckey,
+ withPubKey
+ );
+ },
+
+ /**
+ * Compare 2 KeyIds of possible different length (short, long, FPR-length, with or without prefixed
+ * 0x are accepted)
+ *
+ * @param keyId1 string
+ * @param keyId2 string
+ *
+ * @returns true or false, given the comparison of the last minimum-length characters.
+ */
+ compareKeyIds(keyId1, keyId2) {
+ var keyId1Raw = keyId1.replace(/^0x/, "").toUpperCase();
+ var keyId2Raw = keyId2.replace(/^0x/, "").toUpperCase();
+
+ var minlength = Math.min(keyId1Raw.length, keyId2Raw.length);
+
+ if (minlength < keyId1Raw.length) {
+ // Limit keyId1 to minlength
+ keyId1Raw = keyId1Raw.substr(-minlength, minlength);
+ }
+
+ if (minlength < keyId2Raw.length) {
+ // Limit keyId2 to minlength
+ keyId2Raw = keyId2Raw.substr(-minlength, minlength);
+ }
+
+ return keyId1Raw === keyId2Raw;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/keyLookupHelper.jsm b/comm/mail/extensions/openpgp/content/modules/keyLookupHelper.jsm
new file mode 100644
index 0000000000..621b61b2ae
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/keyLookupHelper.jsm
@@ -0,0 +1,380 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["KeyLookupHelper"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ CollectedKeysDB: "chrome://openpgp/content/modules/CollectedKeysDB.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailKeyServer: "chrome://openpgp/content/modules/keyserver.jsm",
+ EnigmailKeyserverURIs: "chrome://openpgp/content/modules/keyserverUris.jsm",
+ EnigmailWkdLookup: "chrome://openpgp/content/modules/wkdLookup.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var KeyLookupHelper = {
+ /**
+ * Internal helper function, search for keys by either keyID
+ * or email address on a keyserver.
+ * Returns additional flags regarding lookup and import.
+ * Will never show feedback prompts.
+ *
+ * @param {string} mode - "interactive-import" or "silent-collection"
+ * In interactive-import mode, the user will be asked to confirm
+ * import of keys into the permanent keyring.
+ * In silent-collection mode, only updates to existing keys will
+ * be imported. New keys will only be added to CollectedKeysDB.
+ * @param {nsIWindow} window - parent window
+ * @param {string} identifier - search value, either key ID or fingerprint or email address.
+ * @returns {object} flags
+ * @returns {boolean} flags.keyImported - At least one key was imported.
+ * @returns {boolean} flags.foundUpdated - At least one update for a local existing key was found and imported.
+ * @returns {boolean} flags.foundUnchanged - All found keys are identical to already existing local keys.
+ * @returns {boolean} flags.collectedForLater - At least one key was added to CollectedKeysDB.
+ */
+
+ isExpiredOrRevoked(keyTrust) {
+ return keyTrust.match(/e/i) || keyTrust.match(/r/i);
+ },
+
+ async _lookupAndImportOnKeyserver(mode, window, identifier) {
+ let keyImported = false;
+ let foundUpdated = false;
+ let foundUnchanged = false;
+ let collectedForLater = false;
+
+ let ksArray = lazy.EnigmailKeyserverURIs.getKeyServers();
+ if (!ksArray.length) {
+ return false;
+ }
+
+ let continueSearching = true;
+ for (let ks of ksArray) {
+ let foundKey;
+ if (ks.startsWith("vks://")) {
+ foundKey = await lazy.EnigmailKeyServer.downloadNoImport(
+ identifier,
+ ks
+ );
+ } else if (ks.startsWith("hkp://") || ks.startsWith("hkps://")) {
+ foundKey =
+ await lazy.EnigmailKeyServer.searchAndDownloadSingleResultNoImport(
+ identifier,
+ ks
+ );
+ }
+ if (foundKey && "keyData" in foundKey) {
+ let errorInfo = {};
+ let keyList = await lazy.EnigmailKey.getKeyListFromKeyBlock(
+ foundKey.keyData,
+ errorInfo,
+ false,
+ true,
+ false
+ );
+ // We might get a zero length keyList, if we refuse to use the key
+ // that we received because of its properties.
+ if (keyList && keyList.length == 1) {
+ let oldKey = lazy.EnigmailKeyRing.getKeyById(keyList[0].fpr);
+ if (oldKey) {
+ await lazy.EnigmailKeyRing.importKeyDataSilent(
+ window,
+ foundKey.keyData,
+ true,
+ "0x" + keyList[0].fpr
+ );
+
+ let updatedKey = lazy.EnigmailKeyRing.getKeyById(keyList[0].fpr);
+ // If new imported/merged key is equal to old key,
+ // don't notify about new keys details.
+ if (JSON.stringify(oldKey) !== JSON.stringify(updatedKey)) {
+ foundUpdated = true;
+ keyImported = true;
+ if (mode == "interactive-import") {
+ lazy.EnigmailDialog.keyImportDlg(
+ window,
+ keyList.map(a => a.id)
+ );
+ }
+ } else {
+ foundUnchanged = true;
+ }
+ } else {
+ keyList = keyList.filter(k => k.userIds.length);
+ keyList = keyList.filter(k => !this.isExpiredOrRevoked(k.keyTrust));
+ if (keyList.length && mode == "interactive-import") {
+ keyImported =
+ await lazy.EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ keyList,
+ foundKey.keyData,
+ true
+ );
+ if (keyImported) {
+ // In interactive mode, don't offer the user to import keys multiple times.
+ // When silently collecting keys, it's fine to discover everything we can.
+ continueSearching = false;
+ }
+ }
+ if (!keyImported) {
+ collectedForLater = true;
+ let db = await lazy.CollectedKeysDB.getInstance();
+ for (let newKey of keyList) {
+ // If key is known in the db: merge + update.
+ let key = await db.mergeExisting(newKey, foundKey.keyData, {
+ uri: lazy.EnigmailKeyServer.serverReqURL(
+ `0x${newKey.fpr}`,
+ ks
+ ),
+ type: "keyserver",
+ });
+ await db.storeKey(key);
+ }
+ }
+ }
+ } else {
+ if (keyList && keyList.length > 1) {
+ throw new Error("Unexpected multiple results from keyserver " + ks);
+ }
+ console.log(
+ "failed to process data retrieved from keyserver " +
+ ks +
+ ": " +
+ errorInfo.value
+ );
+ }
+ }
+ if (!continueSearching) {
+ break;
+ }
+ }
+
+ return { keyImported, foundUpdated, foundUnchanged, collectedForLater };
+ },
+
+ /**
+ * Search online for keys by key ID on keyserver.
+ *
+ * @param {string} mode - "interactive-import" or "silent-collection"
+ * In interactive-import mode, the user will be asked to confirm
+ * import of keys into the permanent keyring.
+ * In silent-collection mode, only updates to existing keys will
+ * be imported. New keys will only be added to CollectedKeysDB.
+ * @param {nsIWindow} window - parent window
+ * @param {string} keyId - the key ID to search for.
+ * @param {boolean} giveFeedbackToUser - false to be silent,
+ * true to show feedback to user after search and import is complete.
+ * @returns {boolean} - true if at least one key was imported.
+ */
+ async lookupAndImportByKeyID(mode, window, keyId, giveFeedbackToUser) {
+ if (!/^0x/i.test(keyId)) {
+ keyId = "0x" + keyId;
+ }
+ let importResult = await this._lookupAndImportOnKeyserver(
+ mode,
+ window,
+ keyId
+ );
+ if (
+ mode == "interactive-import" &&
+ giveFeedbackToUser &&
+ !importResult.keyImported
+ ) {
+ let msgId;
+ if (importResult.foundUnchanged) {
+ msgId = "no-update-found";
+ } else {
+ msgId = "no-key-found2";
+ }
+ let value = await lazy.l10n.formatValue(msgId);
+ lazy.EnigmailDialog.alert(window, value);
+ }
+ return importResult.keyImported;
+ },
+
+ /**
+ * Search online for keys by email address.
+ * Will search both WKD and keyserver.
+ *
+ * @param {string} mode - "interactive-import" or "silent-collection"
+ * In interactive-import mode, the user will be asked to confirm
+ * import of keys into the permanent keyring.
+ * In silent-collection mode, only updates to existing keys will
+ * be imported. New keys will only be added to CollectedKeysDB.
+ * @param {nsIWindow} window - parent window
+ * @param {string} email - the email address to search for.
+ * @param {boolean} giveFeedbackToUser - false to be silent,
+ * true to show feedback to user after search and import is complete.
+ * @returns {boolean} - true if at least one key was imported.
+ */
+ async lookupAndImportByEmail(mode, window, email, giveFeedbackToUser) {
+ let resultKeyImported = false;
+
+ let wkdKeyImported = false;
+ let wkdFoundUnchanged = false;
+
+ let wkdResult;
+ let wkdUrl;
+ if (lazy.EnigmailWkdLookup.isWkdAvailable(email)) {
+ wkdUrl = await lazy.EnigmailWkdLookup.getDownloadUrlFromEmail(
+ email,
+ true
+ );
+ wkdResult = await lazy.EnigmailWkdLookup.downloadKey(wkdUrl);
+ if (!wkdResult) {
+ wkdUrl = await lazy.EnigmailWkdLookup.getDownloadUrlFromEmail(
+ email,
+ false
+ );
+ wkdResult = await lazy.EnigmailWkdLookup.downloadKey(wkdUrl);
+ }
+ }
+
+ if (!wkdResult) {
+ console.debug("searchKeysOnInternet no wkd data for " + email);
+ } else {
+ let errorInfo = {};
+ let keyList = await lazy.EnigmailKey.getKeyListFromKeyBlock(
+ wkdResult,
+ errorInfo,
+ false,
+ true,
+ false,
+ true
+ );
+ if (!keyList) {
+ console.debug(
+ "failed to process data retrieved from WKD server: " + errorInfo.value
+ );
+ } else {
+ let existingKeys = [];
+ let newKeys = [];
+
+ for (let wkdKey of keyList) {
+ let oldKey = lazy.EnigmailKeyRing.getKeyById(wkdKey.fpr);
+ if (oldKey) {
+ await lazy.EnigmailKeyRing.importKeyDataSilent(
+ window,
+ wkdKey.pubKey,
+ true,
+ "0x" + wkdKey.fpr
+ );
+
+ let updatedKey = lazy.EnigmailKeyRing.getKeyById(wkdKey.fpr);
+ // If new imported/merged key is equal to old key,
+ // don't notify about new keys details.
+ if (JSON.stringify(oldKey) !== JSON.stringify(updatedKey)) {
+ // If a caller ever needs information what we found,
+ // this is the place to set: wkdFoundUpdated = true
+ existingKeys.push(wkdKey.id);
+ } else {
+ wkdFoundUnchanged = true;
+ }
+ } else if (wkdKey.userIds.length) {
+ newKeys.push(wkdKey);
+ }
+ }
+
+ if (existingKeys.length) {
+ if (mode == "interactive-import") {
+ lazy.EnigmailDialog.keyImportDlg(window, existingKeys);
+ }
+ wkdKeyImported = true;
+ }
+
+ newKeys = newKeys.filter(k => !this.isExpiredOrRevoked(k.keyTrust));
+ if (newKeys.length && mode == "interactive-import") {
+ wkdKeyImported =
+ wkdKeyImported ||
+ (await lazy.EnigmailKeyRing.importKeyArrayWithConfirmation(
+ window,
+ newKeys,
+ true
+ ));
+ }
+ if (!wkdKeyImported) {
+ // If a caller ever needs information what we found,
+ // this is the place to set: wkdCollectedForLater = true
+ let db = await lazy.CollectedKeysDB.getInstance();
+ for (let newKey of newKeys) {
+ // If key is known in the db: merge + update.
+ let key = await db.mergeExisting(newKey, newKey.pubKey, {
+ uri: wkdUrl,
+ type: "wkd",
+ });
+ await db.storeKey(key);
+ }
+ }
+ }
+ }
+
+ let { keyImported, foundUnchanged } =
+ await this._lookupAndImportOnKeyserver(mode, window, email);
+ resultKeyImported = wkdKeyImported || keyImported;
+
+ if (
+ mode == "interactive-import" &&
+ giveFeedbackToUser &&
+ !resultKeyImported &&
+ !keyImported
+ ) {
+ let msgId;
+ if (wkdFoundUnchanged || foundUnchanged) {
+ msgId = "no-update-found";
+ } else {
+ msgId = "no-key-found2";
+ }
+ let value = await lazy.l10n.formatValue(msgId);
+ lazy.EnigmailDialog.alert(window, value);
+ }
+
+ return resultKeyImported;
+ },
+
+ /**
+ * This function will perform discovery of new or updated OpenPGP
+ * keys using various mechanisms.
+ *
+ * @param {string} mode - "interactive-import" or "silent-collection"
+ * @param {string} email - search for keys for this email address,
+ * (parameter allowed to be null or empty)
+ * @param {string[]} keyIds - KeyIDs that should be updated.
+ * (parameter allowed to be null or empty)
+ *
+ * @returns {boolean} - Returns true if at least one key was imported.
+ */
+ async fullOnlineDiscovery(mode, window, email, keyIds) {
+ // Try to get updates for all existing keys from keyserver,
+ // by key ID, to get updated validy/revocation info.
+ // (A revoked key on the keyserver might have no user ID.)
+ let atLeastoneImport = false;
+ if (keyIds) {
+ for (let keyId of keyIds) {
+ // Ensure the function call goes first in the logic or expression,
+ // to ensure it's always called, even if atLeastoneImport is already true.
+ let rv = await this.lookupAndImportByKeyID(mode, window, keyId, false);
+ atLeastoneImport = rv || atLeastoneImport;
+ }
+ }
+ // Now check for updated or new keys by email address
+ let rv2 = await this.lookupAndImportByEmail(mode, window, email, false);
+ atLeastoneImport = rv2 || atLeastoneImport;
+ return atLeastoneImport;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/keyObj.jsm b/comm/mail/extensions/openpgp/content/modules/keyObj.jsm
new file mode 100644
index 0000000000..ed9137cb3a
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/keyObj.jsm
@@ -0,0 +1,679 @@
+/*
+ * 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";
+
+var EXPORTED_SYMBOLS = ["newEnigmailKeyObj"];
+
+/**
+ This module implements the EnigmailKeyObj class with the following members:
+
+ - keyId - 16 digits (8-byte) public key ID (/not/ preceded with 0x)
+ - userId - main user ID
+ - fpr - fingerprint
+ - fprFormatted - a formatted version of the fingerprint following the scheme .... .... ....
+ - expiry - Expiry date as printable string
+ - expiryTime - Expiry time as seconds after 01/01/1970
+ - created - Key creation date as printable string
+ - keyCreated - Key creation date/time as number
+ - keyTrust - key trust code as provided by GnuPG (calculated key validity)
+ - keyUseFor - key usage type as provided by GnuPG (key capabilities)
+ - ownerTrust - owner trust as provided by GnuPG
+ - photoAvailable - [Boolean] true if photo is available
+ - secretAvailable - [Boolean] true if secret key is available
+ - algoSym - public key algorithm type (String, e.g. RSA)
+ - keySize - size of public key
+ - type - "pub" or "grp"
+ - userIds - [Array]: - Contains ALL UIDs (including the primary UID)
+ * userId - User ID
+ * keyTrust - trust level of user ID
+ * uidFpr - fingerprint of the user ID
+ * type - one of "uid" (regular user ID), "uat" (photo)
+ * uatNum - photo number (starting with 0 for each key)
+ - subKeys - [Array]:
+ * keyId - subkey ID (16 digits (8-byte))
+ * expiry - Expiry date as printable string
+ * expiryTime - Expiry time as seconds after 01/01/1970
+ * created - Subkey creation date as printable string
+ * keyCreated - Subkey creation date/time as number
+ * keyTrust - key trust code as provided by GnuPG
+ * keyUseFor - key usage type as provided by GnuPG
+ * algoSym - subkey algorithm type (String, e.g. RSA)
+ * keySize - subkey size
+ * type - "sub"
+
+ - methods:
+ * hasSubUserIds
+ * getKeyExpiry
+ * getEncryptionValidity
+ * getSigningValidity
+ * getPubKeyValidity
+ * clone
+ * getMinimalPubKey
+ * getVirtualKeySize
+*/
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+function newEnigmailKeyObj(keyData) {
+ return new EnigmailKeyObj(keyData);
+}
+
+class EnigmailKeyObj {
+ constructor(keyData) {
+ this.keyId = "";
+ this.expiry = "";
+ this.expiryTime = 0;
+ this.created = "";
+ this.keyTrust = "";
+ this.keyUseFor = "";
+ this.ownerTrust = "";
+ this.algoSym = "";
+ this.keySize = "";
+ this.userId = "";
+ this.userIds = [];
+ this.subKeys = [];
+ this.fpr = "";
+ this.minimalKeyBlock = [];
+ this.photoAvailable = false;
+ this.secretAvailable = false;
+ this.secretMaterial = false;
+
+ this.type = keyData.type;
+ if ("keyId" in keyData) {
+ this.keyId = keyData.keyId;
+ }
+ if ("expiryTime" in keyData) {
+ this.expiryTime = keyData.expiryTime;
+ this.expiry = keyData.expiryTime
+ ? new Services.intl.DateTimeFormat().format(
+ new Date(keyData.expiryTime * 1000)
+ )
+ : "";
+ }
+ if ("effectiveExpiryTime" in keyData) {
+ this.effectiveExpiryTime = keyData.effectiveExpiryTime;
+ this.effectiveExpiry = keyData.effectiveExpiryTime
+ ? new Services.intl.DateTimeFormat().format(
+ new Date(keyData.effectiveExpiryTime * 1000)
+ )
+ : "";
+ }
+
+ const ATTRS = [
+ "created",
+ "keyCreated",
+ "keyTrust",
+ "keyUseFor",
+ "ownerTrust",
+ "algoSym",
+ "keySize",
+ "userIds",
+ "subKeys",
+ "fpr",
+ "secretAvailable",
+ "secretMaterial",
+ "photoAvailable",
+ "userId",
+ "hasIgnoredAttributes",
+ ];
+ for (let i of ATTRS) {
+ if (i in keyData) {
+ this[i] = keyData[i];
+ }
+ }
+ }
+
+ /**
+ * create a copy of the object
+ */
+ clone() {
+ let cp = new EnigmailKeyObj(["copy"]);
+ for (let i in this) {
+ if (i !== "fprFormatted") {
+ if (typeof this[i] !== "function") {
+ if (typeof this[i] === "object") {
+ cp[i] = lazy.EnigmailFuncs.cloneObj(this[i]);
+ } else {
+ cp[i] = this[i];
+ }
+ }
+ }
+ }
+
+ return cp;
+ }
+
+ /**
+ * Does the key have secondary user IDs?
+ *
+ * @return: Boolean - true if yes; false if no
+ */
+ hasSubUserIds() {
+ let nUid = 0;
+ for (let i in this.userIds) {
+ if (this.userIds[i].type === "uid") {
+ ++nUid;
+ }
+ }
+
+ return nUid >= 2;
+ }
+
+ /**
+ * Get a formatted version of the fingerprint:
+ * 1234 5678 90AB CDEF .... ....
+ *
+ * @returns String - the formatted fingerprint
+ */
+ get fprFormatted() {
+ let f = lazy.EnigmailKey.formatFpr(this.fpr);
+ if (f.length === 0) {
+ f = this.fpr;
+ }
+ return f;
+ }
+
+ /**
+ * Determine if the public key is valid. If not, return a description why it's not
+ *
+ * @returns Object:
+ * - keyValid: Boolean (true if key is valid)
+ * - reason: String (explanation of invalidity)
+ */
+ getPubKeyValidity(exceptionReason = null) {
+ let retVal = {
+ keyValid: false,
+ reason: "",
+ };
+ if (this.keyTrust.search(/r/i) >= 0) {
+ // public key revoked
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-pub-key-revoked", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ } else if (
+ exceptionReason != "ignoreExpired" &&
+ this.keyTrust.search(/e/i) >= 0
+ ) {
+ // public key expired
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-pub-key-expired", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ } else {
+ retVal.keyValid = true;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Check whether a key can be used for signing and return a description of why not
+ *
+ * @returns Object:
+ * - keyValid: Boolean (true if key is valid)
+ * - reason: String (explanation of invalidity)
+ */
+ getSigningValidity(exceptionReason = null) {
+ let retVal = this.getPubKeyValidity(exceptionReason);
+
+ if (!retVal.keyValid) {
+ return retVal;
+ }
+
+ if (!this.secretAvailable) {
+ retVal.keyValid = false;
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-no-secret-key", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ return retVal;
+ }
+
+ if (/s/.test(this.keyUseFor) && this.secretMaterial) {
+ return retVal;
+ }
+
+ retVal.keyValid = false;
+ let expired = 0;
+ let revoked = 0;
+ let found = 0;
+ let noSecret = 0;
+
+ for (let sk in this.subKeys) {
+ if (this.subKeys[sk].keyUseFor.search(/s/) >= 0) {
+ if (
+ this.subKeys[sk].keyTrust.search(/e/i) >= 0 &&
+ exceptionReason != "ignoreExpired"
+ ) {
+ ++expired;
+ } else if (this.subKeys[sk].keyTrust.search(/r/i) >= 0) {
+ ++revoked;
+ } else if (!this.subKeys[sk].secretMaterial) {
+ ++noSecret;
+ } else {
+ // found subkey usable
+ ++found;
+ }
+ }
+ }
+
+ if (!found) {
+ if (exceptionReason != "ignoreExpired" && expired) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-sign-sub-keys-expired",
+ {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ }
+ );
+ } else if (revoked) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-sign-sub-keys-revoked",
+ {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ }
+ );
+ } else if (noSecret) {
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-no-secret-key", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ } else {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-pub-key-not-for-signing",
+ {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ }
+ );
+ }
+ } else {
+ retVal.keyValid = true;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Check whether a key can be used for encryption and return a description of why not
+ *
+ * @param {boolean} requireDecryptionKey:
+ * If true, require secret key material to be available
+ * for at least one encryption key.
+ * @param {string} exceptionReason:
+ * Can be used to override the requirement to check for
+ * full validity, and accept certain scenarios as valid.
+ * If value is set to "ignoreExpired",
+ * then an expired key isn't treated as invalid.
+ * Set to null to get the default behavior.
+ * @param {string} subId:
+ * A key ID of a subkey or null.
+ * If subId is null, any part of the key will be
+ * considered when looking for a valid encryption key.
+ * If subId is non-null, only this subkey will be
+ * checked.
+ *
+ * @returns Object:
+ * - keyValid: Boolean (true if key is valid)
+ * - reason: String (explanation of invalidity)
+ */
+ getEncryptionValidity(
+ requireDecryptionKey,
+ exceptionReason = null,
+ subId = null
+ ) {
+ let retVal = this.getPubKeyValidity(exceptionReason);
+ if (!retVal.keyValid) {
+ return retVal;
+ }
+
+ if (
+ !subId &&
+ this.keyUseFor.search(/e/) >= 0 &&
+ (!requireDecryptionKey || this.secretMaterial)
+ ) {
+ // We can stop and return the result we already found,
+ // because we aren't looking at a specific subkey (!subId),
+ // and the primary key is usable for encryption.
+ // If we must own secret key material (requireDecryptionKey),
+ // in this scenario it's sufficient to have secret material for
+ // the primary key.
+ return retVal;
+ }
+
+ retVal.keyValid = false;
+
+ let expired = 0;
+ let revoked = 0;
+ let found = 0;
+ let noSecret = 0;
+
+ for (let sk of this.subKeys) {
+ if (subId && subId != sk.keyId) {
+ continue;
+ }
+
+ if (sk.keyUseFor.search(/e/) >= 0) {
+ if (
+ sk.keyTrust.search(/e/i) >= 0 &&
+ exceptionReason != "ignoreExpired"
+ ) {
+ ++expired;
+ } else if (sk.keyTrust.search(/r/i) >= 0) {
+ ++revoked;
+ } else if (requireDecryptionKey && !sk.secretMaterial) {
+ ++noSecret;
+ } else {
+ // found subkey usable
+ ++found;
+ }
+ }
+ }
+
+ if (!found) {
+ let idToShow = subId ? subId : this.keyId;
+
+ if (exceptionReason != "ignoreExpired" && expired) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-enc-sub-keys-expired",
+ {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ }
+ );
+ } else if (revoked) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-enc-sub-keys-revoked",
+ {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ }
+ );
+ } else if (noSecret) {
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-no-secret-key", {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ });
+ } else {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-pub-key-not-for-encryption",
+ {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ }
+ );
+ }
+ } else {
+ retVal.keyValid = true;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Determine the next expiry date of the key. This is either the public key expiry date,
+ * or the maximum expiry date of a signing or encryption subkey. I.e. this returns the next
+ * date at which the key cannot be used for signing and/or encryption anymore
+ *
+ * @returns Number - The expiry date as seconds after 01/01/1970
+ */
+ getKeyExpiry() {
+ let expiryDate = Number.MAX_VALUE;
+ let encryption = -1;
+ let signing = -1;
+
+ // check public key expiry date
+ if (this.expiryTime > 0) {
+ expiryDate = this.expiryTime;
+ }
+
+ for (let sk in this.subKeys) {
+ if (this.subKeys[sk].keyUseFor.search(/[eE]/) >= 0) {
+ let expiry = this.subKeys[sk].expiryTime;
+ if (expiry === 0) {
+ expiry = Number.MAX_VALUE;
+ }
+ encryption = Math.max(encryption, expiry);
+ } else if (this.subKeys[sk].keyUseFor.search(/[sS]/) >= 0) {
+ let expiry = this.subKeys[sk].expiryTime;
+ if (expiry === 0) {
+ expiry = Number.MAX_VALUE;
+ }
+ signing = Math.max(signing, expiry);
+ }
+ }
+
+ if (expiryDate > encryption) {
+ if (this.keyUseFor.search(/[eE]/) < 0) {
+ expiryDate = encryption;
+ }
+ }
+
+ if (expiryDate > signing) {
+ if (this.keyUseFor.search(/[Ss]/) < 0) {
+ expiryDate = signing;
+ }
+ }
+
+ return expiryDate;
+ }
+
+ /**
+ * Export the minimum key for the public key object:
+ * public key, desired UID, newest signing/encryption subkey
+ *
+ * @param {string} emailAddr: [optional] email address of UID to extract. Use primary UID if null .
+ *
+ * @returns Object:
+ * - exitCode (0 = success)
+ * - errorMsg (if exitCode != 0)
+ * - keyData: BASE64-encded string of key data
+ */
+ getMinimalPubKey(emailAddr) {
+ lazy.EnigmailLog.DEBUG(
+ "keyObj.jsm: EnigmailKeyObj.getMinimalPubKey: " + this.keyId + "\n"
+ );
+
+ if (emailAddr) {
+ try {
+ emailAddr = lazy.EnigmailFuncs.stripEmail(emailAddr.toLowerCase());
+ } catch (x) {
+ emailAddr = emailAddr.toLowerCase();
+ }
+
+ let foundUid = false,
+ uid = "";
+ for (let i in this.userIds) {
+ try {
+ uid = lazy.EnigmailFuncs.stripEmail(
+ this.userIds[i].userId.toLowerCase()
+ );
+ } catch (x) {
+ uid = this.userIds[i].userId.toLowerCase();
+ }
+
+ if (uid == emailAddr) {
+ foundUid = true;
+ break;
+ }
+ }
+ if (!foundUid) {
+ emailAddr = false;
+ }
+ }
+
+ if (!emailAddr) {
+ emailAddr = this.userId;
+ }
+
+ try {
+ emailAddr = lazy.EnigmailFuncs.stripEmail(emailAddr.toLowerCase());
+ } catch (x) {
+ emailAddr = emailAddr.toLowerCase();
+ }
+
+ let newestSigningKey = 0,
+ newestEncryptionKey = 0,
+ subkeysArr = null;
+
+ // search for valid subkeys
+ for (let sk in this.subKeys) {
+ if (!"indDre".includes(this.subKeys[sk].keyTrust)) {
+ if (this.subKeys[sk].keyUseFor.search(/[sS]/) >= 0) {
+ // found signing subkey
+ if (this.subKeys[sk].keyCreated > newestSigningKey) {
+ newestSigningKey = this.subKeys[sk].keyCreated;
+ }
+ }
+ if (this.subKeys[sk].keyUseFor.search(/[eE]/) >= 0) {
+ // found encryption subkey
+ if (this.subKeys[sk].keyCreated > newestEncryptionKey) {
+ newestEncryptionKey = this.subKeys[sk].keyCreated;
+ }
+ }
+ }
+ }
+
+ if (newestSigningKey > 0 && newestEncryptionKey > 0) {
+ subkeysArr = [newestEncryptionKey, newestSigningKey];
+ }
+
+ if (!(emailAddr in this.minimalKeyBlock)) {
+ const cApi = lazy.EnigmailCryptoAPI();
+ this.minimalKeyBlock[emailAddr] = cApi.sync(
+ cApi.getMinimalPubKey(this.fpr, emailAddr, subkeysArr)
+ );
+ }
+ return this.minimalKeyBlock[emailAddr];
+ }
+
+ /**
+ * Obtain a "virtual" key size that allows to compare different algorithms with each other
+ * e.g. elliptic curve keys have small key sizes with high cryptographic strength
+ *
+ *
+ * @returns Number: a virtual size
+ */
+ getVirtualKeySize() {
+ lazy.EnigmailLog.DEBUG(
+ "keyObj.jsm: EnigmailKeyObj.getVirtualKeySize: " + this.keyId + "\n"
+ );
+
+ switch (this.algoSym) {
+ case "DSA":
+ return this.keySize / 2;
+ case "ECDSA":
+ return this.keySize * 8;
+ case "EDDSA":
+ return this.keySize * 32;
+ default:
+ return this.keySize;
+ }
+ }
+
+ /**
+ * @param {boolean} minimalKey - if true, reduce key to minimum required
+ *
+ * @returns {object}:
+ * - {Number} exitCode: result code (0: OK)
+ * - {String} keyData: ASCII armored key data material
+ * - {String} errorMsg: error message in case exitCode !== 0
+ */
+ getSecretKey(minimalKey) {
+ const cApi = lazy.EnigmailCryptoAPI();
+ return cApi.sync(cApi.extractSecretKey(this.fpr, minimalKey));
+ }
+
+ iSimpleOneSubkeySameExpiry() {
+ if (this.subKeys.length == 0) {
+ return true;
+ }
+
+ if (this.subKeys.length > 1) {
+ return false;
+ }
+
+ let subKey = this.subKeys[0];
+
+ if (!this.expiryTime && !subKey.expiryTime) {
+ return true;
+ }
+
+ let deltaSeconds = this.expiryTime - subKey.expiryTime;
+ if (deltaSeconds < 0) {
+ deltaSeconds *= -1;
+ }
+
+ // If expiry dates differ by less than a half day, then we
+ // treat it as having roughly the same expiry date.
+ return deltaSeconds < 12 * 60 * 60;
+ }
+
+ /**
+ * Obtain the list of alternative email addresses, except the one
+ * that is given as the parameter.
+ *
+ * @param {boolean} exceptThisEmail - an email address that will
+ * be excluded in the result array.
+ * @returns {string[]} - an array of all email addresses found in all
+ * of the key's user IDs, excluding exceptThisEmail.
+ */
+ getAlternativeEmails(exceptThisEmail) {
+ let result = [];
+
+ for (let u of this.userIds) {
+ let email;
+ try {
+ email = lazy.EnigmailFuncs.stripEmail(u.userId.toLowerCase());
+ } catch (x) {
+ email = u.userId.toLowerCase();
+ }
+
+ if (email == exceptThisEmail) {
+ continue;
+ }
+
+ result.push(email);
+ }
+
+ return result;
+ }
+
+ getUserIdWithEmail(email) {
+ for (let u of this.userIds) {
+ let e;
+ try {
+ e = lazy.EnigmailFuncs.stripEmail(u.userId.toLowerCase());
+ } catch (x) {
+ e = u.userId.toLowerCase();
+ }
+
+ if (email == e) {
+ return u;
+ }
+ }
+
+ return null;
+ }
+}
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);
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/keyserver.jsm b/comm/mail/extensions/openpgp/content/modules/keyserver.jsm
new file mode 100644
index 0000000000..a2c66ade63
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/keyserver.jsm
@@ -0,0 +1,1549 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailKeyServer"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ FeedUtils: "resource:///modules/FeedUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+const ENIG_DEFAULT_HKP_PORT = "11371";
+const ENIG_DEFAULT_HKPS_PORT = "443";
+const ENIG_DEFAULT_LDAP_PORT = "389";
+
+/**
+ KeySrvListener API
+ Object implementing:
+ - onProgress: function(percentComplete) [only implemented for download()]
+ - onCancel: function() - the body will be set by the callee
+*/
+
+function createError(errId) {
+ let msg = "";
+
+ switch (errId) {
+ case lazy.EnigmailConstants.KEYSERVER_ERR_ABORTED:
+ msg = lazy.l10n.formatValueSync("keyserver-error-aborted");
+ break;
+ case lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR:
+ msg = lazy.l10n.formatValueSync("keyserver-error-server-error");
+ break;
+ case lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE:
+ msg = lazy.l10n.formatValueSync("keyserver-error-unavailable");
+ break;
+ case lazy.EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR:
+ msg = lazy.l10n.formatValueSync("keyserver-error-security-error");
+ break;
+ case lazy.EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR:
+ msg = lazy.l10n.formatValueSync("keyserver-error-certificate-error");
+ break;
+ case lazy.EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR:
+ msg = lazy.l10n.formatValueSync("keyserver-error-import-error");
+ break;
+ case lazy.EnigmailConstants.KEYSERVER_ERR_UNKNOWN:
+ msg = lazy.l10n.formatValueSync("keyserver-error-unknown");
+ break;
+ }
+
+ return {
+ result: errId,
+ errorDetails: msg,
+ };
+}
+
+/**
+ * parse a keyserver specification and return host, protocol and port
+ *
+ * @param keyserver: String - name of keyserver with optional protocol and port.
+ * E.g. keys.gnupg.net, hkps://keys.gnupg.net:443
+ *
+ * @returns Object: {port, host, protocol} (all Strings)
+ */
+function parseKeyserverUrl(keyserver) {
+ if (keyserver.length > 1024) {
+ // insane length of keyserver is forbidden
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ keyserver = keyserver.toLowerCase().trim();
+ let protocol = "";
+ if (keyserver.search(/^[a-zA-Z0-9_.-]+:\/\//) === 0) {
+ protocol = keyserver.replace(/^([a-zA-Z0-9_.-]+)(:\/\/.*)/, "$1");
+ keyserver = keyserver.replace(/^[a-zA-Z0-9_.-]+:\/\//, "");
+ } else {
+ protocol = "hkp";
+ }
+
+ let port = "";
+ switch (protocol) {
+ case "hkp":
+ port = ENIG_DEFAULT_HKP_PORT;
+ break;
+ case "https":
+ case "hkps":
+ port = ENIG_DEFAULT_HKPS_PORT;
+ break;
+ case "ldap":
+ port = ENIG_DEFAULT_LDAP_PORT;
+ break;
+ }
+
+ let m = keyserver.match(/^(.+)(:)(\d+)$/);
+ if (m && m.length == 4) {
+ keyserver = m[1];
+ port = m[3];
+ }
+
+ if (keyserver.search(/^(keys\.mailvelope\.com|api\.protonmail\.ch)$/) === 0) {
+ protocol = "hkps";
+ port = ENIG_DEFAULT_HKPS_PORT;
+ }
+ if (keyserver.search(/^(keybase\.io)$/) === 0) {
+ protocol = "keybase";
+ port = ENIG_DEFAULT_HKPS_PORT;
+ }
+
+ return {
+ protocol,
+ host: keyserver,
+ port,
+ };
+}
+
+/**
+ Object to handle HKP/HKPS requests via builtin XMLHttpRequest()
+ */
+const accessHkpInternal = {
+ /**
+ * Create the payload of hkp requests (upload only)
+ *
+ */
+ async buildHkpPayload(actionFlag, searchTerms) {
+ switch (actionFlag) {
+ case lazy.EnigmailConstants.UPLOAD_KEY:
+ let exitCodeObj = {};
+ let keyData = await lazy.EnigmailKeyRing.extractPublicKeys(
+ ["0x" + searchTerms], // TODO: confirm input is ID or fingerprint
+ null,
+ null,
+ null,
+ exitCodeObj,
+ {}
+ );
+ if (exitCodeObj.value !== 0 || keyData.length === 0) {
+ return null;
+ }
+ return 'keytext="' + encodeURIComponent(keyData) + '"';
+
+ case lazy.EnigmailConstants.DOWNLOAD_KEY:
+ case lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
+ case lazy.EnigmailConstants.SEARCH_KEY:
+ return "";
+ }
+
+ // other actions are not yet implemented
+ return null;
+ },
+
+ /**
+ * return the URL and the HTTP access method for a given action
+ */
+ createRequestUrl(keyserver, actionFlag, searchTerm) {
+ let keySrv = parseKeyserverUrl(keyserver);
+
+ let method = "GET";
+ let protocol;
+
+ switch (keySrv.protocol) {
+ case "hkp":
+ protocol = "http";
+ break;
+ case "ldap":
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ default:
+ // equals to hkps
+ protocol = "https";
+ }
+
+ let url = protocol + "://" + keySrv.host + ":" + keySrv.port;
+
+ if (actionFlag === lazy.EnigmailConstants.UPLOAD_KEY) {
+ url += "/pks/add";
+ method = "POST";
+ } else if (
+ actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY ||
+ actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT
+ ) {
+ if (searchTerm.indexOf("0x") !== 0) {
+ searchTerm = "0x" + searchTerm;
+ }
+ url += "/pks/lookup?search=" + searchTerm + "&op=get&options=mr";
+ } else if (actionFlag === lazy.EnigmailConstants.SEARCH_KEY) {
+ url +=
+ "/pks/lookup?search=" +
+ escape(searchTerm) +
+ "&fingerprint=on&op=index&options=mr&exact=on";
+ }
+
+ return {
+ url,
+ host: keySrv.host,
+ method,
+ };
+ },
+
+ /**
+ * Upload, search or download keys from a keyserver
+ *
+ * @param actionFlag: Number - Keyserver Action Flags: from EnigmailConstants
+ * @param keyId: String - space-separated list of search terms or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Number (Status-ID)>
+ */
+ async accessKeyServer(actionFlag, keyserver, keyId, listener) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessHkpInternal.accessKeyServer(${keyserver})\n`
+ );
+ if (!keyserver) {
+ throw new Error("accessKeyServer requires explicit keyserver parameter");
+ }
+
+ let payLoad = await this.buildHkpPayload(actionFlag, keyId);
+
+ return new Promise((resolve, reject) => {
+ let xmlReq = null;
+ if (listener && typeof listener === "object") {
+ listener.onCancel = function () {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessHkpInternal.accessKeyServer - onCancel() called\n`
+ );
+ if (xmlReq) {
+ xmlReq.abort();
+ }
+ reject(createError(lazy.EnigmailConstants.KEYSERVER_ERR_ABORTED));
+ };
+ }
+ if (actionFlag === lazy.EnigmailConstants.REFRESH_KEY) {
+ // we don't (need to) distinguish between refresh and download for our internal protocol
+ actionFlag = lazy.EnigmailConstants.DOWNLOAD_KEY;
+ }
+
+ if (payLoad === null) {
+ reject(createError(lazy.EnigmailConstants.KEYSERVER_ERR_UNKNOWN));
+ return;
+ }
+
+ xmlReq = new XMLHttpRequest();
+
+ xmlReq.onload = function () {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessHkpInternal: onload(): status=" +
+ xmlReq.status +
+ "\n"
+ );
+ switch (actionFlag) {
+ case lazy.EnigmailConstants.UPLOAD_KEY:
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessHkpInternal: onload: " +
+ xmlReq.responseText +
+ "\n"
+ );
+ if (xmlReq.status >= 400) {
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ resolve(0);
+ }
+ return;
+
+ case lazy.EnigmailConstants.SEARCH_KEY:
+ if (xmlReq.status === 404) {
+ // key not found
+ resolve("");
+ } else if (xmlReq.status >= 400) {
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ resolve(xmlReq.responseText);
+ }
+ return;
+
+ case lazy.EnigmailConstants.DOWNLOAD_KEY:
+ case lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
+ if (xmlReq.status >= 400 && xmlReq.status < 500) {
+ // key not found
+ resolve(1);
+ } else if (xmlReq.status >= 500) {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessHkpInternal: onload: " +
+ xmlReq.responseText +
+ "\n"
+ );
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ let errorMsgObj = {},
+ importedKeysObj = {};
+
+ if (actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY) {
+ let importMinimal = false;
+ let r = lazy.EnigmailKeyRing.importKey(
+ null,
+ false,
+ xmlReq.responseText,
+ false,
+ "",
+ errorMsgObj,
+ importedKeysObj,
+ importMinimal
+ );
+ if (r === 0) {
+ resolve(importedKeysObj.value);
+ } else {
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR
+ )
+ );
+ }
+ } else {
+ // DOWNLOAD_KEY_NO_IMPORT
+ resolve(xmlReq.responseText);
+ }
+ }
+ return;
+ }
+ resolve(-1);
+ };
+
+ xmlReq.onerror = function (e) {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessHkpInternal.accessKeyServer: onerror: " +
+ e +
+ "\n"
+ );
+ let err = lazy.FeedUtils.createTCPErrorFromFailedXHR(e.target);
+ switch (err.type) {
+ case "SecurityCertificate":
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR
+ )
+ );
+ break;
+ case "SecurityProtocol":
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR)
+ );
+ break;
+ case "Network":
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE
+ )
+ );
+ break;
+ }
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE)
+ );
+ };
+
+ xmlReq.onloadend = function () {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessHkpInternal.accessKeyServer: loadEnd\n"
+ );
+ };
+
+ let { url, method } = this.createRequestUrl(keyserver, actionFlag, keyId);
+
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessHkpInternal.accessKeyServer: requesting ${url}\n`
+ );
+ xmlReq.open(method, url);
+ xmlReq.send(payLoad);
+ });
+ },
+
+ /**
+ * Download keys from a keyserver
+ *
+ * @param keyIDs: String - space-separated list of search terms or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<...>
+ */
+ async download(autoImport, keyIDs, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessHkpInternal.download(${keyIDs})\n`
+ );
+ let keyIdArr = keyIDs.split(/ +/);
+ let retObj = {
+ result: 0,
+ errorDetails: "",
+ keyList: [],
+ };
+
+ for (let i = 0; i < keyIdArr.length; i++) {
+ try {
+ let r = await this.accessKeyServer(
+ autoImport
+ ? lazy.EnigmailConstants.DOWNLOAD_KEY
+ : lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
+ keyserver,
+ keyIdArr[i],
+ listener
+ );
+ if (autoImport) {
+ if (Array.isArray(r)) {
+ retObj.keyList = retObj.keyList.concat(r);
+ }
+ } else if (typeof r == "string") {
+ retObj.keyData = r;
+ } else {
+ retObj.result = r;
+ }
+ } catch (ex) {
+ retObj.result = ex.result;
+ retObj.errorDetails = ex.errorDetails;
+ throw retObj;
+ }
+
+ if (listener && "onProgress" in listener) {
+ listener.onProgress(((i + 1) / keyIdArr.length) * 100);
+ }
+ }
+
+ return retObj;
+ },
+
+ refresh(keyServer, listener = null) {
+ let keyList = lazy.EnigmailKeyRing.getAllKeys()
+ .keyList.map(keyObj => {
+ return "0x" + keyObj.fpr;
+ })
+ .join(" ");
+
+ return this.download(true, keyList, keyServer, listener);
+ },
+
+ /**
+ * Upload keys to a keyserver
+ *
+ * @param keyIDs: String - space-separated list of search terms or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return {boolean} - Returns true if the key was sent successfully
+ */
+ async upload(keyIDs, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessHkpInternal.upload(${keyIDs})\n`
+ );
+ let keyIdArr = keyIDs.split(/ +/);
+ let rv = false;
+
+ for (let i = 0; i < keyIdArr.length; i++) {
+ try {
+ let r = await this.accessKeyServer(
+ lazy.EnigmailConstants.UPLOAD_KEY,
+ keyserver,
+ keyIdArr[i],
+ listener
+ );
+ if (r === 0) {
+ rv = true;
+ } else {
+ rv = false;
+ break;
+ }
+ } catch (ex) {
+ console.log(ex.errorDetails);
+ rv = false;
+ break;
+ }
+
+ if (listener && "onProgress" in listener) {
+ listener.onProgress(((i + 1) / keyIdArr.length) * 100);
+ }
+ }
+
+ return rv;
+ },
+
+ /**
+ * Search for keys on a keyserver
+ *
+ * @param searchTerm: String - search term
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Object>
+ * - result: Number
+ * - pubKeys: Array of Object:
+ * PubKeys: Object with:
+ * - keyId: String
+ * - keyLen: String
+ * - keyType: String
+ * - created: String (YYYY-MM-DD)
+ * - status: String: one of ''=valid, r=revoked, e=expired
+ * - uid: Array of Strings with UIDs
+ */
+ async searchKeyserver(searchTerm, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessHkpInternal.search(${searchTerm})\n`
+ );
+ let retObj = {
+ result: 0,
+ errorDetails: "",
+ pubKeys: [],
+ };
+ let key = null;
+
+ let searchArr = searchTerm.split(/ +/);
+
+ for (let k in searchArr) {
+ let r = await this.accessKeyServer(
+ lazy.EnigmailConstants.SEARCH_KEY,
+ keyserver,
+ searchArr[k],
+ listener
+ );
+
+ let lines = r.split(/\r?\n/);
+
+ for (var i = 0; i < lines.length; i++) {
+ let line = lines[i].split(/:/).map(unescape);
+ if (line.length <= 1) {
+ continue;
+ }
+
+ switch (line[0]) {
+ case "info":
+ if (line[1] !== "1") {
+ // protocol version not supported
+ retObj.result = 7;
+ retObj.errorDetails = await lazy.l10n.formatValue(
+ "keyserver-error-unsupported"
+ );
+ retObj.pubKeys = [];
+ return retObj;
+ }
+ break;
+ case "pub":
+ if (line.length >= 6) {
+ if (key) {
+ retObj.pubKeys.push(key);
+ key = null;
+ }
+ let dat = new Date(line[4] * 1000);
+ let month = String(dat.getMonth() + 101).substr(1);
+ let day = String(dat.getDate() + 100).substr(1);
+ key = {
+ keyId: line[1],
+ keyLen: line[3],
+ keyType: line[2],
+ created: dat.getFullYear() + "-" + month + "-" + day,
+ uid: [],
+ status: line[6],
+ };
+ }
+ break;
+ case "uid":
+ key.uid.push(
+ lazy.EnigmailData.convertToUnicode(line[1].trim(), "utf-8")
+ );
+ }
+ }
+
+ if (key) {
+ retObj.pubKeys.push(key);
+ }
+ }
+
+ return retObj;
+ },
+};
+
+/**
+ Object to handle KeyBase requests (search & download only)
+ */
+const accessKeyBase = {
+ /**
+ * return the URL and the HTTP access method for a given action
+ */
+ createRequestUrl(actionFlag, searchTerm) {
+ let url = "https://keybase.io/_/api/1.0/user/";
+
+ if (actionFlag === lazy.EnigmailConstants.UPLOAD_KEY) {
+ // not supported
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ } else if (
+ actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY ||
+ actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT
+ ) {
+ if (searchTerm.indexOf("0x") === 0) {
+ searchTerm = searchTerm.substr(0, 40);
+ }
+ url +=
+ "lookup.json?key_fingerprint=" +
+ escape(searchTerm) +
+ "&fields=public_keys";
+ } else if (actionFlag === lazy.EnigmailConstants.SEARCH_KEY) {
+ url += "autocomplete.json?q=" + escape(searchTerm);
+ }
+
+ return {
+ url,
+ method: "GET",
+ };
+ },
+
+ /**
+ * Upload, search or download keys from a keyserver
+ *
+ * @param actionFlag: Number - Keyserver Action Flags: from EnigmailConstants
+ * @param keyId: String - space-separated list of search terms or key IDs
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Number (Status-ID)>
+ */
+ async accessKeyServer(actionFlag, keyId, listener) {
+ lazy.EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: accessKeyServer()\n`);
+
+ return new Promise((resolve, reject) => {
+ let xmlReq = null;
+ if (listener && typeof listener === "object") {
+ listener.onCancel = function () {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessKeyBase: accessKeyServer - onCancel() called\n`
+ );
+ if (xmlReq) {
+ xmlReq.abort();
+ }
+ reject(createError(lazy.EnigmailConstants.KEYSERVER_ERR_ABORTED));
+ };
+ }
+ if (actionFlag === lazy.EnigmailConstants.REFRESH_KEY) {
+ // we don't (need to) distinguish between refresh and download for our internal protocol
+ actionFlag = lazy.EnigmailConstants.DOWNLOAD_KEY;
+ }
+
+ xmlReq = new XMLHttpRequest();
+
+ xmlReq.onload = function () {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: onload(): status=" + xmlReq.status + "\n"
+ );
+ switch (actionFlag) {
+ case lazy.EnigmailConstants.SEARCH_KEY:
+ if (xmlReq.status >= 400) {
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ resolve(xmlReq.responseText);
+ }
+ return;
+
+ case lazy.EnigmailConstants.DOWNLOAD_KEY:
+ case lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
+ if (xmlReq.status >= 400 && xmlReq.status < 500) {
+ // key not found
+ resolve(1);
+ } else if (xmlReq.status >= 500) {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: onload: " + xmlReq.responseText + "\n"
+ );
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ try {
+ let resp = JSON.parse(xmlReq.responseText);
+ if (resp.status.code === 0) {
+ for (let hit in resp.them) {
+ lazy.EnigmailLog.DEBUG(
+ JSON.stringify(resp.them[hit].public_keys.primary) + "\n"
+ );
+
+ if (resp.them[hit] !== null) {
+ let errorMsgObj = {},
+ importedKeysObj = {};
+
+ if (actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY) {
+ let r = lazy.EnigmailKeyRing.importKey(
+ null,
+ false,
+ resp.them[hit].public_keys.primary.bundle,
+ false,
+ "",
+ errorMsgObj,
+ importedKeysObj
+ );
+ if (r === 0) {
+ resolve(importedKeysObj.value);
+ } else {
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR
+ )
+ );
+ }
+ } else {
+ // DOWNLOAD_KEY_NO_IMPORT
+ resolve(resp.them[hit].public_keys.primary.bundle);
+ }
+ }
+ }
+ }
+ } catch (ex) {
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_UNKNOWN)
+ );
+ }
+ }
+ return;
+ }
+ resolve(-1);
+ };
+
+ xmlReq.onerror = function (e) {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessKeyBase: onerror: " + e + "\n"
+ );
+ let err = lazy.FeedUtils.createTCPErrorFromFailedXHR(e.target);
+ switch (err.type) {
+ case "SecurityCertificate":
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR
+ )
+ );
+ break;
+ case "SecurityProtocol":
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR)
+ );
+ break;
+ case "Network":
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE
+ )
+ );
+ break;
+ }
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE)
+ );
+ };
+
+ xmlReq.onloadend = function () {
+ lazy.EnigmailLog.DEBUG("keyserver.jsm: accessKeyBase: loadEnd\n");
+ };
+
+ let { url, method } = this.createRequestUrl(actionFlag, keyId);
+
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessKeyBase: requesting ${url}\n`
+ );
+ xmlReq.open(method, url);
+ xmlReq.send("");
+ });
+ },
+
+ /**
+ * Download keys from a KeyBase
+ *
+ * @param keyIDs: String - space-separated list of search terms or key IDs
+ * @param keyserver: (not used for keybase)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<...>
+ */
+ async download(autoImport, keyIDs, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: download()\n`);
+ let keyIdArr = keyIDs.split(/ +/);
+ let retObj = {
+ result: 0,
+ errorDetails: "",
+ keyList: [],
+ };
+
+ for (let i = 0; i < keyIdArr.length; i++) {
+ try {
+ let r = await this.accessKeyServer(
+ autoImport
+ ? lazy.EnigmailConstants.DOWNLOAD_KEY
+ : lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
+ keyIdArr[i],
+ listener
+ );
+ if (r.length > 0) {
+ retObj.keyList = retObj.keyList.concat(r);
+ }
+ } catch (ex) {
+ retObj.result = ex.result;
+ retObj.errorDetails = ex.result;
+ throw retObj;
+ }
+
+ if (listener && "onProgress" in listener) {
+ listener.onProgress(i / keyIdArr.length);
+ }
+ }
+
+ return retObj;
+ },
+
+ /**
+ * Search for keys on a keyserver
+ *
+ * @param searchTerm: String - search term
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Object>
+ * - result: Number
+ * - pubKeys: Array of Object:
+ * PubKeys: Object with:
+ * - keyId: String
+ * - keyLen: String
+ * - keyType: String
+ * - created: String (YYYY-MM-DD)
+ * - status: String: one of ''=valid, r=revoked, e=expired
+ * - uid: Array of Strings with UIDs
+
+ */
+ async searchKeyserver(searchTerm, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: search()\n`);
+ let retObj = {
+ result: 0,
+ errorDetails: "",
+ pubKeys: [],
+ };
+
+ try {
+ let r = await this.accessKeyServer(
+ lazy.EnigmailConstants.SEARCH_KEY,
+ searchTerm,
+ listener
+ );
+
+ let res = JSON.parse(r);
+ let completions = res.completions;
+
+ for (let hit in completions) {
+ if (
+ completions[hit] &&
+ completions[hit].components.key_fingerprint !== undefined
+ ) {
+ let uid = completions[hit].components.username.val;
+ if ("full_name" in completions[hit].components) {
+ uid += " (" + completions[hit].components.full_name.val + ")";
+ }
+ let key = {
+ keyId:
+ completions[hit].components.key_fingerprint.val.toUpperCase(),
+ keyLen:
+ completions[hit].components.key_fingerprint.nbits.toString(),
+ keyType:
+ completions[hit].components.key_fingerprint.algo.toString(),
+ created: 0, //date.toDateString(),
+ uid: [uid],
+ status: "",
+ };
+ retObj.pubKeys.push(key);
+ }
+ }
+ } catch (ex) {
+ retObj.result = ex.result;
+ retObj.errorDetails = ex.errorDetails;
+ throw retObj;
+ }
+
+ return retObj;
+ },
+
+ upload() {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ },
+
+ refresh(keyServer, listener = null) {
+ lazy.EnigmailLog.DEBUG(`keyserver.jsm: accessKeyBase: refresh()\n`);
+ let keyList = lazy.EnigmailKeyRing.getAllKeys()
+ .keyList.map(keyObj => {
+ return "0x" + keyObj.fpr;
+ })
+ .join(" ");
+
+ return this.download(true, keyList, keyServer, listener);
+ },
+};
+
+function getAccessType(keyserver) {
+ if (!keyserver) {
+ throw new Error("getAccessType requires explicit keyserver parameter");
+ }
+
+ let srv = parseKeyserverUrl(keyserver);
+ switch (srv.protocol) {
+ case "keybase":
+ return accessKeyBase;
+ case "vks":
+ return accessVksServer;
+ }
+
+ if (srv.host.search(/keys.openpgp.org$/i) >= 0) {
+ return accessVksServer;
+ }
+
+ return accessHkpInternal;
+}
+
+/**
+ Object to handle VKS requests (for example keys.openpgp.org)
+ */
+const accessVksServer = {
+ /**
+ * Create the payload of VKS requests (currently upload only)
+ *
+ */
+ async buildJsonPayload(actionFlag, searchTerms, locale) {
+ switch (actionFlag) {
+ case lazy.EnigmailConstants.UPLOAD_KEY:
+ let exitCodeObj = {};
+ let keyData = await lazy.EnigmailKeyRing.extractPublicKeys(
+ ["0x" + searchTerms], // must be id or fingerprint
+ null,
+ null,
+ null,
+ exitCodeObj,
+ {}
+ );
+ if (exitCodeObj.value !== 0 || keyData.length === 0) {
+ return null;
+ }
+
+ return JSON.stringify({
+ keytext: keyData,
+ });
+
+ case lazy.EnigmailConstants.GET_CONFIRMATION_LINK:
+ return JSON.stringify({
+ token: searchTerms.token,
+ addresses: searchTerms.addresses,
+ locale: [locale],
+ });
+
+ case lazy.EnigmailConstants.DOWNLOAD_KEY:
+ case lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
+ case lazy.EnigmailConstants.SEARCH_KEY:
+ return "";
+ }
+
+ // other actions are not yet implemented
+ return null;
+ },
+
+ /**
+ * return the URL and the HTTP access method for a given action
+ */
+ createRequestUrl(keyserver, actionFlag, searchTerm) {
+ let keySrv = parseKeyserverUrl(keyserver);
+ let contentType = "text/plain;charset=UTF-8";
+
+ let method = "GET";
+
+ let url = "https://" + keySrv.host;
+
+ if (actionFlag === lazy.EnigmailConstants.UPLOAD_KEY) {
+ url += "/vks/v1/upload";
+ method = "POST";
+ contentType = "application/json";
+ } else if (actionFlag === lazy.EnigmailConstants.GET_CONFIRMATION_LINK) {
+ url += "/vks/v1/request-verify";
+ method = "POST";
+ contentType = "application/json";
+ } else if (
+ actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY ||
+ actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT ||
+ actionFlag === lazy.EnigmailConstants.SEARCH_KEY
+ ) {
+ if (searchTerm) {
+ let lookup = "/vks/";
+ if (searchTerm.indexOf("0x") === 0) {
+ searchTerm = searchTerm.substr(2);
+ if (
+ searchTerm.length == 16 &&
+ searchTerm.search(/^[A-F0-9]+$/) === 0
+ ) {
+ lookup = "/vks/v1/by-keyid/" + searchTerm;
+ } else if (
+ searchTerm.length == 40 &&
+ searchTerm.search(/^[A-F0-9]+$/) === 0
+ ) {
+ lookup = "/vks/v1/by-fingerprint/" + searchTerm;
+ }
+ } else {
+ try {
+ searchTerm = lazy.EnigmailFuncs.stripEmail(searchTerm);
+ } catch (x) {}
+ lookup = "/vks/v1/by-email/" + searchTerm;
+ }
+ url += lookup;
+ }
+ }
+
+ return {
+ url,
+ method,
+ contentType,
+ };
+ },
+
+ /**
+ * Upload, search or download keys from a keyserver
+ *
+ * @param actionFlag: Number - Keyserver Action Flags: from EnigmailConstants
+ * @param keyId: String - space-separated list of search terms or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Number (Status-ID)>
+ */
+ async accessKeyServer(actionFlag, keyserver, keyId, listener) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.accessKeyServer(${keyserver})\n`
+ );
+ if (keyserver === null) {
+ keyserver = "keys.openpgp.org";
+ }
+
+ let uiLocale = Services.locale.appLocalesAsBCP47[0];
+ let payLoad = await this.buildJsonPayload(actionFlag, keyId, uiLocale);
+
+ return new Promise((resolve, reject) => {
+ let xmlReq = null;
+ if (listener && typeof listener === "object") {
+ listener.onCancel = function () {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.accessKeyServer - onCancel() called\n`
+ );
+ if (xmlReq) {
+ xmlReq.abort();
+ }
+ reject(createError(lazy.EnigmailConstants.KEYSERVER_ERR_ABORTED));
+ };
+ }
+ if (actionFlag === lazy.EnigmailConstants.REFRESH_KEY) {
+ // we don't (need to) distinguish between refresh and download for our internal protocol
+ actionFlag = lazy.EnigmailConstants.DOWNLOAD_KEY;
+ }
+
+ if (payLoad === null) {
+ reject(createError(lazy.EnigmailConstants.KEYSERVER_ERR_UNKNOWN));
+ return;
+ }
+
+ xmlReq = new XMLHttpRequest();
+
+ xmlReq.onload = function () {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessVksServer.onload(): status=" +
+ xmlReq.status +
+ "\n"
+ );
+ switch (actionFlag) {
+ case lazy.EnigmailConstants.UPLOAD_KEY:
+ case lazy.EnigmailConstants.GET_CONFIRMATION_LINK:
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessVksServer.onload: " +
+ xmlReq.responseText +
+ "\n"
+ );
+ if (xmlReq.status >= 400) {
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ resolve(xmlReq.responseText);
+ }
+ return;
+
+ case lazy.EnigmailConstants.SEARCH_KEY:
+ if (xmlReq.status === 404) {
+ // key not found
+ resolve("");
+ } else if (xmlReq.status >= 400) {
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ resolve(xmlReq.responseText);
+ }
+ return;
+
+ case lazy.EnigmailConstants.DOWNLOAD_KEY:
+ case lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT:
+ if (xmlReq.status >= 400 && xmlReq.status < 500) {
+ // key not found
+ resolve(1);
+ } else if (xmlReq.status >= 500) {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessVksServer.onload: " +
+ xmlReq.responseText +
+ "\n"
+ );
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_ERROR)
+ );
+ } else {
+ let errorMsgObj = {},
+ importedKeysObj = {};
+ if (actionFlag === lazy.EnigmailConstants.DOWNLOAD_KEY) {
+ let r = lazy.EnigmailKeyRing.importKey(
+ null,
+ false,
+ xmlReq.responseText,
+ false,
+ "",
+ errorMsgObj,
+ importedKeysObj
+ );
+ if (r === 0) {
+ resolve(importedKeysObj.value);
+ } else {
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_IMPORT_ERROR
+ )
+ );
+ }
+ } else {
+ // DOWNLOAD_KEY_NO_IMPORT
+ resolve(xmlReq.responseText);
+ }
+ }
+ return;
+ }
+ resolve(-1);
+ };
+
+ xmlReq.onerror = function (e) {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessVksServer.accessKeyServer: onerror: " + e + "\n"
+ );
+ let err = lazy.FeedUtils.createTCPErrorFromFailedXHR(e.target);
+ switch (err.type) {
+ case "SecurityCertificate":
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_CERTIFICATE_ERROR
+ )
+ );
+ break;
+ case "SecurityProtocol":
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SECURITY_ERROR)
+ );
+ break;
+ case "Network":
+ reject(
+ createError(
+ lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE
+ )
+ );
+ break;
+ }
+ reject(
+ createError(lazy.EnigmailConstants.KEYSERVER_ERR_SERVER_UNAVAILABLE)
+ );
+ };
+
+ xmlReq.onloadend = function () {
+ lazy.EnigmailLog.DEBUG(
+ "keyserver.jsm: accessVksServer.accessKeyServer: loadEnd\n"
+ );
+ };
+
+ let { url, method, contentType } = this.createRequestUrl(
+ keyserver,
+ actionFlag,
+ keyId
+ );
+
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.accessKeyServer: requesting ${method} for ${url}\n`
+ );
+ xmlReq.open(method, url);
+ xmlReq.setRequestHeader("Content-Type", contentType);
+ xmlReq.send(payLoad);
+ });
+ },
+
+ /**
+ * Download keys from a keyserver
+ *
+ * @param keyIDs: String - space-separated list of search terms or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<...>
+ */
+ async download(autoImport, keyIDs, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.download(${keyIDs})\n`
+ );
+ let keyIdArr = keyIDs.split(/ +/);
+ let retObj = {
+ result: 0,
+ errorDetails: "",
+ keyList: [],
+ };
+
+ for (let i = 0; i < keyIdArr.length; i++) {
+ try {
+ let r = await this.accessKeyServer(
+ autoImport
+ ? lazy.EnigmailConstants.DOWNLOAD_KEY
+ : lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
+ keyserver,
+ keyIdArr[i],
+ listener
+ );
+ if (autoImport) {
+ if (Array.isArray(r)) {
+ retObj.keyList = retObj.keyList.concat(r);
+ }
+ } else if (typeof r == "string") {
+ retObj.keyData = r;
+ } else {
+ retObj.result = r;
+ }
+ } catch (ex) {
+ retObj.result = ex.result;
+ retObj.errorDetails = ex.errorDetails;
+ throw retObj;
+ }
+
+ if (listener && "onProgress" in listener) {
+ listener.onProgress(((i + 1) / keyIdArr.length) * 100);
+ }
+ }
+
+ return retObj;
+ },
+
+ refresh(keyServer, listener = null) {
+ let keyList = lazy.EnigmailKeyRing.getAllKeys()
+ .keyList.map(keyObj => {
+ return "0x" + keyObj.fpr;
+ })
+ .join(" ");
+
+ return this.download(true, keyList, keyServer, listener);
+ },
+
+ async requestConfirmationLink(keyserver, jsonFragment) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.requestConfirmationLink()\n`
+ );
+
+ let response = JSON.parse(jsonFragment);
+
+ let addr = [];
+
+ for (let email in response.status) {
+ if (response.status[email] !== "published") {
+ addr.push(email);
+ }
+ }
+
+ if (addr.length > 0) {
+ let r = await this.accessKeyServer(
+ lazy.EnigmailConstants.GET_CONFIRMATION_LINK,
+ keyserver,
+ {
+ token: response.token,
+ addresses: addr,
+ },
+ null
+ );
+
+ if (typeof r === "string") {
+ return addr.length;
+ }
+ }
+
+ return 0;
+ },
+
+ /**
+ * Upload keys to a keyserver
+ *
+ * @param keyIDs: String - space-separated list of search terms or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return {boolean} - Returns true if the key was sent successfully
+ */
+ async upload(keyIDs, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.upload(${keyIDs})\n`
+ );
+ let keyIdArr = keyIDs.split(/ +/);
+ let rv = false;
+
+ for (let i = 0; i < keyIdArr.length; i++) {
+ let keyObj = lazy.EnigmailKeyRing.getKeyById(keyIdArr[i]);
+
+ if (!keyObj.secretAvailable) {
+ throw new Error(
+ "public keyserver uploading supported only for user's own keys"
+ );
+ }
+
+ try {
+ let r = await this.accessKeyServer(
+ lazy.EnigmailConstants.UPLOAD_KEY,
+ keyserver,
+ keyIdArr[i],
+ listener
+ );
+ if (typeof r === "string") {
+ let req = await this.requestConfirmationLink(keyserver, r);
+ if (req >= 0) {
+ rv = true;
+ }
+ } else {
+ rv = false;
+ break;
+ }
+ } catch (ex) {
+ console.log(ex.errorDetails);
+ rv = false;
+ break;
+ }
+
+ if (listener && "onProgress" in listener) {
+ listener.onProgress(((i + 1) / keyIdArr.length) * 100);
+ }
+ }
+
+ return rv;
+ },
+
+ /**
+ * Search for keys on a keyserver
+ *
+ * @param searchTerm: String - search term
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Object>
+ * - result: Number
+ * - pubKeys: Array of Object:
+ * PubKeys: Object with:
+ * - keyId: String
+ * - keyLen: String
+ * - keyType: String
+ * - created: String (YYYY-MM-DD)
+ * - status: String: one of ''=valid, r=revoked, e=expired
+ * - uid: Array of Strings with UIDs
+ */
+ async searchKeyserver(searchTerm, keyserver, listener = null) {
+ lazy.EnigmailLog.DEBUG(
+ `keyserver.jsm: accessVksServer.search(${searchTerm})\n`
+ );
+ let retObj = {
+ result: 0,
+ errorDetails: "",
+ pubKeys: [],
+ };
+ let key = null;
+
+ let searchArr = searchTerm.split(/ +/);
+
+ try {
+ for (let i in searchArr) {
+ let r = await this.accessKeyServer(
+ lazy.EnigmailConstants.SEARCH_KEY,
+ keyserver,
+ searchArr[i],
+ listener
+ );
+
+ const cApi = lazy.EnigmailCryptoAPI();
+ let keyList = await cApi.getKeyListFromKeyBlockAPI(
+ r,
+ true,
+ false,
+ true,
+ false
+ );
+ if (!keyList) {
+ retObj.result = -1;
+ // TODO: should we set retObj.errorDetails to a string?
+ return retObj;
+ }
+
+ for (let k in keyList) {
+ key = {
+ keyId: keyList[k].fpr,
+ keyLen: "0",
+ keyType: "",
+ created: keyList[k].created,
+ uid: [keyList[k].name],
+ status: keyList[k].revoke ? "r" : "",
+ };
+
+ for (let uid of keyList[k].uids) {
+ key.uid.push(uid);
+ }
+
+ retObj.pubKeys.push(key);
+ }
+ }
+ } catch (ex) {
+ retObj.result = ex.result;
+ retObj.errorDetails = ex.errorDetails;
+ throw retObj;
+ }
+
+ return retObj;
+ },
+};
+
+var EnigmailKeyServer = {
+ /**
+ * Download keys from a keyserver
+ *
+ * @param keyIDs: String - space-separated list of FPRs or key IDs
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Object>
+ * Object: - result: Number - result Code (0 = OK),
+ * - keyList: Array of String - imported key FPR
+ */
+ async download(keyIDs, keyserver = null, listener) {
+ let acc = getAccessType(keyserver);
+ return acc.download(true, keyIDs, keyserver, listener);
+ },
+
+ async downloadNoImport(keyIDs, keyserver = null, listener) {
+ let acc = getAccessType(keyserver);
+ return acc.download(false, keyIDs, keyserver, listener);
+ },
+
+ serverReqURL(keyIDs, keyserver) {
+ let acc = getAccessType(keyserver);
+ let { url } = acc.createRequestUrl(
+ keyserver,
+ lazy.EnigmailConstants.DOWNLOAD_KEY_NO_IMPORT,
+ keyIDs
+ );
+ return url;
+ },
+
+ /**
+ * Upload keys to a keyserver
+ *
+ * @param keyIDs: String - space-separated list of key IDs or FPR
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return {boolean} - Returns true if the key was sent successfully
+ */
+
+ async upload(keyIDs, keyserver = null, listener) {
+ let acc = getAccessType(keyserver);
+ return acc.upload(keyIDs, keyserver, listener);
+ },
+
+ /**
+ * Search keys on a keyserver
+ *
+ * @param searchString: String - search term. Multiple email addresses can be search by spaces
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<Object>
+ * - result: Number
+ * - pubKeys: Array of Object:
+ * PubKeys: Object with:
+ * - keyId: String
+ * - keyLen: String
+ * - keyType: String
+ * - created: String (YYYY-MM-DD)
+ * - status: String: one of ''=valid, r=revoked, e=expired
+ * - uid: Array of Strings with UIDs
+ */
+ async searchKeyserver(searchString, keyserver = null, listener) {
+ let acc = getAccessType(keyserver);
+ return acc.search(searchString, keyserver, listener);
+ },
+
+ async searchAndDownloadSingleResultNoImport(
+ searchString,
+ keyserver = null,
+ listener
+ ) {
+ let acc = getAccessType(keyserver);
+ let searchResult = await acc.searchKeyserver(
+ searchString,
+ keyserver,
+ listener
+ );
+ if (searchResult.result != 0 || searchResult.pubKeys.length != 1) {
+ return null;
+ }
+ return this.downloadNoImport(
+ searchResult.pubKeys[0].keyId,
+ keyserver,
+ listener
+ );
+ },
+
+ /**
+ * Refresh all keys
+ *
+ * @param keyserver: String - keyserver URL (optionally incl. protocol)
+ * @param listener: optional Object implementing the KeySrvListener API (above)
+ *
+ * @return: Promise<resultStatus> (identical to download)
+ */
+ refresh(keyserver = null, listener) {
+ let acc = getAccessType(keyserver);
+ return acc.refresh(keyserver, listener);
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/keyserverUris.jsm b/comm/mail/extensions/openpgp/content/modules/keyserverUris.jsm
new file mode 100644
index 0000000000..e3746f730d
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/keyserverUris.jsm
@@ -0,0 +1,43 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailKeyserverURIs"];
+
+function getKeyServers() {
+ let keyservers = Services.prefs
+ .getCharPref("mail.openpgp.keyserver_list")
+ .split(/\s*[,;]\s*/g);
+ return keyservers.filter(
+ ks =>
+ ks.startsWith("vks://") ||
+ ks.startsWith("hkp://") ||
+ ks.startsWith("hkps://")
+ );
+}
+
+function getUploadKeyServer() {
+ let keyservers = Services.prefs
+ .getCharPref("mail.openpgp.keyserver_list")
+ .split(/\s*[,;]\s*/g);
+ for (let ks of keyservers) {
+ if (
+ !ks.startsWith("vks://") &&
+ !ks.startsWith("hkp://") &&
+ !ks.startsWith("hkps://")
+ ) {
+ continue;
+ }
+ return ks;
+ }
+ return null;
+}
+
+var EnigmailKeyserverURIs = {
+ getKeyServers,
+ getUploadKeyServer,
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/log.jsm b/comm/mail/extensions/openpgp/content/modules/log.jsm
new file mode 100644
index 0000000000..5c2829017c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/log.jsm
@@ -0,0 +1,151 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailLog"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var EnigmailLog = {
+ level: 3,
+ directory: null,
+ fileStream: null,
+
+ setLogLevel(newLogLevel) {
+ EnigmailLog.level = newLogLevel;
+ },
+
+ getLogLevel() {
+ return EnigmailLog.level;
+ },
+
+ setLogDirectory(newLogDirectory) {
+ EnigmailLog.directory =
+ newLogDirectory + (AppConstants.platform == "win" ? "\\" : "/");
+ EnigmailLog.createLogFiles();
+ },
+
+ createLogFiles() {
+ if (
+ EnigmailLog.directory &&
+ !EnigmailLog.fileStream &&
+ EnigmailLog.level >= 5
+ ) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(EnigmailLog.directory + "enigdbug.txt");
+ let ofStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ofStream.init(file, -1, -1, 0);
+
+ EnigmailLog.fileStream = ofStream;
+ }
+ },
+
+ onShutdown() {
+ if (EnigmailLog.fileStream) {
+ EnigmailLog.fileStream.close();
+ }
+ EnigmailLog.fileStream = null;
+ },
+
+ WRITE(str) {
+ function withZeroes(val, digits) {
+ return ("0000" + val.toString()).substr(-digits);
+ }
+
+ var d = new Date();
+ var datStr =
+ d.getFullYear() +
+ "-" +
+ withZeroes(d.getMonth() + 1, 2) +
+ "-" +
+ withZeroes(d.getDate(), 2) +
+ " " +
+ withZeroes(d.getHours(), 2) +
+ ":" +
+ withZeroes(d.getMinutes(), 2) +
+ ":" +
+ withZeroes(d.getSeconds(), 2) +
+ "." +
+ withZeroes(d.getMilliseconds(), 3) +
+ " ";
+ if (EnigmailLog.level >= 4) {
+ dump(datStr + str);
+ }
+
+ if (EnigmailLog.fileStream) {
+ EnigmailLog.fileStream.write(datStr, datStr.length);
+ EnigmailLog.fileStream.write(str, str.length);
+ }
+ },
+
+ DEBUG(str) {
+ try {
+ EnigmailLog.WRITE("[DEBUG] " + str);
+ } catch (ex) {}
+ },
+
+ WARNING(str) {
+ EnigmailLog.WRITE("[WARN] " + str);
+ },
+
+ ERROR(str) {
+ try {
+ var consoleSvc = Services.console;
+ var scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptError.init(
+ str,
+ null,
+ null,
+ 0,
+ 0,
+ scriptError.errorFlag,
+ "Enigmail"
+ );
+ consoleSvc.logMessage(scriptError);
+ } catch (ex) {}
+
+ EnigmailLog.WRITE("[ERROR] " + str);
+ },
+
+ CONSOLE(str) {
+ if (EnigmailLog.level >= 3) {
+ EnigmailLog.WRITE("[CONSOLE] " + str);
+ }
+ },
+
+ /**
+ * Log an exception including the stack trace
+ *
+ * referenceInfo: String - arbitrary text to write before the exception is logged
+ * ex: exception object
+ */
+ writeException(referenceInfo, ex) {
+ EnigmailLog.ERROR(
+ referenceInfo +
+ ": caught exception: " +
+ ex.name +
+ "\n" +
+ "Message: '" +
+ ex.message +
+ "'\n" +
+ "File: " +
+ ex.fileName +
+ "\n" +
+ "Line: " +
+ ex.lineNumber +
+ "\n" +
+ "Stack: " +
+ ex.stack +
+ "\n"
+ );
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/masterpass.jsm b/comm/mail/extensions/openpgp/content/modules/masterpass.jsm
new file mode 100644
index 0000000000..49e535ebf7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/masterpass.jsm
@@ -0,0 +1,332 @@
+/*
+ * 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 = ["OpenPGPMasterpass"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ RNP: "chrome://openpgp/content/modules/RNP.jsm",
+});
+
+var OpenPGPMasterpass = {
+ _initDone: false,
+ _sdr: null,
+
+ getSDR() {
+ if (!this._sdr) {
+ try {
+ this._sdr = Cc["@mozilla.org/security/sdr;1"].getService(
+ Ci.nsISecretDecoderRing
+ );
+ } catch (ex) {
+ lazy.EnigmailLog.writeException("masterpass.jsm", ex);
+ }
+ }
+ return this._sdr;
+ },
+
+ filename: "encrypted-openpgp-passphrase.txt",
+ secringFilename: "secring.gpg",
+
+ getPassPath() {
+ let path = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ path.append(this.filename);
+ return path;
+ },
+
+ getSecretKeyRingFile() {
+ let path = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ path.append(this.secringFilename);
+ return path;
+ },
+
+ getOpenPGPSecretRingAlreadyExists() {
+ return this.getSecretKeyRingFile().exists();
+ },
+
+ async _repairOrWarn() {
+ let [prot, unprot] = lazy.RNP.getProtectedKeysCount();
+ let haveAtLeastOneSecretKey = prot || unprot;
+
+ if (
+ !(await IOUtils.exists(this.getPassPath().path)) &&
+ haveAtLeastOneSecretKey
+ ) {
+ // We couldn't read the OpenPGP password from file.
+ // This could either mean the file doesn't exist, which indicates
+ // either a corruption, or the condition after a failed migration
+ // from early Enigmail migrator versions (bug 1656287).
+ // Or it could mean the user has a primary password set,
+ // but the user failed to enter it correctly,
+ // or we are facing the consequences of multiple password prompts.
+
+ let secFileName = this.getSecretKeyRingFile().path;
+ let title = "OpenPGP corruption detected";
+
+ if (prot) {
+ let info;
+ if (!unprot) {
+ info =
+ "Your Thunderbird Profile contains inconsistent or corrupted OpenPGP data. You have secret keys that were previously protected with an automatic passphrase, " +
+ "but file encrypted-openpgp-passphrase.txt is missing. File " +
+ secFileName +
+ " that contains your secret keys cannot be accessed. " +
+ "You must manually repair this corruption by moving the file to a different folder. Then restart, then import your secret keys from a backup. " +
+ "The OpenPGP functionality will be disabled until repaired. ";
+ } else {
+ info =
+ "Your Thunderbird Profile contains inconsistent or corrupted OpenPGP data. You have secret keys that were previously protected with an automatic passphrase, " +
+ "but file encrypted-openpgp-passphrase.txt is missing. File " +
+ secFileName +
+ " contains secret keys cannot be accessed. However, it also contains unprotected keys, which you may continue to access. " +
+ "You must manually repair this corruption by moving the file to a different folder. Then restart, then import your secret keys from a backup. You may also try to import the corrupted file, to import the unprotected keys. " +
+ "The OpenPGP functionality will be disabled until repaired. ";
+ }
+ Services.prompt.alert(null, title, info);
+ throw new Error(
+ "Error, secring.gpg exists, but cannot obtain password from encrypted-openpgp-passphrase.txt"
+ );
+ } else {
+ // only unprotected keys
+ // maybe https://bugzilla.mozilla.org/show_bug.cgi?id=1656287
+ let info =
+ "Your Thunderbird Profile contains inconsistent or corrupted OpenPGP data. You have secret keys, " +
+ "but file encrypted-openpgp-passphrase.txt is missing. " +
+ "If you have recently used Enigmail version 2.2 to migrate your old keys, an incomplete migration is probably the cause of the corruption. " +
+ "An automatic repair can be attempted. " +
+ "The OpenPGP functionality will be disabled until repaired. " +
+ "Before repairing, you should make a backup of file " +
+ secFileName +
+ " that contains your secret keys. " +
+ "After repairing, you may run the Enigmail migration again, or use OpenPGP Key Manager to accept your keys as personal keys.";
+
+ let button = "I confirm I created a backup. Perform automatic repair.";
+
+ let promptFlags =
+ Services.prompt.BUTTON_POS_0 *
+ Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL +
+ Services.prompt.BUTTON_POS_1_DEFAULT;
+
+ let confirm = Services.prompt.confirmEx(
+ null, // window
+ title,
+ info,
+ promptFlags,
+ button,
+ null,
+ null,
+ null,
+ {}
+ );
+
+ if (confirm != 0) {
+ throw new Error(
+ "Error, secring.gpg exists, but cannot obtain password from encrypted-openpgp-passphrase.txt"
+ );
+ }
+
+ await this._ensurePasswordCreatedAndCached();
+ await lazy.RNP.protectUnprotectedKeys();
+ await lazy.RNP.saveKeyRings();
+ }
+ }
+ },
+
+ async _ensurePasswordCreatedAndCached() {
+ if (this.cachedPassword) {
+ return;
+ }
+
+ let sdr = this.getSDR();
+ if (!sdr) {
+ throw new Error("Failed to obtain the SDR service.");
+ }
+
+ if (await IOUtils.exists(this.getPassPath().path)) {
+ let encryptedPass = await IOUtils.readUTF8(this.getPassPath().path);
+ encryptedPass = encryptedPass.trim();
+ if (!encryptedPass) {
+ throw new Error(
+ "Failed to obtain encrypted password data from file " +
+ this.getPassPath().path
+ );
+ }
+
+ try {
+ this.cachedPassword = sdr.decryptString(encryptedPass);
+ // This is the success scenario, in which we return early.
+ return;
+ } catch (e) {
+ // This code handles the corruption described in bug 1790610.
+
+ // Failure to decrypt should be the only scenario that
+ // reaches this code path.
+
+ // Is a primary password set?
+ let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
+ Ci.nsIPK11TokenDB
+ );
+ let token = tokenDB.getInternalKeyToken();
+ if (token.hasPassword && !token.isLoggedIn()) {
+ // Yes, primary password is set, but user is not logged in.
+ // Let's throw now, a future action will result in trying again.
+ throw e;
+ }
+
+ // No. We have profile corruption: key4.db doesn't contain the
+ // key to decrypt file encrypted-openpgp-passphrase.txt
+ // Move to backup file and create a fresh file to fix the situation.
+
+ let backup = await IOUtils.createUniqueFile(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ this.filename + ".corrupt"
+ );
+
+ try {
+ await IOUtils.move(this.getPassPath().path, backup);
+ console.warn(
+ `${this.filename} corruption fixed. Corrupted file moved to ${backup}`
+ );
+ } catch (e2) {
+ console.warn(
+ `Cannot move corrupted file ${this.filename} to backup name ${backup}`
+ );
+ // We cannot repair, so restarting doesn't help, keep running,
+ // and hope the user notices this error in console.
+ throw e2;
+ }
+
+ let secRingFile = this.getSecretKeyRingFile();
+ if (secRingFile.exists() && secRingFile.fileSize > 0) {
+ // We have secret keys that can no longer be accessed.
+
+ try {
+ let backupOld = await IOUtils.createUniqueFile(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ this.secringFilename + ".old.corrupt"
+ );
+ await IOUtils.move(secRingFile.path + ".old", backupOld);
+ } catch (eOld) {}
+
+ let backup2 = await IOUtils.createUniqueFile(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ this.secringFilename + ".corrupt"
+ );
+
+ try {
+ await IOUtils.move(secRingFile.path, backup2);
+ console.warn(
+ `secring.gpg corruption fixed. Corrupted file moved to ${backup}`
+ );
+ await IOUtils.write(secRingFile.path, new Uint8Array());
+ } catch (e3) {
+ console.warn(
+ `Cannot move corrupted file ${this.filename} to backup name ${backup}`
+ );
+ // We cannot repair, so restarting doesn't help, keep running,
+ // and hope the user notices this error in console.
+ throw e3;
+ }
+
+ // RNP might have already read the old file, we cannot easily
+ // trigger rereading of the file, so let's restart.
+ lazy.MailUtils.restartApplication();
+ return;
+ }
+
+ // If we arrive here, we have successfully repaired, and
+ // can proceed with the code below to create a fresh file.
+ }
+ }
+
+ if (await IOUtils.exists(this.getPassPath().path)) {
+ // This check is an additional precaution, to prevent against
+ // logic errors, or unexpected filesystem behavior.
+ // If this file already exists, we MUST NOT create it again.
+ // The code below is executed if the file does not exist yet,
+ // or if the file was deleted or moved, after automatic repairing.
+ throw new Error("File " + this.getPassPath().path + " already exists");
+ }
+
+ // Make sure we don't use the new password unless we're successful
+ // in encrypting and storing it to disk.
+ // (This may fail if the user has a primary password set,
+ // but refuses to enter it.)
+ let newPass = this.generatePassword();
+ let encryptedPass = sdr.encryptString(newPass);
+ if (!encryptedPass) {
+ throw new Error("cannot create OpenPGP password");
+ }
+ await IOUtils.writeUTF8(this.getPassPath().path, encryptedPass);
+
+ this.cachedPassword = newPass;
+ },
+
+ generatePassword() {
+ // TODO: Patrick suggested to replace with
+ // EnigmailRNG.getRandomString(numChars)
+ const random_bytes = new Uint8Array(32);
+ crypto.getRandomValues(random_bytes);
+ let result = "";
+ for (let i = 0; i < 32; i++) {
+ result += (random_bytes[i] % 16).toString(16);
+ }
+ return result;
+ },
+
+ cachedPassword: null,
+
+ // This function requires the password to already exist and be cached.
+ retrieveCachedPassword() {
+ if (!this.cachedPassword) {
+ // Obviously some functionality requires the password, but we
+ // don't have it yet.
+ // The best we can do is spawn reading and caching asynchronously,
+ // this will cause the password to be available once the user
+ // retries the current operation.
+ this.ensurePasswordIsCached();
+ throw new Error("no cached password");
+ }
+ return this.cachedPassword;
+ },
+
+ async ensurePasswordIsCached() {
+ if (this.cachedPassword) {
+ return;
+ }
+
+ if (!this._initDone) {
+ // set flag immediately, to avoid any potential recursion
+ // causing us to repair twice in parallel.
+ this._initDone = true;
+ await this._repairOrWarn();
+ }
+
+ if (this.cachedPassword) {
+ return;
+ }
+
+ await this._ensurePasswordCreatedAndCached();
+ },
+
+ // This function may trigger password creation, if necessary
+ async retrieveOpenPGPPassword() {
+ lazy.EnigmailLog.DEBUG("masterpass.jsm: retrieveMasterPassword()\n");
+
+ await this.ensurePasswordIsCached();
+ return this.cachedPassword;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/mime.jsm b/comm/mail/extensions/openpgp/content/modules/mime.jsm
new file mode 100644
index 0000000000..9b514b9387
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/mime.jsm
@@ -0,0 +1,571 @@
+/*
+ * 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 = ["EnigmailMime"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm",
+ jsmime: "resource:///modules/jsmime.jsm",
+ MsgUtils: "resource:///modules/MimeMessageUtils.jsm",
+});
+
+var EnigmailMime = {
+ /***
+ * create a string of random characters suitable to use for a boundary in a
+ * MIME message following RFC 2045
+ *
+ * @return: string to use as MIME boundary
+ * @see {MimeMultiPart._makePartSeparator}
+ */
+ createBoundary() {
+ return "------------" + lazy.MsgUtils.randomString(24);
+ },
+
+ /***
+ * determine the "boundary" part of a mail content type.
+ *
+ * @contentTypeStr: the string containing all parts of a content-type.
+ * (e.g. multipart/mixed; boundary="xyz") --> returns "xyz"
+ *
+ * @return: String containing the boundary parameter; or ""
+ */
+
+ getBoundary(contentTypeStr) {
+ return EnigmailMime.getParameter(contentTypeStr, "boundary");
+ },
+
+ /***
+ * determine the "protocol" part of a mail content type.
+ *
+ * @contentTypeStr: the string containing all parts of a content-type.
+ * (e.g. multipart/signed; protocol="xyz") --> returns "xyz"
+ *
+ * @return: String containing the protocol parameter; or ""
+ */
+
+ getProtocol(contentTypeStr) {
+ return EnigmailMime.getParameter(contentTypeStr, "protocol");
+ },
+
+ /***
+ * determine an arbitrary "parameter" part of a mail header.
+ *
+ * @param headerStr: the string containing all parts of the header.
+ * @param parameter: the parameter we are looking for
+ *
+ *
+ * 'multipart/signed; protocol="xyz"', 'protocol' --> returns "xyz"
+ *
+ * @return: String containing the parameter; or ""
+ */
+
+ getParameter(headerStr, parameter) {
+ let paramsArr = EnigmailMime.getAllParameters(headerStr);
+ parameter = parameter.toLowerCase();
+ if (parameter in paramsArr) {
+ return paramsArr[parameter];
+ }
+ return "";
+ },
+
+ /***
+ * get all parameter attributes of a mail header.
+ *
+ * @param headerStr: the string containing all parts of the header.
+ *
+ * @return: Array of Object containing the key value pairs
+ *
+ * 'multipart/signed; protocol="xyz"'; boundary="xxx"
+ * --> returns [ ["protocol": "xyz"], ["boundary": "xxx"] ]
+ */
+
+ getAllParameters(headerStr) {
+ headerStr = headerStr.replace(/[\r\n]+[ \t]+/g, "");
+ let hdrMap = lazy.jsmime.headerparser.parseParameterHeader(
+ ";" + headerStr,
+ true,
+ true
+ );
+
+ let paramArr = [];
+ let i = hdrMap.entries();
+ let p = i.next();
+ while (p.value) {
+ paramArr[p.value[0].toLowerCase()] = p.value[1];
+ p = i.next();
+ }
+
+ return paramArr;
+ },
+
+ /***
+ * determine the "charset" part of a mail content type.
+ *
+ * @contentTypeStr: the string containing all parts of a content-type.
+ * (e.g. multipart/mixed; charset="utf-8") --> returns "utf-8"
+ *
+ * @return: String containing the charset parameter; or null
+ */
+
+ getCharset(contentTypeStr) {
+ return EnigmailMime.getParameter(contentTypeStr, "charset");
+ },
+
+ /**
+ * Convert a MIME header value into a UTF-8 encoded representation following RFC 2047
+ */
+ encodeHeaderValue(aStr) {
+ let ret = "";
+
+ let exp = /[^\x01-\x7F]/; // eslint-disable-line no-control-regex
+ if (aStr.search(exp) >= 0) {
+ let s = lazy.EnigmailData.convertFromUnicode(aStr, "utf-8");
+ ret = "=?UTF-8?B?" + btoa(s) + "?=";
+ } else {
+ ret = aStr;
+ }
+
+ return ret;
+ },
+
+ /**
+ * format MIME header with maximum length of 72 characters.
+ */
+ formatHeaderData(hdrValue) {
+ let header;
+ if (Array.isArray(hdrValue)) {
+ header = hdrValue.join("").split(" ");
+ } else {
+ header = hdrValue.split(/ +/);
+ }
+
+ let line = "";
+ let lines = [];
+
+ for (let i = 0; i < header.length; i++) {
+ if (line.length + header[i].length >= 72) {
+ lines.push(line + "\r\n");
+ line = " " + header[i];
+ } else {
+ line += " " + header[i];
+ }
+ }
+
+ lines.push(line);
+
+ return lines.join("").trim();
+ },
+
+ /**
+ * Correctly encode and format a set of email addresses for RFC 2047
+ */
+ formatEmailAddress(addressData) {
+ const adrArr = addressData.split(/, */);
+
+ for (let i in adrArr) {
+ try {
+ const m = adrArr[i].match(
+ /(.*[\w\s]+?)<([\w-][\w.-]+@[\w-][\w.-]+[a-zA-Z]{1,4})>/
+ );
+ if (m && m.length == 3) {
+ adrArr[i] = this.encodeHeaderValue(m[1]) + " <" + m[2] + ">";
+ }
+ } catch (ex) {}
+ }
+
+ return adrArr.join(", ");
+ },
+
+ /**
+ * Extract the subject from the 1st line of the message body, if the message body starts
+ * with: "Subject: ...\r?\n\r?\n".
+ *
+ * @param msgBody - String: message body
+ *
+ * @returns
+ * if subject is found:
+ * Object:
+ * - messageBody - String: message body without subject
+ * - subject - String: extracted subject
+ *
+ * if subject not found: null
+ */
+ extractSubjectFromBody(msgBody) {
+ let m = msgBody.match(/^(\r?\n?Subject: [^\r\n]+\r?\n\r?\n)/i);
+ if (m && m.length > 0) {
+ let subject = m[0].replace(/[\r\n]/g, "");
+ subject = subject.substr(9);
+ msgBody = msgBody.substr(m[0].length);
+
+ return {
+ messageBody: msgBody,
+ subject,
+ };
+ }
+
+ return null;
+ },
+
+ /***
+ * determine if the message data contains a first mime part with content-type = "text/rfc822-headers"
+ * if so, extract the corresponding field(s)
+ */
+
+ extractProtectedHeaders(contentData) {
+ // find first MIME delimiter. Anything before that delimiter is the top MIME structure
+ let m = contentData.search(/^--/m);
+
+ let protectedHdr = [
+ "subject",
+ "date",
+ "from",
+ "to",
+ "cc",
+ "reply-to",
+ "references",
+ "newsgroups",
+ "followup-to",
+ "message-id",
+ ];
+ let newHeaders = {};
+
+ // read headers of first MIME part and extract the boundary parameter
+ let outerHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ outerHdr.initialize(contentData.substr(0, m));
+
+ let ct = outerHdr.extractHeader("content-type", false) || "";
+ if (ct === "") {
+ return null;
+ }
+
+ let startPos = -1,
+ endPos = -1,
+ bound = "";
+
+ if (ct.search(/^multipart\//i) === 0) {
+ // multipart/xyz message type
+ if (m < 5) {
+ return null;
+ }
+
+ bound = EnigmailMime.getBoundary(ct);
+ if (bound === "") {
+ return null;
+ }
+
+ // Escape regex chars in the boundary.
+ bound = bound.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ // search for "outer" MIME delimiter(s)
+ let r = new RegExp("^--" + bound, "mg");
+
+ startPos = -1;
+ endPos = -1;
+
+ // 1st match: start of 1st MIME-subpart
+ let match = r.exec(contentData);
+ if (match && match.index) {
+ startPos = match.index;
+ }
+
+ // 2nd match: end of 1st MIME-subpart
+ match = r.exec(contentData);
+ if (match && match.index) {
+ endPos = match.index;
+ }
+
+ if (startPos < 0 || endPos < 0) {
+ return null;
+ }
+ } else {
+ startPos = contentData.length;
+ endPos = 0;
+ }
+
+ let headers = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ headers.initialize(contentData.substring(0, startPos));
+
+ // we got a potentially protected header. Let's check ...
+ ct = headers.extractHeader("content-type", false) || "";
+ if (this.getParameter(ct, "protected-headers").search(/^v1$/i) !== 0) {
+ return null;
+ }
+
+ for (let i in protectedHdr) {
+ if (headers.hasHeader(protectedHdr[i])) {
+ let extracted = headers.extractHeader(protectedHdr[i], true);
+ newHeaders[protectedHdr[i]] =
+ lazy.jsmime.headerparser.decodeRFC2047Words(extracted) || undefined;
+ }
+ }
+
+ // contentBody holds the complete 1st MIME part
+ let contentBody = contentData.substring(
+ startPos + bound.length + 3,
+ endPos
+ );
+ let i = contentBody.search(/^[A-Za-z]/m); // skip empty lines
+ if (i > 0) {
+ contentBody = contentBody.substr(i);
+ }
+
+ headers.initialize(contentBody);
+
+ let innerCt = headers.extractHeader("content-type", false) || "";
+
+ if (innerCt.search(/^text\/rfc822-headers/i) === 0) {
+ let charset = EnigmailMime.getCharset(innerCt);
+ let ctt = headers.extractHeader("content-transfer-encoding", false) || "";
+
+ // determine where the headers end and the MIME-subpart body starts
+ let bodyStartPos = contentBody.search(/\r?\n\s*\r?\n/) + 1;
+
+ if (bodyStartPos < 10) {
+ return null;
+ }
+
+ bodyStartPos += contentBody.substr(bodyStartPos).search(/^[A-Za-z]/m);
+
+ let ctBodyData = contentBody.substr(bodyStartPos);
+
+ if (ctt.search(/^base64/i) === 0) {
+ ctBodyData = lazy.EnigmailData.decodeBase64(ctBodyData) + "\n";
+ } else if (ctt.search(/^quoted-printable/i) === 0) {
+ ctBodyData = lazy.EnigmailData.decodeQuotedPrintable(ctBodyData) + "\n";
+ }
+
+ if (charset) {
+ ctBodyData = lazy.EnigmailData.convertToUnicode(ctBodyData, charset);
+ }
+
+ // get the headers of the MIME-subpart body --> that's the ones we need
+ let bodyHdr = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ bodyHdr.initialize(ctBodyData);
+
+ for (let i in protectedHdr) {
+ let extracted = bodyHdr.extractHeader(protectedHdr[i], true);
+ if (bodyHdr.hasHeader(protectedHdr[i])) {
+ newHeaders[protectedHdr[i]] =
+ lazy.jsmime.headerparser.decodeRFC2047Words(extracted) || undefined;
+ }
+ }
+ } else {
+ startPos = -1;
+ endPos = -1;
+ }
+
+ return {
+ newHeaders,
+ startPos,
+ endPos,
+ securityLevel: 0,
+ };
+ },
+
+ /**
+ * Get the part number from a URI spec (e.g. mailbox:///folder/xyz?part=1.2.3.5)
+ *
+ * @param spec: String - the URI spec to inspect
+ *
+ * @returns String: the mime part number (or "" if none found)
+ */
+ getMimePartNumber(spec) {
+ let m = spec.match(/([\?&]part=)(\d+(\.\d+)*)/);
+
+ if (m && m.length >= 3) {
+ return m[2];
+ }
+
+ return "";
+ },
+
+ /**
+ * Try to determine if the message structure is a known MIME structure,
+ * based on the MIME part number and the uriSpec.
+ *
+ * @param mimePartNumber: String - the MIME part we are requested to decrypt
+ * @param uriSpec: String - the URI spec of the message (or msg part) loaded by TB
+ *
+ * @returns Boolean: true: regular message structure, MIME part is safe to be decrypted
+ * false: otherwise
+ */
+ isRegularMimeStructure(mimePartNumber, uriSpec, acceptSubParts = false) {
+ if (mimePartNumber.length === 0) {
+ return true;
+ }
+
+ if (acceptSubParts && mimePartNumber.search(/^1(\.1)*$/) === 0) {
+ return true;
+ }
+ if (mimePartNumber === "1") {
+ return true;
+ }
+
+ if (!uriSpec) {
+ return true;
+ }
+
+ // is the message a subpart of a complete attachment?
+ let msgPart = this.getMimePartNumber(uriSpec);
+ if (msgPart.length > 0) {
+ // load attached messages
+ if (
+ mimePartNumber.indexOf(msgPart) === 0 &&
+ mimePartNumber.substr(msgPart.length).search(/^(\.1)+$/) === 0
+ ) {
+ return true;
+ }
+
+ // load attachments of attached messages
+ if (
+ msgPart.indexOf(mimePartNumber) === 0 &&
+ uriSpec.search(/[\?&]filename=/) > 0
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Parse a MIME message and return a tree structure of TreeObject
+ *
+ * @param url: String - the URL to load and parse
+ * @param getBody: Boolean - if true, delivers the body text of each MIME part
+ * @param callbackFunc Function - the callback function that is called asynchronously
+ * when parsing is complete.
+ * Function signature: callBackFunc(TreeObject)
+ *
+ * @returns undefined
+ */
+ getMimeTreeFromUrl(url, getBody = false, callbackFunc) {
+ function onData(data) {
+ let tree = getMimeTree(data, getBody);
+ callbackFunc(tree);
+ }
+
+ let chan = lazy.EnigmailStreams.createChannel(url);
+ let bufferListener = lazy.EnigmailStreams.newStringStreamListener(onData);
+ chan.asyncOpen(bufferListener, null);
+ },
+
+ getMimeTree,
+};
+
+/**
+ * Parse a MIME message and return a tree structure of TreeObject.
+ *
+ * TreeObject contains the following main parts:
+ * - partNum: String
+ * - headers: Map, containing all headers.
+ * Special headers for contentType and charset
+ * - body: String, if getBody == true
+ * - subParts: Array of TreeObject
+ *
+ * @param mimeStr: String - a MIME structure to parse
+ * @param getBody: Boolean - if true, delivers the body text of each MIME part
+ *
+ * @returns TreeObject, or NULL in case of failure
+ */
+function getMimeTree(mimeStr, getBody = false) {
+ let mimeTree = {
+ partNum: "",
+ headers: null,
+ body: "",
+ parent: null,
+ subParts: [],
+ },
+ currentPart = "",
+ currPartNum = "";
+
+ const jsmimeEmitter = {
+ createPartObj(partNum, headers, parent) {
+ let ct;
+
+ if (headers.has("content-type")) {
+ ct = headers.contentType.type;
+ let it = headers.get("content-type").entries();
+ for (let i of it) {
+ ct += "; " + i[0] + '="' + i[1] + '"';
+ }
+ }
+
+ return {
+ partNum,
+ headers,
+ fullContentType: ct,
+ body: "",
+ parent,
+ subParts: [],
+ };
+ },
+
+ /** JSMime API */
+ startMessage() {
+ currentPart = mimeTree;
+ },
+
+ endMessage() {},
+
+ startPart(partNum, headers) {
+ //dump("mime.jsm: jsmimeEmitter.startPart: partNum=" + partNum + "\n");
+ partNum = "1" + (partNum !== "" ? "." : "") + partNum;
+ let newPart = this.createPartObj(partNum, headers, currentPart);
+
+ if (partNum.indexOf(currPartNum) === 0) {
+ // found sub-part
+ currentPart.subParts.push(newPart);
+ } else {
+ // found same or higher level
+ currentPart.subParts.push(newPart);
+ }
+ currPartNum = partNum;
+ currentPart = newPart;
+ },
+ endPart(partNum) {
+ //dump("mime.jsm: jsmimeEmitter.startPart: partNum=" + partNum + "\n");
+ currentPart = currentPart.parent;
+ },
+
+ deliverPartData(partNum, data) {
+ //dump("mime.jsm: jsmimeEmitter.deliverPartData: partNum=" + partNum + " / " + typeof data + "\n");
+ if (typeof data === "string") {
+ currentPart.body += data;
+ } else {
+ currentPart.body += lazy.EnigmailData.arrayBufferToString(data);
+ }
+ },
+ };
+
+ let opt = {
+ strformat: "unicode",
+ bodyformat: getBody ? "decode" : "none",
+ stripcontinuations: false,
+ };
+
+ try {
+ let p = new lazy.jsmime.MimeParser(jsmimeEmitter, opt);
+ p.deliverData(mimeStr);
+ return mimeTree.subParts[0];
+ } catch (ex) {
+ return null;
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/mimeDecrypt.jsm b/comm/mail/extensions/openpgp/content/modules/mimeDecrypt.jsm
new file mode 100644
index 0000000000..c927df5fba
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/mimeDecrypt.jsm
@@ -0,0 +1,933 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailMimeDecrypt"];
+
+/**
+ * Module for handling PGP/MIME encrypted messages
+ * implemented as an XPCOM object
+ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { EnigmailSingletons } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/singletons.jsm"
+);
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+const ENCODING_DEFAULT = 0;
+const ENCODING_BASE64 = 1;
+const ENCODING_QP = 2;
+
+const LAST_MSG = EnigmailSingletons.lastDecryptedMessage;
+
+var gDebugLogLevel = 3;
+
+var gNumProc = 0;
+
+var EnigmailMimeDecrypt = {
+ /**
+ * create a new instance of a PGP/MIME decryption handler
+ */
+ newPgpMimeHandler() {
+ return new MimeDecryptHandler();
+ },
+
+ /**
+ * Wrap the decrypted output into a message/rfc822 attachment
+ *
+ * @param {string} decryptingMimePartNum: requested MIME part number
+ * @param {object} uri: nsIURI object of the decrypted message
+ *
+ * @returns {string}: prefix for message data
+ */
+ pretendAttachment(decryptingMimePartNum, uri) {
+ if (decryptingMimePartNum === "1" || !uri) {
+ return "";
+ }
+
+ let msg = "";
+ let mimePartNumber = lazy.EnigmailMime.getMimePartNumber(uri.spec);
+
+ if (mimePartNumber === decryptingMimePartNum + ".1") {
+ msg =
+ 'Content-Type: message/rfc822; name="attachment.eml"\r\n' +
+ "Content-Transfer-Encoding: 7bit\r\n" +
+ 'Content-Disposition: attachment; filename="attachment.eml"\r\n\r\n';
+
+ try {
+ let dbHdr = uri.QueryInterface(Ci.nsIMsgMessageUrl).messageHeader;
+ if (dbHdr.subject) {
+ msg += `Subject: ${dbHdr.subject}\r\n`;
+ }
+ if (dbHdr.author) {
+ msg += `From: ${dbHdr.author}\r\n`;
+ }
+ if (dbHdr.recipients) {
+ msg += `To: ${dbHdr.recipients}\r\n`;
+ }
+ if (dbHdr.ccList) {
+ msg += `Cc: ${dbHdr.ccList}\r\n`;
+ }
+ } catch (x) {
+ console.debug(x);
+ }
+ }
+
+ return msg;
+ },
+};
+
+////////////////////////////////////////////////////////////////////
+// handler for PGP/MIME encrypted messages
+// data is processed from libmime -> nsPgpMimeProxy
+
+function MimeDecryptHandler() {
+ lazy.EnigmailLog.DEBUG("mimeDecrypt.jsm: MimeDecryptHandler()\n"); // always log this one
+ this.mimeSvc = null;
+ this.initOk = false;
+ this.boundary = "";
+ this.pipe = null;
+ this.closePipe = false;
+ this.statusStr = "";
+ this.outQueue = "";
+ this.dataLength = 0;
+ this.bytesWritten = 0;
+ this.mimePartCount = 0;
+ this.headerMode = 0;
+ this.xferEncoding = ENCODING_DEFAULT;
+ this.matchedPgpDelimiter = 0;
+ this.exitCode = null;
+ this.msgWindow = null;
+ this.msgUriSpec = null;
+ this.returnStatus = null;
+ this.proc = null;
+ this.statusDisplayed = false;
+ this.uri = null;
+ this.backgroundJob = false;
+ this.decryptedHeaders = {};
+ this.mimePartNumber = "";
+ this.allowNestedDecrypt = false;
+ this.dataIsBase64 = null;
+ this.base64Cache = "";
+}
+
+MimeDecryptHandler.prototype = {
+ inStream: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+
+ onStartRequest(request, uri) {
+ if (!lazy.EnigmailCore.getService()) {
+ // Ensure Enigmail is initialized
+ return;
+ }
+ lazy.EnigmailLog.DEBUG("mimeDecrypt.jsm: onStartRequest\n"); // always log this one
+
+ ++gNumProc;
+ if (gNumProc > Services.prefs.getIntPref("temp.openpgp.maxNumProcesses")) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: number of parallel requests above threshold - ignoring request\n"
+ );
+ return;
+ }
+
+ this.initOk = true;
+ this.mimeSvc = request.QueryInterface(Ci.nsIPgpMimeProxy);
+ if ("mimePart" in this.mimeSvc) {
+ this.mimePartNumber = this.mimeSvc.mimePart;
+ } else {
+ this.mimePartNumber = "";
+ }
+
+ if ("allowNestedDecrypt" in this.mimeSvc) {
+ this.allowNestedDecrypt = this.mimeSvc.allowNestedDecrypt;
+ }
+
+ if (this.allowNestedDecrypt) {
+ // We want to ignore signature status of the top level part "1".
+ // Unfortunately, because of our streaming approach to process
+ // MIME content, the parent MIME part was already processed,
+ // and it could have already called into the header sink to set
+ // the signature status. Or, an async job could be currently
+ // running, and the call into the header sink could happen in
+ // the near future.
+ // That means, we must inform the header sink to forget status
+ // information it might have already received for MIME part "1",
+ // an in addition, remember that future information for "1" should
+ // be ignored.
+
+ EnigmailSingletons.messageReader.ignoreStatusFrom("1");
+ }
+
+ if ("messageURI" in this.mimeSvc) {
+ this.uri = this.mimeSvc.messageURI;
+ if (this.uri) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: onStartRequest: uri='" + this.uri.spec + "'\n"
+ );
+ } else {
+ lazy.EnigmailLog.DEBUG("mimeDecrypt.jsm: onStartRequest: uri=null\n");
+ }
+ } else if (uri) {
+ this.uri = uri.QueryInterface(Ci.nsIURI);
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: onStartRequest: uri='" + this.uri.spec + "'\n"
+ );
+ }
+ this.pipe = null;
+ this.closePipe = false;
+ this.exitCode = null;
+ this.msgWindow = lazy.EnigmailVerify.lastWindow;
+ this.msgUriSpec = lazy.EnigmailVerify.lastMsgUri;
+
+ this.statusDisplayed = false;
+ this.returnStatus = null;
+ this.dataLength = 0;
+ this.decryptedData = "";
+ this.mimePartCount = 0;
+ this.bytesWritten = 0;
+ this.matchedPgpDelimiter = 0;
+ this.dataIsBase64 = null;
+ this.base64Cache = "";
+ this.outQueue = "";
+ this.statusStr = "";
+ this.headerMode = 0;
+ this.decryptedHeaders = {};
+ this.xferEncoding = ENCODING_DEFAULT;
+ this.boundary = lazy.EnigmailMime.getBoundary(this.mimeSvc.contentType);
+
+ let now = Date.now();
+ let timeoutReached =
+ EnigmailSingletons.lastMessageDecryptTime &&
+ now - EnigmailSingletons.lastMessageDecryptTime > 10000;
+ if (timeoutReached || !this.isReloadingLastMessage()) {
+ EnigmailSingletons.clearLastDecryptedMessage();
+ EnigmailSingletons.lastMessageDecryptTime = now;
+ }
+ },
+
+ processData(data) {
+ // detect MIME part boundary
+ if (data.includes(this.boundary)) {
+ LOCAL_DEBUG("mimeDecrypt.jsm: processData: found boundary\n");
+ ++this.mimePartCount;
+ this.headerMode = 1;
+ return;
+ }
+
+ // found PGP/MIME "body"
+ if (this.mimePartCount == 2) {
+ if (this.headerMode == 1) {
+ // we are in PGP/MIME main part headers
+ if (data.search(/\r|\n/) === 0) {
+ // end of Mime-part headers reached
+ this.headerMode = 2;
+ } else if (data.search(/^content-transfer-encoding:\s*/i) >= 0) {
+ // extract content-transfer-encoding
+ data = data.replace(/^content-transfer-encoding:\s*/i, "");
+ data = data.replace(/;.*/, "").toLowerCase().trim();
+ if (data.search(/base64/i) >= 0) {
+ this.xferEncoding = ENCODING_BASE64;
+ } else if (data.search(/quoted-printable/i) >= 0) {
+ this.xferEncoding = ENCODING_QP;
+ }
+ }
+ // else: PGP/MIME main part body
+ } else if (this.xferEncoding == ENCODING_QP) {
+ this.cacheData(lazy.EnigmailData.decodeQuotedPrintable(data));
+ } else {
+ this.cacheData(data);
+ }
+ }
+ },
+
+ onDataAvailable(req, stream, offset, count) {
+ // get data from libmime
+ if (!this.initOk) {
+ return;
+ }
+ this.inStream.init(stream);
+
+ if (count > 0) {
+ var data = this.inStream.read(count);
+
+ if (this.mimePartCount == 0 && this.dataIsBase64 === null) {
+ // try to determine if this could be a base64 encoded message part
+ this.dataIsBase64 = this.isBase64Encoding(data);
+ }
+
+ if (!this.dataIsBase64) {
+ if (data.search(/[\r\n][^\r\n]+[\r\n]/) >= 0) {
+ // process multi-line data line by line
+ let lines = data.replace(/\r\n/g, "\n").split(/\n/);
+
+ for (let i = 0; i < lines.length; i++) {
+ this.processData(lines[i] + "\r\n");
+ }
+ } else {
+ this.processData(data);
+ }
+ } else {
+ this.base64Cache += data;
+ }
+ }
+ },
+
+ /**
+ * Try to determine if data is base64 endoded
+ */
+ isBase64Encoding(str) {
+ let ret = false;
+
+ str = str.replace(/[\r\n]/, "");
+ if (str.search(/^[A-Za-z0-9+/=]+$/) === 0) {
+ let excess = str.length % 4;
+ str = str.substring(0, str.length - excess);
+
+ try {
+ atob(str);
+ // if the conversion succeeds, we have a base64 encoded message
+ ret = true;
+ } catch (ex) {
+ // not a base64 encoded
+ console.debug(ex);
+ }
+ }
+
+ return ret;
+ },
+
+ // cache encrypted data
+ cacheData(str) {
+ if (gDebugLogLevel > 4) {
+ LOCAL_DEBUG("mimeDecrypt.jsm: cacheData: " + str.length + "\n");
+ }
+
+ this.outQueue += str;
+ },
+
+ processBase64Message() {
+ LOCAL_DEBUG("mimeDecrypt.jsm: processBase64Message\n");
+
+ try {
+ this.base64Cache = lazy.EnigmailData.decodeBase64(this.base64Cache);
+ } catch (ex) {
+ // if decoding failed, try non-encoded version
+ console.debug(ex);
+ }
+
+ let lines = this.base64Cache.replace(/\r\n/g, "\n").split(/\n/);
+
+ for (let i = 0; i < lines.length; i++) {
+ this.processData(lines[i] + "\r\n");
+ }
+ },
+
+ /**
+ * Determine if we are reloading the same message as the previous one
+ *
+ * @returns Boolean
+ */
+ isReloadingLastMessage() {
+ if (!this.uri) {
+ return false;
+ }
+ if (!LAST_MSG.lastMessageURI) {
+ return false;
+ }
+ if ("lastMessageData" in LAST_MSG && LAST_MSG.lastMessageData === "") {
+ return false;
+ }
+
+ let currMsg = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+
+ if (
+ LAST_MSG.lastMessageURI.folder === currMsg.folder &&
+ LAST_MSG.lastMessageURI.msgNum === currMsg.msgNum
+ ) {
+ return true;
+ }
+
+ return false;
+ },
+
+ onStopRequest(request, status, dummy) {
+ LOCAL_DEBUG("mimeDecrypt.jsm: onStopRequest\n");
+ --gNumProc;
+ if (!this.initOk) {
+ return;
+ }
+
+ if (this.dataIsBase64) {
+ this.processBase64Message();
+ }
+
+ this.msgWindow = lazy.EnigmailVerify.lastWindow;
+ this.msgUriSpec = lazy.EnigmailVerify.lastMsgUri;
+
+ let href = Services.wm.getMostRecentWindow(null)?.document?.location.href;
+
+ if (
+ href == "about:blank" ||
+ href == "chrome://messenger/content/viewSource.xhtml"
+ ) {
+ return;
+ }
+
+ let url = {};
+ let currMsg = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+
+ this.backgroundJob = false;
+
+ if (this.uri) {
+ // return if not decrypting currently displayed message (except if
+ // printing, replying, etc)
+
+ this.backgroundJob =
+ this.uri.spec.search(/[&?]header=(print|quotebody)/) >= 0;
+
+ try {
+ if (!Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ // "decrypt manually" mode
+ let manUrl = {};
+
+ if (lazy.EnigmailVerify.getManualUri()) {
+ manUrl.value = lazy.EnigmailFuncs.getUrlFromUriSpec(
+ lazy.EnigmailVerify.getManualUri()
+ );
+ }
+
+ // print a message if not message explicitly decrypted
+ let currUrlSpec = this.uri.spec.replace(
+ /(\?.*)(number=[0-9]*)(&.*)?$/,
+ "?$2"
+ );
+ let manUrlSpec = manUrl.value.spec.replace(
+ /(\?.*)(number=[0-9]*)(&.*)?$/,
+ "?$2"
+ );
+
+ if (!this.backgroundJob && currUrlSpec.indexOf(manUrlSpec) !== 0) {
+ this.handleManualDecrypt();
+ return;
+ }
+ }
+
+ if (this.msgUriSpec) {
+ url.value = lazy.EnigmailFuncs.getUrlFromUriSpec(this.msgUriSpec);
+ }
+
+ if (
+ this.uri.spec.search(/[&?]header=[^&]+/) > 0 &&
+ this.uri.spec.search(/[&?]examineEncryptedParts=true/) < 0
+ ) {
+ if (
+ this.uri.spec.search(/[&?]header=(filter|enigmailFilter)(&.*)?$/) >
+ 0
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: onStopRequest: detected incoming message processing\n"
+ );
+ return;
+ }
+ }
+
+ if (
+ this.uri.spec.search(/[&?]header=[^&]+/) < 0 &&
+ this.uri.spec.search(/[&?]part=[.0-9]+/) < 0 &&
+ this.uri.spec.search(/[&?]examineEncryptedParts=true/) < 0
+ ) {
+ if (this.uri && url && url.value) {
+ let fixedQueryRef = this.uri.pathQueryRef.replace(/&number=0$/, "");
+ if (
+ url.value.host !== this.uri.host ||
+ url.value.pathQueryRef !== fixedQueryRef
+ ) {
+ return;
+ }
+ }
+ }
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.writeException("mimeDecrypt.js", ex);
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: error while processing " + this.msgUriSpec + "\n"
+ );
+ }
+ }
+
+ let spec = this.uri ? this.uri.spec : null;
+ lazy.EnigmailLog.DEBUG(
+ `mimeDecrypt.jsm: checking MIME structure for ${this.mimePartNumber} / ${spec}\n`
+ );
+
+ if (
+ !this.allowNestedDecrypt &&
+ !lazy.EnigmailMime.isRegularMimeStructure(
+ this.mimePartNumber,
+ spec,
+ false
+ )
+ ) {
+ EnigmailSingletons.addUriWithNestedEncryptedPart(this.msgUriSpec);
+ // ignore, do not display
+ return;
+ }
+
+ if (!this.isReloadingLastMessage()) {
+ if (this.xferEncoding == ENCODING_BASE64) {
+ this.outQueue = lazy.EnigmailData.decodeBase64(this.outQueue) + "\n";
+ }
+
+ let win = this.msgWindow;
+
+ if (!lazy.EnigmailDecryption.isReady(win)) {
+ return;
+ }
+
+ // limit output to 100 times message size to avoid DoS attack
+ let maxOutput = this.outQueue.length * 100;
+
+ lazy.EnigmailLog.DEBUG("mimeDecryp.jsm: starting decryption\n");
+ //EnigmailLog.DEBUG(this.outQueue + "\n");
+
+ let options = {
+ fromAddr: lazy.EnigmailDecryption.getFromAddr(win),
+ maxOutputLength: maxOutput,
+ };
+
+ if (!options.fromAddr) {
+ var win2 = Services.wm.getMostRecentWindow(null);
+ options.fromAddr = lazy.EnigmailDecryption.getFromAddr(win2);
+ }
+
+ const cApi = lazy.EnigmailCryptoAPI();
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: got API: " + cApi.api_name + "\n"
+ );
+
+ // The processing of a contained signed message must be able to
+ // check that this parent object is encrypted. We set the msg ID
+ // early, despite the full results not yet being available.
+ LAST_MSG.lastMessageURI = currMsg;
+ LAST_MSG.mimePartNumber = this.mimePartNumber;
+
+ this.returnStatus = cApi.sync(cApi.decryptMime(this.outQueue, options));
+
+ if (!this.returnStatus) {
+ this.returnStatus = {
+ decryptedData: "",
+ exitCode: -1,
+ statusFlags: lazy.EnigmailConstants.DECRYPTION_FAILED,
+ };
+ }
+
+ if (
+ this.returnStatus.statusFlags & lazy.EnigmailConstants.DECRYPTION_OKAY
+ ) {
+ this.returnStatus.statusFlags |=
+ lazy.EnigmailConstants.PGP_MIME_ENCRYPTED;
+ }
+
+ if (this.returnStatus.exitCode) {
+ // Failure
+ if (this.returnStatus.decryptedData.length) {
+ // However, we got decrypted data.
+ // Did we get any verification failure flags?
+ // If yes, then conclude only verification failed.
+ if (
+ this.returnStatus.statusFlags &
+ (lazy.EnigmailConstants.BAD_SIGNATURE |
+ lazy.EnigmailConstants.UNCERTAIN_SIGNATURE |
+ lazy.EnigmailConstants.EXPIRED_SIGNATURE |
+ lazy.EnigmailConstants.EXPIRED_KEY_SIGNATURE)
+ ) {
+ this.returnStatus.statusFlags |=
+ lazy.EnigmailConstants.DECRYPTION_OKAY;
+ } else {
+ this.returnStatus.statusFlags |=
+ lazy.EnigmailConstants.DECRYPTION_FAILED;
+ }
+ } else {
+ // no data
+ this.returnStatus.statusFlags |=
+ lazy.EnigmailConstants.DECRYPTION_FAILED;
+ }
+ }
+
+ this.decryptedData = this.returnStatus.decryptedData;
+ this.handleResult(this.returnStatus.exitCode);
+
+ let decError =
+ this.returnStatus.statusFlags &
+ lazy.EnigmailConstants.DECRYPTION_FAILED;
+
+ // don't return decrypted data if decryption failed (because it's likely an MDC error),
+ // unless we are called for permanent decryption
+ if (decError) {
+ this.decryptedData = "";
+ }
+
+ this.displayStatus();
+
+ // HACK: remove filename from 1st HTML and plaintext parts to make TB display message without attachment
+ this.decryptedData = this.decryptedData.replace(
+ /^Content-Disposition: inline; filename="msg.txt"/m,
+ "Content-Disposition: inline"
+ );
+ this.decryptedData = this.decryptedData.replace(
+ /^Content-Disposition: inline; filename="msg.html"/m,
+ "Content-Disposition: inline"
+ );
+
+ let prefix = EnigmailMimeDecrypt.pretendAttachment(
+ this.mimePartNumber,
+ this.uri
+ );
+ this.returnData(prefix + this.decryptedData);
+
+ // don't remember the last message if it contains an embedded PGP/MIME message
+ // to avoid ending up in a loop
+ if (
+ this.mimePartNumber === "1" &&
+ this.decryptedData.search(
+ /^Content-Type:[\t ]+multipart\/encrypted/im
+ ) < 0 &&
+ !decError
+ ) {
+ LAST_MSG.lastMessageData = this.decryptedData;
+ LAST_MSG.lastStatus = this.returnStatus;
+ LAST_MSG.lastStatus.decryptedHeaders = this.decryptedHeaders;
+ } else {
+ LAST_MSG.lastMessageURI = null;
+ LAST_MSG.lastMessageData = "";
+ }
+
+ this.decryptedData = "";
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: onStopRequest: process terminated\n"
+ ); // always log this one
+ this.proc = null;
+ } else {
+ this.returnStatus = LAST_MSG.lastStatus;
+ this.decryptedHeaders = LAST_MSG.lastStatus.decryptedHeaders;
+ this.mimePartNumber = LAST_MSG.mimePartNumber;
+ this.exitCode = 0;
+ this.displayStatus();
+ this.returnData(LAST_MSG.lastMessageData);
+ }
+ },
+
+ displayStatus() {
+ lazy.EnigmailLog.DEBUG("mimeDecrypt.jsm: displayStatus()\n");
+
+ if (
+ this.exitCode === null ||
+ this.msgWindow === null ||
+ this.statusDisplayed
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: displayStatus: nothing to display\n"
+ );
+ return;
+ }
+
+ let uriSpec = this.uri ? this.uri.spec : null;
+
+ try {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: displayStatus for uri " + uriSpec + "\n"
+ );
+ let headerSink = EnigmailSingletons.messageReader;
+
+ if (headerSink && this.uri && !this.backgroundJob) {
+ headerSink.processDecryptionResult(
+ this.uri,
+ "modifyMessageHeaders",
+ JSON.stringify(this.decryptedHeaders),
+ this.mimePartNumber
+ );
+
+ headerSink.updateSecurityStatus(
+ this.msgUriSpec,
+ this.exitCode,
+ this.returnStatus.statusFlags,
+ this.returnStatus.extStatusFlags,
+ this.returnStatus.keyId,
+ this.returnStatus.userId,
+ this.returnStatus.sigDetails,
+ this.returnStatus.errorMsg,
+ this.returnStatus.blockSeparation,
+ this.uri,
+ JSON.stringify({
+ encryptedTo: this.returnStatus.encToDetails,
+ }),
+ this.mimePartNumber
+ );
+ } else {
+ this.updateHeadersInMsgDb();
+ }
+ this.statusDisplayed = true;
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.writeException("mimeDecrypt.jsm", ex);
+ }
+ LOCAL_DEBUG("mimeDecrypt.jsm: displayStatus done\n");
+ },
+
+ handleResult(exitCode) {
+ LOCAL_DEBUG("mimeDecrypt.jsm: done: " + exitCode + "\n");
+
+ if (gDebugLogLevel > 4) {
+ LOCAL_DEBUG(
+ "mimeDecrypt.jsm: done: decrypted data='" + this.decryptedData + "'\n"
+ );
+ }
+
+ // ensure newline at the end of the stream
+ if (!this.decryptedData.endsWith("\n")) {
+ this.decryptedData += "\r\n";
+ }
+
+ try {
+ this.extractEncryptedHeaders();
+ this.extractAutocryptGossip();
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ let mightNeedWrapper = true;
+
+ // It's unclear which scenario this check is supposed to fix.
+ // Based on a comment in mimeVerify (which seems code seems to be
+ // derived from), it might be intended to fix forwarding of empty
+ // messages. Not sure this is still necessary. We should check if
+ // this code can be removed.
+ let i = this.decryptedData.search(/\n\r?\n/);
+ // It's unknown why this test checks for > instead of >=
+ if (i > 0) {
+ var hdr = this.decryptedData.substr(0, i).split(/\r?\n/);
+ for (let j = 0; j < hdr.length; j++) {
+ if (hdr[j].search(/^\s*content-type:\s+text\/(plain|html)/i) >= 0) {
+ LOCAL_DEBUG(
+ "mimeDecrypt.jsm: done: adding multipart/mixed around " +
+ hdr[j] +
+ "\n"
+ );
+
+ this.addWrapperToDecryptedResult();
+ mightNeedWrapper = false;
+ break;
+ }
+ }
+ }
+
+ if (mightNeedWrapper) {
+ let headerBoundaryPosition = this.decryptedData.search(/\n\r?\n/);
+ if (
+ headerBoundaryPosition >= 0 &&
+ !/^Content-Type:/im.test(
+ this.decryptedData.substr(0, headerBoundaryPosition)
+ )
+ ) {
+ this.decryptedData =
+ "Content-Type: text/plain; charset=utf-8\r\n\r\n" +
+ this.decryptedData;
+ }
+ }
+
+ this.exitCode = exitCode;
+ },
+
+ addWrapperToDecryptedResult() {
+ let wrapper = lazy.EnigmailMime.createBoundary();
+
+ this.decryptedData =
+ 'Content-Type: multipart/mixed; boundary="' +
+ wrapper +
+ '"\r\n' +
+ "Content-Disposition: inline\r\n\r\n" +
+ "--" +
+ wrapper +
+ "\r\n" +
+ this.decryptedData +
+ "\r\n" +
+ "--" +
+ wrapper +
+ "--\r\n";
+ },
+
+ extractContentType(data) {
+ let i = data.search(/\n\r?\n/);
+ if (i <= 0) {
+ return null;
+ }
+
+ let headers = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ headers.initialize(data.substr(0, i));
+ return headers.extractHeader("content-type", false);
+ },
+
+ // return data to libMime
+ returnData(data) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: returnData: " + data.length + " bytes\n"
+ );
+
+ let proto = null;
+ let ct = this.extractContentType(data);
+ if (ct && ct.search(/multipart\/signed/i) >= 0) {
+ proto = lazy.EnigmailMime.getProtocol(ct);
+ }
+
+ if (
+ proto &&
+ proto.search(/application\/(pgp|pkcs7|x-pkcs7)-signature/i) >= 0
+ ) {
+ try {
+ lazy.EnigmailLog.DEBUG(
+ "mimeDecrypt.jsm: returnData: using direct verification\n"
+ );
+ this.mimeSvc.contentType = ct;
+ if ("mimePart" in this.mimeSvc) {
+ this.mimeSvc.mimePart = this.mimeSvc.mimePart + ".1";
+ }
+ let veri = lazy.EnigmailVerify.newVerifier(proto);
+ veri.onStartRequest(this.mimeSvc, this.uri);
+ veri.onTextData(data);
+ veri.onStopRequest(null, 0);
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.ERROR(
+ "mimeDecrypt.jsm: returnData(): mimeSvc.onDataAvailable failed:\n" +
+ ex.toString()
+ );
+ }
+ } else {
+ try {
+ this.mimeSvc.outputDecryptedData(data, data.length);
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.ERROR(
+ "mimeDecrypt.jsm: returnData(): cannot send decrypted data to MIME processing:\n" +
+ ex.toString()
+ );
+ }
+ }
+ },
+
+ handleManualDecrypt() {
+ try {
+ let headerSink = EnigmailSingletons.messageReader;
+
+ if (headerSink && this.uri && !this.backgroundJob) {
+ headerSink.updateSecurityStatus(
+ this.msgUriSpec,
+ lazy.EnigmailConstants.POSSIBLE_PGPMIME,
+ 0,
+ 0,
+ "",
+ "",
+ "",
+ lazy.l10n.formatValueSync("possibly-pgp-mime"),
+ "",
+ this.uri,
+ null,
+ ""
+ );
+ }
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ return 0;
+ },
+
+ updateHeadersInMsgDb() {
+ if (this.mimePartNumber !== "1") {
+ return;
+ }
+ if (!this.uri) {
+ return;
+ }
+
+ if (this.decryptedHeaders && "subject" in this.decryptedHeaders) {
+ try {
+ let msgDbHdr = this.uri.QueryInterface(
+ Ci.nsIMsgMessageUrl
+ ).messageHeader;
+ msgDbHdr.subject = this.decryptedHeaders.subject;
+ } catch (x) {
+ console.debug(x);
+ }
+ }
+ },
+
+ extractEncryptedHeaders() {
+ let r = lazy.EnigmailMime.extractProtectedHeaders(this.decryptedData);
+ if (!r) {
+ return;
+ }
+
+ this.decryptedHeaders = r.newHeaders;
+ if (r.startPos >= 0 && r.endPos > r.startPos) {
+ this.decryptedData =
+ this.decryptedData.substr(0, r.startPos) +
+ this.decryptedData.substr(r.endPos);
+ }
+ },
+
+ /**
+ * Process the Autocrypt-Gossip header lines.
+ */
+ async extractAutocryptGossip() {
+ let gossipHeaders =
+ MimeParser.extractHeaders(this.decryptedData).get("autocrypt-gossip") ||
+ [];
+ for (let h of gossipHeaders) {
+ try {
+ let keyData = atob(
+ MimeParser.getParameter(h.replace(/ /g, ""), "keydata")
+ );
+ if (keyData) {
+ LAST_MSG.gossip.push(keyData);
+ }
+ } catch {}
+ }
+ },
+};
+
+////////////////////////////////////////////////////////////////////
+// General-purpose functions, not exported
+
+function LOCAL_DEBUG(str) {
+ if (gDebugLogLevel) {
+ lazy.EnigmailLog.DEBUG(str);
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm b/comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm
new file mode 100644
index 0000000000..dd4018d704
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm
@@ -0,0 +1,760 @@
+/* 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";
+
+/**
+ * Module for creating PGP/MIME signed and/or encrypted messages
+ * implemented as XPCOM component
+ */
+
+const EXPORTED_SYMBOLS = ["EnigmailMimeEncrypt"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailEncryption: "chrome://openpgp/content/modules/encryption.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ jsmime: "resource:///modules/jsmime.jsm",
+});
+
+// our own contract IDs
+const PGPMIME_ENCRYPT_CID = Components.ID(
+ "{96fe88f9-d2cd-466f-93e0-3a351df4c6d2}"
+);
+const PGPMIME_ENCRYPT_CONTRACTID = "@enigmail.net/compose/mimeencrypt;1";
+
+const maxBufferLen = 102400;
+const MIME_SIGNED = 1; // only one MIME layer
+const MIME_ENCRYPTED = 2; // only one MIME layer, combined enc/sig data
+const MIME_OUTER_ENC_INNER_SIG = 3; // use two MIME layers
+
+var gDebugLogLevel = 1;
+
+function PgpMimeEncrypt(sMimeSecurityInfo) {
+ this.wrappedJSObject = this;
+
+ this.signMessage = false;
+ this.requireEncryptMessage = false;
+
+ // "securityInfo" variables
+ this.sendFlags = 0;
+ this.UIFlags = 0;
+ this.senderEmailAddr = "";
+ this.recipients = "";
+ this.bccRecipients = "";
+ this.originalSubject = null;
+ this.autocryptGossipHeaders = "";
+
+ try {
+ if (sMimeSecurityInfo) {
+ this.signMessage = sMimeSecurityInfo.signMessage;
+ this.requireEncryptMessage = sMimeSecurityInfo.requireEncryptMessage;
+ }
+ } catch (ex) {}
+}
+
+PgpMimeEncrypt.prototype = {
+ classDescription: "Enigmail JS Encryption Handler",
+ classID: PGPMIME_ENCRYPT_CID,
+ get contractID() {
+ return PGPMIME_ENCRYPT_CONTRACTID;
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgComposeSecure",
+ "nsIStreamListener",
+ ]),
+
+ signMessage: false,
+ requireEncryptMessage: false,
+
+ // private variables
+
+ inStream: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ msgCompFields: null,
+ outStringStream: null,
+
+ // 0: processing headers
+ // 1: processing body
+ // 2: skipping header
+ inputMode: 0,
+ headerData: "",
+ encapsulate: null,
+ encHeader: null,
+ outerBoundary: null,
+ innerBoundary: null,
+ win: null,
+ //statusStr: "",
+ cryptoOutputLength: 0,
+ cryptoOutput: "",
+ hashAlgorithm: "SHA256", // TODO: coordinate with RNP.jsm
+ cryptoInputBuffer: "",
+ outgoingMessageBuffer: "",
+ mimeStructure: 0,
+ exitCode: -1,
+ inspector: null,
+
+ // nsIStreamListener interface
+ onStartRequest(request) {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: onStartRequest\n");
+ this.encHeader = null;
+ },
+
+ onDataAvailable(req, stream, offset, count) {
+ LOCAL_DEBUG("mimeEncrypt.js: onDataAvailable\n");
+ this.inStream.init(stream);
+ //var data = this.inStream.read(count);
+ //LOCAL_DEBUG("mimeEncrypt.js: >"+data+"<\n");
+ },
+
+ onStopRequest(request, status) {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: onStopRequest\n");
+ },
+
+ // nsIMsgComposeSecure interface
+ requiresCryptoEncapsulation(msgIdentity, msgCompFields) {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: requiresCryptoEncapsulation\n");
+ return (
+ (this.sendFlags &
+ (lazy.EnigmailConstants.SEND_SIGNED |
+ lazy.EnigmailConstants.SEND_ENCRYPTED |
+ lazy.EnigmailConstants.SEND_VERBATIM)) !==
+ 0
+ );
+ },
+
+ beginCryptoEncapsulation(
+ outStream,
+ recipientList,
+ msgCompFields,
+ msgIdentity,
+ sendReport,
+ isDraft
+ ) {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: beginCryptoEncapsulation\n");
+
+ if (!outStream) {
+ throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ try {
+ this.outStream = outStream;
+ this.isDraft = isDraft;
+
+ this.msgCompFields = msgCompFields;
+ this.outStringStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+
+ var windowManager = Services.wm;
+ this.win = windowManager.getMostRecentWindow(null);
+
+ if (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) {
+ this.recipientList = recipientList;
+ this.msgIdentity = msgIdentity;
+ this.msgCompFields = msgCompFields;
+ this.inputMode = 2;
+ return null;
+ }
+
+ if (this.sendFlags & lazy.EnigmailConstants.SEND_PGP_MIME) {
+ if (this.sendFlags & lazy.EnigmailConstants.SEND_ENCRYPTED) {
+ // applies to encrypted and signed & encrypted
+ if (this.sendFlags & lazy.EnigmailConstants.SEND_TWO_MIME_LAYERS) {
+ this.mimeStructure = MIME_OUTER_ENC_INNER_SIG;
+ this.innerBoundary = lazy.EnigmailMime.createBoundary();
+ } else {
+ this.mimeStructure = MIME_ENCRYPTED;
+ }
+ } else if (this.sendFlags & lazy.EnigmailConstants.SEND_SIGNED) {
+ this.mimeStructure = MIME_SIGNED;
+ }
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ this.outerBoundary = lazy.EnigmailMime.createBoundary();
+ this.startCryptoHeaders();
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.writeException("mimeEncrypt.js", ex);
+ throw ex;
+ }
+
+ return null;
+ },
+
+ startCryptoHeaders() {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: startCryptoHeaders\n");
+
+ switch (this.mimeStructure) {
+ case MIME_SIGNED:
+ this.signedHeaders1(false);
+ break;
+ case MIME_ENCRYPTED:
+ case MIME_OUTER_ENC_INNER_SIG:
+ this.encryptedHeaders();
+ break;
+ }
+
+ this.writeSecureHeaders();
+ },
+
+ writeSecureHeaders() {
+ this.encHeader = lazy.EnigmailMime.createBoundary();
+
+ let allHdr = "";
+
+ let addrParser = lazy.jsmime.headerparser.parseAddressingHeader;
+ let newsParser = function (s) {
+ return lazy.jsmime.headerparser.parseStructuredHeader("Newsgroups", s);
+ };
+ let noParser = function (s) {
+ return s;
+ };
+
+ let h = {
+ from: {
+ field: "From",
+ parser: addrParser,
+ },
+ replyTo: {
+ field: "Reply-To",
+ parser: addrParser,
+ },
+ to: {
+ field: "To",
+ parser: addrParser,
+ },
+ cc: {
+ field: "Cc",
+ parser: addrParser,
+ },
+ newsgroups: {
+ field: "Newsgroups",
+ parser: newsParser,
+ },
+ followupTo: {
+ field: "Followup-To",
+ parser: addrParser,
+ },
+ messageId: {
+ field: "Message-Id",
+ parser: noParser,
+ },
+ subject: {
+ field: "Subject",
+ parser: noParser,
+ },
+ };
+
+ let alreadyAddedSubject = false;
+
+ if (
+ (this.mimeStructure == MIME_ENCRYPTED ||
+ this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) &&
+ this.originalSubject &&
+ this.originalSubject.length > 0
+ ) {
+ alreadyAddedSubject = true;
+ allHdr += lazy.jsmime.headeremitter.emitStructuredHeader(
+ "subject",
+ this.originalSubject,
+ {}
+ );
+ }
+
+ for (let i in h) {
+ if (h[i].field == "Subject" && alreadyAddedSubject) {
+ continue;
+ }
+ if (this.msgCompFields[i] && this.msgCompFields[i].length > 0) {
+ allHdr += lazy.jsmime.headeremitter.emitStructuredHeader(
+ h[i].field,
+ h[i].parser(this.msgCompFields[i]),
+ {}
+ );
+ }
+ }
+
+ // special handling for references and in-reply-to
+
+ if (this.originalReferences && this.originalReferences.length > 0) {
+ allHdr += lazy.jsmime.headeremitter.emitStructuredHeader(
+ "references",
+ this.originalReferences,
+ {}
+ );
+
+ let bracket = this.originalReferences.lastIndexOf("<");
+ if (bracket >= 0) {
+ allHdr += lazy.jsmime.headeremitter.emitStructuredHeader(
+ "in-reply-to",
+ this.originalReferences.substr(bracket),
+ {}
+ );
+ }
+ }
+
+ let w = `Content-Type: multipart/mixed; boundary="${this.encHeader}"`;
+
+ if (allHdr.length > 0) {
+ w += `;\r\n protected-headers="v1"\r\n${allHdr}`;
+ } else {
+ w += "\r\n";
+ }
+
+ if (this.autocryptGossipHeaders) {
+ w += this.autocryptGossipHeaders;
+ }
+
+ w += `\r\n--${this.encHeader}\r\n`;
+ this.appendToCryptoInput(w);
+
+ if (this.mimeStructure == MIME_SIGNED) {
+ this.appendToMessage(w);
+ }
+ },
+
+ encryptedHeaders(isEightBit = false) {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: encryptedHeaders\n");
+ let subj = "";
+
+ if (this.sendFlags & lazy.EnigmailConstants.ENCRYPT_SUBJECT) {
+ subj = lazy.jsmime.headeremitter.emitStructuredHeader(
+ "subject",
+ lazy.EnigmailFuncs.getProtectedSubjectText(),
+ {}
+ );
+ }
+ this.appendToMessage(
+ subj +
+ "Content-Type: multipart/encrypted;\r\n" +
+ ' protocol="application/pgp-encrypted";\r\n' +
+ ' boundary="' +
+ this.outerBoundary +
+ '"\r\n' +
+ "\r\n" +
+ "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n" +
+ "--" +
+ this.outerBoundary +
+ "\r\n" +
+ "Content-Type: application/pgp-encrypted\r\n" +
+ "Content-Description: PGP/MIME version identification\r\n" +
+ "\r\n" +
+ "Version: 1\r\n" +
+ "\r\n" +
+ "--" +
+ this.outerBoundary +
+ "\r\n" +
+ 'Content-Type: application/octet-stream; name="encrypted.asc"\r\n' +
+ "Content-Description: OpenPGP encrypted message\r\n" +
+ 'Content-Disposition: inline; filename="encrypted.asc"\r\n' +
+ "\r\n"
+ );
+ },
+
+ signedHeaders1(isEightBit = false) {
+ LOCAL_DEBUG("mimeEncrypt.js: signedHeaders1\n");
+ let boundary;
+ if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) {
+ boundary = this.innerBoundary;
+ } else {
+ boundary = this.outerBoundary;
+ }
+ let sigHeader =
+ "Content-Type: multipart/signed; micalg=pgp-" +
+ this.hashAlgorithm.toLowerCase() +
+ ";\r\n" +
+ ' protocol="application/pgp-signature";\r\n' +
+ ' boundary="' +
+ boundary +
+ '"\r\n' +
+ (isEightBit ? "Content-Transfer-Encoding: 8bit\r\n\r\n" : "\r\n") +
+ "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n" +
+ "--" +
+ boundary +
+ "\r\n";
+ if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) {
+ this.appendToCryptoInput(sigHeader);
+ } else {
+ this.appendToMessage(sigHeader);
+ }
+ },
+
+ signedHeaders2() {
+ LOCAL_DEBUG("mimeEncrypt.js: signedHeaders2\n");
+ let boundary;
+ if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) {
+ boundary = this.innerBoundary;
+ } else {
+ boundary = this.outerBoundary;
+ }
+ let sigHeader =
+ "\r\n--" +
+ boundary +
+ "\r\n" +
+ 'Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"\r\n' +
+ "Content-Description: OpenPGP digital signature\r\n" +
+ 'Content-Disposition: attachment; filename="OpenPGP_signature.asc"\r\n\r\n';
+ if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) {
+ this.appendToCryptoInput(sigHeader);
+ } else {
+ this.appendToMessage(sigHeader);
+ }
+ },
+
+ finishCryptoHeaders() {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: finishCryptoHeaders\n");
+
+ this.appendToMessage("\r\n--" + this.outerBoundary + "--\r\n");
+ },
+
+ finishCryptoEncapsulation(abort, sendReport) {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.js: finishCryptoEncapsulation\n");
+
+ if ((this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== 0) {
+ this.flushOutput();
+ return;
+ }
+
+ if (this.encapsulate) {
+ this.appendToCryptoInput("--" + this.encapsulate + "--\r\n");
+ }
+
+ if (this.encHeader) {
+ this.appendToCryptoInput("\r\n--" + this.encHeader + "--\r\n");
+ if (this.mimeStructure == MIME_SIGNED) {
+ this.appendToMessage("\r\n--" + this.encHeader + "--\r\n");
+ }
+ }
+
+ let statusFlagsObj = {};
+ let errorMsgObj = {};
+ this.exitCode = 0;
+
+ if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) {
+ // prepare the inner crypto layer (the signature)
+ let sendFlagsWithoutEncrypt =
+ this.sendFlags & ~lazy.EnigmailConstants.SEND_ENCRYPTED;
+
+ this.exitCode = lazy.EnigmailEncryption.encryptMessageStart(
+ this.win,
+ this.UIFlags,
+ this.senderEmailAddr,
+ this.recipients,
+ this.bccRecipients,
+ this.hashAlgorithm,
+ sendFlagsWithoutEncrypt,
+ this,
+ statusFlagsObj,
+ errorMsgObj
+ );
+ if (!this.exitCode) {
+ // success
+ let innerSignedMessage = this.cryptoInputBuffer;
+ this.cryptoInputBuffer = "";
+
+ this.signedHeaders1(false);
+ this.appendToCryptoInput(innerSignedMessage);
+ this.signedHeaders2();
+ this.cryptoOutput = this.cryptoOutput
+ .replace(/\r/g, "")
+ .replace(/\n/g, "\r\n"); // force CRLF
+ this.appendToCryptoInput(this.cryptoOutput);
+ this.appendToCryptoInput("\r\n--" + this.innerBoundary + "--\r\n");
+ this.cryptoOutput = "";
+ }
+ }
+
+ if (!this.exitCode) {
+ // no failure yet
+ let encryptionFlags = this.sendFlags;
+ if (this.mimeStructure == MIME_OUTER_ENC_INNER_SIG) {
+ // remove signature flag, because we already signed
+ encryptionFlags = encryptionFlags & ~lazy.EnigmailConstants.SEND_SIGNED;
+ }
+ this.exitCode = lazy.EnigmailEncryption.encryptMessageStart(
+ this.win,
+ this.UIFlags,
+ this.senderEmailAddr,
+ this.recipients,
+ this.bccRecipients,
+ this.hashAlgorithm,
+ encryptionFlags,
+ this,
+ statusFlagsObj,
+ errorMsgObj
+ );
+ }
+
+ try {
+ LOCAL_DEBUG(
+ "mimeEncrypt.js: finishCryptoEncapsulation: exitCode = " +
+ this.exitCode +
+ "\n"
+ );
+ if (this.exitCode !== 0) {
+ throw new Error(
+ "failure in finishCryptoEncapsulation, exitCode: " + this.exitCode
+ );
+ }
+
+ if (this.mimeStructure == MIME_SIGNED) {
+ this.signedHeaders2();
+ }
+
+ this.cryptoOutput = this.cryptoOutput
+ .replace(/\r/g, "")
+ .replace(/\n/g, "\r\n"); // force CRLF
+
+ this.appendToMessage(this.cryptoOutput);
+ this.finishCryptoHeaders();
+ this.flushOutput();
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.writeException("mimeEncrypt.js", ex);
+ throw ex;
+ }
+ },
+
+ mimeCryptoWriteBlock(buffer, length) {
+ if (gDebugLogLevel > 4) {
+ LOCAL_DEBUG("mimeEncrypt.js: mimeCryptoWriteBlock: " + length + "\n");
+ }
+
+ try {
+ let line = buffer.substr(0, length);
+ if (this.inputMode === 0) {
+ if ((this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== 0) {
+ line = lazy.EnigmailData.decodeQuotedPrintable(
+ line.replace("=\r\n", "")
+ );
+ }
+
+ if (
+ (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) === 0 ||
+ line.match(
+ /^(From|To|Subject|Message-ID|Date|User-Agent|MIME-Version):/i
+ ) === null
+ ) {
+ this.headerData += line;
+ }
+
+ if (line.replace(/[\r\n]/g, "").length === 0) {
+ this.inputMode = 1;
+
+ if (
+ this.mimeStructure == MIME_ENCRYPTED ||
+ this.mimeStructure == MIME_OUTER_ENC_INNER_SIG
+ ) {
+ if (!this.encHeader) {
+ let ct = this.getHeader("content-type", false);
+ if (
+ ct.search(/text\/plain/i) === 0 ||
+ ct.search(/text\/html/i) === 0
+ ) {
+ this.encapsulate = lazy.EnigmailMime.createBoundary();
+ this.appendToCryptoInput(
+ 'Content-Type: multipart/mixed; boundary="' +
+ this.encapsulate +
+ '"\r\n\r\n'
+ );
+ this.appendToCryptoInput("--" + this.encapsulate + "\r\n");
+ }
+ }
+ } else if (this.mimeStructure == MIME_SIGNED) {
+ let ct = this.getHeader("content-type", true);
+ let hdr = lazy.EnigmailFuncs.getHeaderData(ct);
+ hdr.boundary = hdr.boundary || "";
+ hdr.boundary = hdr.boundary.replace(/^(['"])(.*)(\1)$/, "$2");
+ }
+
+ this.appendToCryptoInput(this.headerData);
+ if (
+ this.mimeStructure == MIME_SIGNED ||
+ (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !== 0
+ ) {
+ this.appendToMessage(this.headerData);
+ }
+ }
+ } else if (this.inputMode == 1) {
+ if (this.mimeStructure == MIME_SIGNED) {
+ // special treatments for various special cases with PGP/MIME signed messages
+ if (line.substr(0, 5) == "From ") {
+ LOCAL_DEBUG("mimeEncrypt.js: added >From\n");
+ this.appendToCryptoInput(">");
+ }
+ }
+
+ this.appendToCryptoInput(line);
+ if (this.mimeStructure == MIME_SIGNED) {
+ this.appendToMessage(line);
+ } else if (
+ (this.sendFlags & lazy.EnigmailConstants.SEND_VERBATIM) !==
+ 0
+ ) {
+ this.appendToMessage(
+ lazy.EnigmailData.decodeQuotedPrintable(line.replace("=\r\n", ""))
+ );
+ }
+ } else if (this.inputMode == 2) {
+ if (line.replace(/[\r\n]/g, "").length === 0) {
+ this.inputMode = 0;
+ }
+ }
+ } catch (ex) {
+ console.debug(ex);
+ lazy.EnigmailLog.writeException("mimeEncrypt.js", ex);
+ throw ex;
+ }
+
+ return null;
+ },
+
+ appendToMessage(str) {
+ if (gDebugLogLevel > 4) {
+ LOCAL_DEBUG("mimeEncrypt.js: appendToMessage: " + str.length + "\n");
+ }
+
+ this.outgoingMessageBuffer += str;
+
+ if (this.outgoingMessageBuffer.length > maxBufferLen) {
+ this.flushOutput();
+ }
+ },
+
+ flushOutput() {
+ LOCAL_DEBUG(
+ "mimeEncrypt.js: flushOutput: " + this.outgoingMessageBuffer.length + "\n"
+ );
+
+ this.outStringStream.setData(
+ this.outgoingMessageBuffer,
+ this.outgoingMessageBuffer.length
+ );
+ var writeCount = this.outStream.writeFrom(
+ this.outStringStream,
+ this.outgoingMessageBuffer.length
+ );
+ if (writeCount < this.outgoingMessageBuffer.length) {
+ LOCAL_DEBUG(
+ "mimeEncrypt.js: flushOutput: wrote " +
+ writeCount +
+ " instead of " +
+ this.outgoingMessageBuffer.length +
+ " bytes\n"
+ );
+ }
+ this.outgoingMessageBuffer = "";
+ },
+
+ appendToCryptoInput(str) {
+ if (gDebugLogLevel > 4) {
+ LOCAL_DEBUG("mimeEncrypt.js: appendToCryptoInput: " + str.length + "\n");
+ }
+
+ this.cryptoInputBuffer += str;
+ },
+
+ getHeader(hdrStr, fullHeader) {
+ var res = "";
+ var hdrLines = this.headerData.split(/[\r\n]+/);
+ for (let i = 0; i < hdrLines.length; i++) {
+ if (hdrLines[i].length > 0) {
+ if (fullHeader && res !== "") {
+ if (hdrLines[i].search(/^\s+/) === 0) {
+ res += hdrLines[i].replace(/\s*[\r\n]*$/, "");
+ } else {
+ return res;
+ }
+ } else {
+ let j = hdrLines[i].indexOf(":");
+ if (j > 0) {
+ let h = hdrLines[i].substr(0, j).replace(/\s*$/, "");
+ if (h.toLowerCase() == hdrStr.toLowerCase()) {
+ res = hdrLines[i].substr(j + 1).replace(/^\s*/, "");
+ if (!fullHeader) {
+ return res;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return res;
+ },
+
+ getInputForCrypto() {
+ return this.cryptoInputBuffer;
+ },
+
+ addCryptoOutput(s) {
+ LOCAL_DEBUG("mimeEncrypt.js: addCryptoOutput:" + s.length + "\n");
+ this.cryptoOutput += s;
+ this.cryptoOutputLength += s.length;
+ },
+
+ getCryptoOutputLength() {
+ return this.cryptoOutputLength;
+ },
+
+ // API for decryptMessage Listener
+ stdin(pipe) {
+ throw new Error("unexpected");
+ },
+
+ stderr(s) {
+ throw new Error("unexpected");
+ //this.statusStr += s;
+ },
+};
+
+////////////////////////////////////////////////////////////////////
+// General-purpose functions, not exported
+
+function LOCAL_DEBUG(str) {
+ if (gDebugLogLevel) {
+ lazy.EnigmailLog.DEBUG(str);
+ }
+}
+
+function initModule() {
+ lazy.EnigmailLog.DEBUG("mimeEncrypt.jsm: initModule()\n");
+ let nspr_log_modules = Services.env.get("NSPR_LOG_MODULES");
+ let matches = nspr_log_modules.match(/mimeEncrypt:(\d+)/);
+
+ if (matches && matches.length > 1) {
+ gDebugLogLevel = matches[1];
+ LOCAL_DEBUG("mimeEncrypt.js: enabled debug logging\n");
+ }
+}
+
+var EnigmailMimeEncrypt = {
+ Handler: PgpMimeEncrypt,
+
+ startup(reason) {
+ initModule();
+ },
+ shutdown(reason) {},
+
+ createMimeEncrypt(sMimeSecurityInfo) {
+ return new PgpMimeEncrypt(sMimeSecurityInfo);
+ },
+
+ isEnigmailCompField(obj) {
+ return obj instanceof PgpMimeEncrypt;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm b/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm
new file mode 100644
index 0000000000..6ec7a615c2
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm
@@ -0,0 +1,716 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailVerify"];
+
+/**
+ * Module for handling PGP/MIME signed messages implemented as JS module.
+ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+});
+
+const PGPMIME_PROTO = "application/pgp-signature";
+
+var gDebugLog = false;
+
+// MimeVerify Constructor
+function MimeVerify(protocol) {
+ if (!protocol) {
+ protocol = PGPMIME_PROTO;
+ }
+
+ this.protocol = protocol;
+ this.verifyEmbedded = false;
+ this.partiallySigned = false;
+ this.exitCode = null;
+ this.inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+}
+
+var EnigmailVerify = {
+ _initialized: false,
+ lastWindow: null,
+ lastMsgUri: null,
+ manualMsgUri: null,
+
+ currentCtHandler: EnigmailConstants.MIME_HANDLER_UNDEF,
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ let nspr_log_modules = Services.env.get("NSPR_LOG_MODULES");
+ let matches = nspr_log_modules.match(/mimeVerify:(\d+)/);
+
+ if (matches && matches.length > 1) {
+ if (matches[1] > 2) {
+ gDebugLog = true;
+ }
+ }
+ },
+
+ setWindow(window, msgUriSpec) {
+ LOCAL_DEBUG("mimeVerify.jsm: setWindow: " + msgUriSpec + "\n");
+
+ this.lastWindow = window;
+ this.lastMsgUri = msgUriSpec;
+ },
+
+ newVerifier(protocol) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: newVerifier: " + (protocol || "null") + "\n"
+ );
+
+ let v = new MimeVerify(protocol);
+ return v;
+ },
+
+ setManualUri(msgUriSpec) {
+ LOCAL_DEBUG("mimeVerify.jsm: setManualUri: " + msgUriSpec + "\n");
+ this.manualMsgUri = msgUriSpec;
+ },
+
+ getManualUri() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: getManualUri\n");
+ return this.manualMsgUri;
+ },
+
+ pgpMimeFactory: {
+ classID: Components.ID("{4f4400a8-9bcc-4b9d-9d53-d2437b377e29}"),
+ createInstance(iid) {
+ return Cc[
+ "@mozilla.org/mimecth;1?type=multipart/encrypted"
+ ].createInstance(iid);
+ },
+ },
+
+ /**
+ * Sets the PGPMime content type handler as the registered handler.
+ */
+ registerPGPMimeHandler() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: registerPGPMimeHandler\n");
+
+ if (this.currentCtHandler == EnigmailConstants.MIME_HANDLER_PGPMIME) {
+ return;
+ }
+
+ let reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ reg.registerFactory(
+ this.pgpMimeFactory.classID,
+ "PGP/MIME verification",
+ "@mozilla.org/mimecth;1?type=multipart/signed",
+ this.pgpMimeFactory
+ );
+
+ this.currentCtHandler = EnigmailConstants.MIME_HANDLER_PGPMIME;
+ },
+
+ /**
+ * Clears the PGPMime content type handler registration. If no factory is
+ * registered, S/MIME works.
+ */
+ unregisterPGPMimeHandler() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: unregisterPGPMimeHandler\n");
+
+ let reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (this.currentCtHandler == EnigmailConstants.MIME_HANDLER_PGPMIME) {
+ reg.unregisterFactory(this.pgpMimeFactory.classID, this.pgpMimeFactory);
+ }
+
+ this.currentCtHandler = EnigmailConstants.MIME_HANDLER_SMIME;
+ },
+};
+
+// MimeVerify implementation
+// verify the signature of PGP/MIME signed messages
+MimeVerify.prototype = {
+ dataCount: 0,
+ foundMsg: false,
+ startMsgStr: "",
+ window: null,
+ msgUriSpec: null,
+ statusDisplayed: false,
+ inStream: null,
+ sigFile: null,
+ sigData: "",
+ mimePartNumber: "",
+
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ parseContentType() {
+ let contentTypeLine = this.mimeSvc.contentType;
+
+ // Eat up CRLF's.
+ contentTypeLine = contentTypeLine.replace(/[\r\n]/g, "");
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: parseContentType: " + contentTypeLine + "\n"
+ );
+
+ let protoRx = RegExp(
+ "protocol\\s*=\\s*[\\'\\\"]" + this.protocol + "[\\\"\\']",
+ "i"
+ );
+
+ if (
+ contentTypeLine.search(/multipart\/signed/i) >= 0 &&
+ contentTypeLine.search(protoRx) > 0
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: parseContentType: found MIME signed message\n"
+ );
+ this.foundMsg = true;
+ let hdr = lazy.EnigmailFuncs.getHeaderData(contentTypeLine);
+ hdr.boundary = hdr.boundary || "";
+ hdr.micalg = hdr.micalg || "";
+ this.boundary = hdr.boundary.replace(/^(['"])(.*)(\1)$/, "$2");
+ }
+ },
+
+ onStartRequest(request, uri) {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: onStartRequest\n"); // always log this one
+
+ this.mimeSvc = request.QueryInterface(Ci.nsIPgpMimeProxy);
+ this.msgUriSpec = EnigmailVerify.lastMsgUri;
+
+ if ("mimePart" in this.mimeSvc) {
+ this.mimePartNumber = this.mimeSvc.mimePart;
+ } else {
+ this.mimePartNumber = "";
+ }
+
+ if ("messageURI" in this.mimeSvc) {
+ this.uri = this.mimeSvc.messageURI;
+ } else if (uri) {
+ this.uri = uri.QueryInterface(Ci.nsIURI);
+ }
+
+ this.dataCount = 0;
+ this.foundMsg = false;
+ this.backgroundJob = false;
+ this.startMsgStr = "";
+ this.boundary = "";
+ this.proc = null;
+ this.closePipe = false;
+ this.pipe = null;
+ this.readMode = 0;
+ this.keepData = "";
+ this.last80Chars = "";
+ this.signedData = "";
+ this.statusStr = "";
+ this.returnStatus = null;
+ this.statusDisplayed = false;
+ this.protectedHeaders = null;
+ this.parseContentType();
+ },
+
+ onDataAvailable(req, stream, offset, count) {
+ LOCAL_DEBUG("mimeVerify.jsm: onDataAvailable: " + count + "\n");
+ if (count > 0) {
+ this.inStream.init(stream);
+ var data = this.inStream.read(count);
+ this.onTextData(data);
+ }
+ },
+
+ onTextData(data) {
+ LOCAL_DEBUG("mimeVerify.jsm: onTextData\n");
+
+ this.dataCount += data.length;
+
+ this.keepData += data;
+ if (this.readMode === 0) {
+ // header data
+ let i = this.findNextMimePart();
+ if (i >= 0) {
+ i += 2 + this.boundary.length;
+ if (this.keepData[i] == "\n") {
+ ++i;
+ } else if (this.keepData[i] == "\r") {
+ ++i;
+ if (this.keepData[i] == "\n") {
+ ++i;
+ }
+ }
+
+ this.keepData = this.keepData.substr(i);
+ data = this.keepData;
+ this.readMode = 1;
+ } else {
+ this.keepData = data.substr(-this.boundary.length - 3);
+ }
+ }
+
+ if (this.readMode === 1) {
+ // "real data"
+ if (data.includes("-")) {
+ // only check current line for speed reasons
+ let i = this.findNextMimePart();
+ if (i >= 0) {
+ // end of "read data found"
+ if (this.keepData[i - 2] == "\r" && this.keepData[i - 1] == "\n") {
+ --i;
+ }
+
+ this.signedData = this.keepData.substr(0, i - 1);
+ this.keepData = this.keepData.substr(i);
+ this.readMode = 2;
+ }
+ } else {
+ return;
+ }
+ }
+
+ if (this.readMode === 2) {
+ let i = this.keepData.indexOf("--" + this.boundary + "--");
+ if (i >= 0) {
+ // ensure that we keep everything until we got the "end" boundary
+ if (this.keepData[i - 2] == "\r" && this.keepData[i - 1] == "\n") {
+ --i;
+ }
+ this.keepData = this.keepData.substr(0, i - 1);
+ this.readMode = 3;
+ }
+ }
+
+ if (this.readMode === 3) {
+ // signature data
+ if (this.protocol === PGPMIME_PROTO) {
+ let xferEnc = this.getContentTransferEncoding();
+ if (xferEnc.search(/base64/i) >= 0) {
+ let bound = this.getBodyPart();
+ this.keepData =
+ lazy.EnigmailData.decodeBase64(
+ this.keepData.substring(bound.start, bound.end)
+ ) + "\n";
+ } else if (xferEnc.search(/quoted-printable/i) >= 0) {
+ let bound = this.getBodyPart();
+ let qp = this.keepData.substring(bound.start, bound.end);
+ this.keepData = lazy.EnigmailData.decodeQuotedPrintable(qp) + "\n";
+ }
+
+ // extract signature data
+ let s = Math.max(this.keepData.search(/^-----BEGIN PGP /m), 0);
+ let e = Math.max(
+ this.keepData.search(/^-----END PGP /m),
+ this.keepData.length - 30
+ );
+ this.sigData = this.keepData.substring(s, e + 30);
+ } else {
+ this.sigData = "";
+ }
+
+ this.keepData = "";
+ this.readMode = 4; // ignore any further data
+ }
+ },
+
+ getBodyPart() {
+ let start = this.keepData.search(/(\n\n|\r\n\r\n)/);
+ if (start < 0) {
+ start = 0;
+ }
+ let end = this.keepData.indexOf("--" + this.boundary + "--") - 1;
+ if (end < 0) {
+ end = this.keepData.length;
+ }
+
+ return {
+ start,
+ end,
+ };
+ },
+
+ // determine content-transfer encoding of mime part, assuming that whole
+ // message is in this.keepData
+ getContentTransferEncoding() {
+ let enc = "7bit";
+ let m = this.keepData.match(/^(content-transfer-encoding:)(.*)$/im);
+ if (m && m.length > 2) {
+ enc = m[2].trim().toLowerCase();
+ }
+
+ return enc;
+ },
+
+ findNextMimePart() {
+ let startOk = false;
+ let endOk = false;
+
+ let i = this.keepData.indexOf("--" + this.boundary);
+ if (i === 0) {
+ startOk = true;
+ }
+ if (i > 0) {
+ if (this.keepData[i - 1] == "\r" || this.keepData[i - 1] == "\n") {
+ startOk = true;
+ }
+ }
+
+ if (!startOk) {
+ return -1;
+ }
+
+ if (i + this.boundary.length + 2 < this.keepData.length) {
+ if (
+ this.keepData[i + this.boundary.length + 2] == "\r" ||
+ this.keepData[i + this.boundary.length + 2] == "\n" ||
+ this.keepData.substr(i + this.boundary.length + 2, 2) == "--"
+ ) {
+ endOk = true;
+ }
+ }
+ // else
+ // endOk = true;
+
+ if (i >= 0 && startOk && endOk) {
+ return i;
+ }
+ return -1;
+ },
+
+ isAllowedSigPart(queryMimePartNumber, loadedUriSpec) {
+ // allowed are:
+ // - the top part 1
+ // - the child 1.1 if 1 is an encryption layer
+ // - a part that is the one we are loading
+ // - a part that is the first child of the one we are loading,
+ // and the child we are loading is an encryption layer
+
+ if (queryMimePartNumber.length === 0) {
+ return false;
+ }
+
+ if (queryMimePartNumber === "1") {
+ return true;
+ }
+
+ if (queryMimePartNumber == "1.1" || queryMimePartNumber == "1.1.1") {
+ if (!this.uri) {
+ // We aren't loading in message displaying, but some other
+ // context, could be e.g. forwarding.
+ return false;
+ }
+
+ // If we are processing "1.1", it means we're the child of the
+ // top mime part. Don't process the signature unless the top
+ // level mime part is an encryption layer.
+ // If we are processing "1.1.1", then potentially the top level
+ // mime part was a signature and has been ignored, and "1.1"
+ // might be an encrypted part that was allowed.
+
+ let currMsg = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+ let parentToCheck = queryMimePartNumber == "1.1.1" ? "1.1" : "1";
+ if (
+ lazy.EnigmailSingletons.isLastDecryptedMessagePart(
+ currMsg.folder,
+ currMsg.msgNum,
+ parentToCheck
+ )
+ ) {
+ return true;
+ }
+ }
+
+ if (!loadedUriSpec) {
+ return false;
+ }
+
+ // is the message a subpart of a complete attachment?
+ let msgPart = lazy.EnigmailMime.getMimePartNumber(loadedUriSpec);
+
+ if (msgPart.length > 0) {
+ if (queryMimePartNumber === msgPart + ".1") {
+ return true;
+ }
+
+ let currMsg = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+ if (
+ queryMimePartNumber === msgPart + ".1.1" &&
+ lazy.EnigmailSingletons.isLastDecryptedMessagePart(
+ currMsg.folder,
+ currMsg.msgNum,
+ msgPart + ".1"
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ onStopRequest() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: onStopRequest\n");
+
+ this.window = EnigmailVerify.lastWindow;
+ this.msgUriSpec = EnigmailVerify.lastMsgUri;
+
+ this.backgroundJob = false;
+
+ // don't try to verify if no message found
+ // if (this.verifyEmbedded && (!this.foundMsg)) return; // TODO - check
+
+ let href = Services.wm.getMostRecentWindow(null)?.document?.location.href;
+
+ if (
+ href == "about:blank" ||
+ href == "chrome://messenger/content/viewSource.xhtml"
+ ) {
+ return;
+ }
+
+ if (this.readMode < 4) {
+ // we got incomplete data; simply return what we got
+ this.returnData(
+ this.signedData.length > 0 ? this.signedData : this.keepData
+ );
+
+ return;
+ }
+
+ this.protectedHeaders = lazy.EnigmailMime.extractProtectedHeaders(
+ this.signedData
+ );
+
+ if (
+ this.protectedHeaders &&
+ this.protectedHeaders.startPos >= 0 &&
+ this.protectedHeaders.endPos > this.protectedHeaders.startPos
+ ) {
+ let r =
+ this.signedData.substr(0, this.protectedHeaders.startPos) +
+ this.signedData.substr(this.protectedHeaders.endPos);
+ this.returnData(r);
+ } else {
+ this.returnData(this.signedData);
+ }
+
+ if (!this.isAllowedSigPart(this.mimePartNumber, this.msgUriSpec)) {
+ return;
+ }
+
+ if (this.uri) {
+ // return if not decrypting currently displayed message (except if
+ // printing, replying, etc)
+
+ this.backgroundJob =
+ this.uri.spec.search(/[&?]header=(print|quotebody)/) >= 0;
+
+ try {
+ if (!Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ // "decrypt manually" mode
+ let manUrl = {};
+
+ if (EnigmailVerify.getManualUri()) {
+ manUrl = lazy.EnigmailFuncs.getUrlFromUriSpec(
+ EnigmailVerify.getManualUri()
+ );
+ }
+
+ // print a message if not message explicitly decrypted
+ let currUrlSpec = this.uri.spec.replace(
+ /(\?.*)(number=[0-9]*)(&.*)?$/,
+ "?$2"
+ );
+ let manUrlSpec = manUrl.spec.replace(
+ /(\?.*)(number=[0-9]*)(&.*)?$/,
+ "?$2"
+ );
+
+ if (!this.backgroundJob && currUrlSpec != manUrlSpec) {
+ return; // this.handleManualDecrypt();
+ }
+ }
+
+ if (
+ this.uri.spec.search(/[&?]header=[a-zA-Z0-9]*$/) < 0 &&
+ this.uri.spec.search(/[&?]part=[.0-9]+/) < 0 &&
+ this.uri.spec.search(/[&?]examineEncryptedParts=true/) < 0
+ ) {
+ if (this.uri.spec.search(/[&?]header=filter&.*$/) > 0) {
+ return;
+ }
+
+ let url = this.msgUriSpec
+ ? lazy.EnigmailFuncs.getUrlFromUriSpec(this.msgUriSpec)
+ : null;
+
+ if (url) {
+ let otherId = lazy.EnigmailURIs.msgIdentificationFromUrl(url);
+ let thisId = lazy.EnigmailURIs.msgIdentificationFromUrl(this.uri);
+
+ if (
+ url.host !== this.uri.host ||
+ otherId.folder !== thisId.folder ||
+ otherId.msgNum !== thisId.msgNum
+ ) {
+ return;
+ }
+ }
+ }
+ } catch (ex) {
+ lazy.EnigmailLog.writeException("mimeVerify.jsm", ex);
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: error while processing " + this.msgUriSpec + "\n"
+ );
+ }
+ }
+
+ if (this.protocol === PGPMIME_PROTO) {
+ let win = this.window;
+
+ if (!lazy.EnigmailDecryption.isReady(win)) {
+ return;
+ }
+
+ let options = {
+ fromAddr: lazy.EnigmailDecryption.getFromAddr(win),
+ mimeSignatureData: this.sigData,
+ msgDate: lazy.EnigmailDecryption.getMsgDate(win),
+ };
+ const cApi = lazy.EnigmailCryptoAPI();
+
+ // ensure all lines end with CRLF as specified in RFC 3156, section 5
+ if (this.signedData.search(/[^\r]\n/) >= 0) {
+ this.signedData = this.signedData
+ .replace(/\r\n/g, "\n")
+ .replace(/\n/g, "\r\n");
+ }
+
+ this.returnStatus = cApi.sync(cApi.verifyMime(this.signedData, options));
+
+ if (!this.returnStatus) {
+ this.exitCode = -1;
+ } else {
+ this.exitCode = this.returnStatus.exitCode;
+
+ this.returnStatus.statusFlags |= EnigmailConstants.PGP_MIME_SIGNED;
+
+ if (this.partiallySigned) {
+ this.returnStatus.statusFlags |= EnigmailConstants.PARTIALLY_PGP;
+ }
+
+ this.displayStatus();
+ }
+ }
+ },
+
+ // return data to libMime
+ returnData(data) {
+ lazy.EnigmailLog.DEBUG(
+ "mimeVerify.jsm: returnData: " + data.length + " bytes\n"
+ );
+
+ let m = data.match(/^(content-type: +)([\w/]+)/im);
+ if (m && m.length >= 3) {
+ let contentType = m[2];
+ if (contentType.search(/^text/i) === 0) {
+ // add multipart/mixed boundary to work around TB bug (empty forwarded message)
+ let bound = lazy.EnigmailMime.createBoundary();
+ data =
+ 'Content-Type: multipart/mixed; boundary="' +
+ bound +
+ '"\n' +
+ "Content-Disposition: inline\n\n--" +
+ bound +
+ "\n" +
+ data +
+ "\n--" +
+ bound +
+ "--\n";
+ }
+ }
+
+ this.mimeSvc.outputDecryptedData(data, data.length);
+ },
+
+ setWindow(window, msgUriSpec) {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: setWindow: " + msgUriSpec + "\n");
+
+ if (!this.window) {
+ this.window = window;
+ this.msgUriSpec = msgUriSpec;
+ }
+ },
+
+ displayStatus() {
+ lazy.EnigmailLog.DEBUG("mimeVerify.jsm: displayStatus\n");
+ if (
+ this.exitCode === null ||
+ this.window === null ||
+ this.statusDisplayed ||
+ this.backgroundJob
+ ) {
+ return;
+ }
+
+ try {
+ LOCAL_DEBUG("mimeVerify.jsm: displayStatus displaying result\n");
+ let headerSink = lazy.EnigmailSingletons.messageReader;
+
+ if (this.protectedHeaders) {
+ headerSink.processDecryptionResult(
+ this.uri,
+ "modifyMessageHeaders",
+ JSON.stringify(this.protectedHeaders.newHeaders),
+ this.mimePartNumber
+ );
+ }
+
+ if (headerSink) {
+ headerSink.updateSecurityStatus(
+ this.lastMsgUri,
+ this.exitCode,
+ this.returnStatus.statusFlags,
+ this.returnStatus.extStatusFlags,
+ this.returnStatus.keyId,
+ this.returnStatus.userId,
+ this.returnStatus.sigDetails,
+ this.returnStatus.errorMsg,
+ this.returnStatus.blockSeparation,
+ this.uri,
+ JSON.stringify({
+ encryptedTo: this.returnStatus.encToDetails,
+ }),
+ this.mimePartNumber
+ );
+ }
+ this.statusDisplayed = true;
+ } catch (ex) {
+ lazy.EnigmailLog.writeException("mimeVerify.jsm", ex);
+ }
+ },
+};
+
+////////////////////////////////////////////////////////////////////
+// General-purpose functions, not exported
+
+function LOCAL_DEBUG(str) {
+ if (gDebugLog) {
+ lazy.EnigmailLog.DEBUG(str);
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/msgRead.jsm b/comm/mail/extensions/openpgp/content/modules/msgRead.jsm
new file mode 100644
index 0000000000..04f38bf602
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/msgRead.jsm
@@ -0,0 +1,289 @@
+/*
+ * 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 = ["EnigmailMsgRead"];
+
+/**
+ * Message-reading related functions
+ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+});
+
+var EnigmailMsgRead = {
+ /**
+ * Ensure that Thunderbird prepares certain headers during message reading
+ */
+ ensureExtraAddonHeaders() {
+ let hdr = Services.prefs.getCharPref("mailnews.headers.extraAddonHeaders");
+
+ if (hdr !== "*") {
+ // do nothing if extraAddonHeaders is "*" (all headers)
+ for (let h of ["autocrypt", "openpgp"]) {
+ if (hdr.search(h) < 0) {
+ if (hdr.length > 0) {
+ hdr += " ";
+ }
+ hdr += h;
+ }
+ }
+ Services.prefs.setCharPref("mailnews.headers.extraAddonHeaders", hdr);
+ }
+ },
+
+ /**
+ * Get a mail URL from a uriSpec
+ *
+ * @param uriSpec: String - URI of the desired message
+ *
+ * @returns Object: nsIURL or nsIMsgMailNewsUrl object
+ */
+ getUrlFromUriSpec(uriSpec) {
+ return lazy.EnigmailFuncs.getUrlFromUriSpec(uriSpec);
+ },
+
+ /**
+ * Determine if an attachment is possibly signed
+ */
+ checkSignedAttachment(attachmentObj, index, currentAttachments) {
+ function escapeRegex(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+ }
+
+ var attachmentList;
+ if (index !== null) {
+ attachmentList = attachmentObj;
+ } else {
+ attachmentList = currentAttachments;
+ for (let i = 0; i < attachmentList.length; i++) {
+ if (attachmentList[i].url == attachmentObj.url) {
+ index = i;
+ break;
+ }
+ }
+ if (index === null) {
+ return false;
+ }
+ }
+
+ var signed = false;
+ var findFile;
+
+ var attName = this.getAttachmentName(attachmentList[index])
+ .toLowerCase()
+ .replace(/\+/g, "\\+");
+
+ // check if filename is a signature
+ if (
+ this.getAttachmentName(attachmentList[index]).search(/\.(sig|asc)$/i) >
+ 0 ||
+ attachmentList[index].contentType.match(/^application\/pgp-signature/i)
+ ) {
+ findFile = new RegExp(escapeRegex(attName.replace(/\.(sig|asc)$/, "")));
+ } else if (attName.search(/\.pgp$/i) > 0) {
+ findFile = new RegExp(
+ escapeRegex(attName.replace(/\.pgp$/, "")) + "(\\.pgp)?\\.(sig|asc)$"
+ );
+ } else {
+ findFile = new RegExp(escapeRegex(attName) + "\\.(sig|asc)$");
+ }
+
+ for (let i in attachmentList) {
+ if (
+ i != index &&
+ this.getAttachmentName(attachmentList[i])
+ .toLowerCase()
+ .search(findFile) === 0
+ ) {
+ signed = true;
+ }
+ }
+
+ return signed;
+ },
+
+ /**
+ * Get the name of an attachment from the attachment object
+ */
+ getAttachmentName(attachment) {
+ return attachment.name;
+ },
+
+ /**
+ * Escape text such that it can be used as HTML text
+ */
+ escapeTextForHTML(text, hyperlink) {
+ // Escape special characters
+ if (text.indexOf("&") > -1) {
+ text = text.replace(/&/g, "&amp;");
+ }
+
+ if (text.indexOf("<") > -1) {
+ text = text.replace(/</g, "&lt;");
+ }
+
+ if (text.indexOf(">") > -1) {
+ text = text.replace(/>/g, "&gt;");
+ }
+
+ if (text.indexOf('"') > -1) {
+ text = text.replace(/"/g, "&quot;");
+ }
+
+ if (!hyperlink) {
+ return text;
+ }
+
+ // Hyperlink email addresses (we accept at most 1024 characters before and after the @)
+ var addrs = text.match(
+ /\b[A-Za-z0-9_+.-]{1,1024}@[A-Za-z0-9.-]{1,1024}\b/g
+ );
+
+ var newText, offset, loc;
+ if (addrs && addrs.length) {
+ newText = "";
+ offset = 0;
+
+ for (var j = 0; j < addrs.length; j++) {
+ var addr = addrs[j];
+
+ loc = text.indexOf(addr, offset);
+ if (loc < offset) {
+ break;
+ }
+
+ if (loc > offset) {
+ newText += text.substr(offset, loc - offset);
+ }
+
+ // Strip any period off the end of address
+ addr = addr.replace(/[.]$/, "");
+
+ if (!addr.length) {
+ continue;
+ }
+
+ newText += '<a href="mailto:' + addr + '">' + addr + "</a>";
+
+ offset = loc + addr.length;
+ }
+
+ newText += text.substr(offset, text.length - offset);
+
+ text = newText;
+ }
+
+ // Hyperlink URLs (we don't accept URLS or more than 1024 characters length)
+ var urls = text.match(/\b(http|https|ftp):\S{1,1024}\s/g);
+
+ if (urls && urls.length) {
+ newText = "";
+ offset = 0;
+
+ for (var k = 0; k < urls.length; k++) {
+ var url = urls[k];
+
+ loc = text.indexOf(url, offset);
+ if (loc < offset) {
+ break;
+ }
+
+ if (loc > offset) {
+ newText += text.substr(offset, loc - offset);
+ }
+
+ // Strip delimiters off the end of URL
+ url = url.replace(/\s$/, "");
+ url = url.replace(/([),.']|&gt;|&quot;)$/, "");
+
+ if (!url.length) {
+ continue;
+ }
+
+ newText += '<a href="' + url + '">' + url + "</a>";
+
+ offset = loc + url.length;
+ }
+
+ newText += text.substr(offset, text.length - offset);
+
+ text = newText;
+ }
+
+ return text;
+ },
+
+ /**
+ * Match the key to the sender's from address
+ *
+ * @param {string} keyId: signing key ID
+ * @param {string} fromAddr: sender's email address
+ *
+ * @returns Promise<String>: matching email address
+ */
+ matchUidToSender(keyId, fromAddr) {
+ if (!fromAddr || !keyId) {
+ return null;
+ }
+
+ try {
+ fromAddr = lazy.EnigmailFuncs.stripEmail(fromAddr).toLowerCase();
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ let keyObj = lazy.EnigmailKeyRing.getKeyById(keyId);
+ if (!keyObj) {
+ return null;
+ }
+
+ let userIdList = keyObj.userIds;
+
+ try {
+ for (let i = 0; i < userIdList.length; i++) {
+ if (
+ fromAddr ==
+ lazy.EnigmailFuncs.stripEmail(userIdList[i].userId).toLowerCase()
+ ) {
+ let result = lazy.EnigmailFuncs.stripEmail(userIdList[i].userId);
+ return result;
+ }
+ }
+ } catch (ex) {
+ console.debug(ex);
+ }
+ return null;
+ },
+
+ searchQuotedPgp(node) {
+ if (
+ node.nodeName.toLowerCase() === "blockquote" &&
+ node.textContent.includes("-----BEGIN PGP ")
+ ) {
+ return true;
+ }
+
+ if (node.firstChild && this.searchQuotedPgp(node.firstChild)) {
+ return true;
+ }
+
+ if (node.nextSibling && this.searchQuotedPgp(node.nextSibling)) {
+ return true;
+ }
+
+ return false;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm b/comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm
new file mode 100644
index 0000000000..17de2e3246
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm
@@ -0,0 +1,1338 @@
+/*
+ * 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";
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var EXPORTED_SYMBOLS = ["EnigmailPersistentCrypto"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm",
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailEncryption: "chrome://openpgp/content/modules/encryption.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailFixExchangeMsg:
+ "chrome://openpgp/content/modules/fixExchangeMessage.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ GlodaUtils: "resource:///modules/gloda/GlodaUtils.jsm",
+ jsmime: "resource:///modules/jsmime.jsm",
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ MailCryptoUtils: "resource:///modules/MailCryptoUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var EnigmailPersistentCrypto = {
+ /***
+ * cryptMessage
+ *
+ * Decrypts a message and copy it to a folder. If targetKey is
+ * not null, it encrypts a message to the target key afterwards.
+ *
+ * @param {nsIMsgDBHdr} hdr - message to process
+ * @param {string} destFolder - target folder URI
+ * @param {boolean} move - true for move, false for copy
+ * @param {KeyObject} targetKey - target key if encryption is requested
+ *
+ * @returns {nsMsgKey} Message key of the new message
+ **/
+ async cryptMessage(hdr, destFolder, move, targetKey) {
+ return new Promise(function (resolve, reject) {
+ let msgUriSpec = hdr.folder.getUriForMsg(hdr);
+ let msgUrl = lazy.EnigmailFuncs.getUrlFromUriSpec(msgUriSpec);
+
+ const crypt = new CryptMessageIntoFolder(destFolder, move, targetKey);
+
+ lazy.EnigmailMime.getMimeTreeFromUrl(msgUrl, true, async function (mime) {
+ try {
+ let newMsgKey = await crypt.messageParseCallback(mime, hdr);
+ resolve(newMsgKey);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ },
+
+ changeMessageId(content, newMessageIdPrefix) {
+ let [headerData, body] = MimeParser.extractHeadersAndBody(content);
+ content = "";
+
+ let newHeaders = headerData.rawHeaderText;
+ if (!newHeaders.endsWith("\r\n")) {
+ newHeaders += "\r\n";
+ }
+
+ headerData = undefined;
+
+ let regExpMsgId = new RegExp("^message-id: <(.*)>", "mi");
+ let msgId;
+ let match = newHeaders.match(regExpMsgId);
+
+ if (match) {
+ msgId = match[1];
+ newHeaders = newHeaders.replace(
+ regExpMsgId,
+ "Message-Id: <" + newMessageIdPrefix + "-$1>"
+ );
+
+ // Match the references header across multiple lines
+ // eslint-disable-next-line no-control-regex
+ let regExpReferences = new RegExp("^references: .*([\r\n]*^ .*$)*", "mi");
+ let refLines = newHeaders.match(regExpReferences);
+ if (refLines) {
+ // Take the full match of the existing header
+ let newRef = refLines[0] + " <" + msgId + ">";
+ newHeaders = newHeaders.replace(regExpReferences, newRef);
+ } else {
+ newHeaders += "References: <" + msgId + ">\r\n";
+ }
+ }
+
+ return newHeaders + "\r\n" + body;
+ },
+
+ /*
+ * Copies an email message to a folder, which is a modified copy of an
+ * existing message, optionally creating a new message ID.
+ *
+ * @param {nsIMsgDBHdr} originalMsgHdr - Header of the original message
+ * @param {string} targetFolderUri - Target folder URI
+ * @param {boolean} deleteOrigMsg - Should the original message be deleted?
+ * @param {string} content - New message content
+ * @param {string} newMessageIdPrefix - If this is non-null, create a new message ID
+ * by adding this prefix.
+ *
+ * @returns {nsMsgKey} Message key of the new message
+ */
+ async copyMessageToFolder(
+ originalMsgHdr,
+ targetFolderUri,
+ deleteOrigMsg,
+ content,
+ newMessageIdPrefix
+ ) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: copyMessageToFolder()\n");
+ return new Promise((resolve, reject) => {
+ if (newMessageIdPrefix) {
+ content = this.changeMessageId(content, newMessageIdPrefix);
+ }
+
+ // Create the temporary file where the new message will be stored.
+ let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempFile.append("message.eml");
+ tempFile.createUnique(0, 0o600);
+
+ let outputStream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ outputStream.init(tempFile, 2, 0x200, false); // open as "write only"
+ outputStream.write(content, content.length);
+ outputStream.close();
+
+ // Delete file on exit, because Windows locks the file
+ let extAppLauncher = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsPIExternalAppLauncher);
+ extAppLauncher.deleteTemporaryFileOnExit(tempFile);
+
+ let msgFolder = originalMsgHdr.folder;
+
+ // The following technique was copied from AttachmentDeleter in Thunderbird's
+ // nsMessenger.cpp. There is a "unified" listener which serves as copy and delete
+ // listener. In all cases, the `OnStopCopy()` of the delete listener selects the
+ // replacement message.
+ // The deletion happens in `OnStopCopy()` of the copy listener for local messages
+ // and in `OnStopRunningUrl()` for IMAP messages if the folder is displayed since
+ // otherwise `OnStopRunningUrl()` doesn't run.
+
+ let copyListener, newKey;
+ let statusCode = 0;
+ let destFolder = targetFolderUri
+ ? lazy.MailUtils.getExistingFolder(targetFolderUri)
+ : msgFolder;
+
+ copyListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgCopyServiceListener",
+ "nsIUrlListener",
+ ]),
+ GetMessageId(messageId) {
+ // Maybe enable this later. Most of the Thunderbird code does not supply this.
+ // messageId = { value: msgHdr.messageId };
+ },
+ SetMessageKey(key) {
+ lazy.EnigmailLog.DEBUG(
+ `persistentCrypto.jsm: copyMessageToFolder: Result of CopyFileMessage() is new message with key ${key}\n`
+ );
+ newKey = key;
+ },
+ applyFlags() {
+ let newHdr = destFolder.GetMessageHeader(newKey);
+ newHdr.markRead(originalMsgHdr.isRead);
+ newHdr.markFlagged(originalMsgHdr.isFlagged);
+ newHdr.subject = originalMsgHdr.subject;
+ },
+ OnStartCopy() {},
+ OnStopCopy(status) {
+ statusCode = status;
+ if (statusCode !== 0) {
+ lazy.EnigmailLog.ERROR(
+ `persistentCrypto.jsm: ${statusCode} replacing message, folder="${msgFolder.name}", key=${originalMsgHdr.messageKey}/${newKey}\n`
+ );
+ reject();
+ return;
+ }
+
+ try {
+ tempFile.remove();
+ } catch (ex) {}
+
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: copyMessageToFolder: Triggering deletion from OnStopCopy()\n"
+ );
+ this.applyFlags();
+
+ if (deleteOrigMsg) {
+ lazy.EnigmailLog.DEBUG(
+ `persistentCrypto.jsm: copyMessageToFolder: Deleting old message with key ${originalMsgHdr.messageKey}\n`
+ );
+ msgFolder.deleteMessages(
+ [originalMsgHdr],
+ null,
+ true,
+ false,
+ null,
+ false
+ );
+ }
+ resolve(newKey);
+ },
+ };
+
+ MailServices.copy.copyFileMessage(
+ tempFile,
+ destFolder,
+ null,
+ false,
+ originalMsgHdr.flags,
+ "",
+ copyListener,
+ null
+ );
+ });
+ },
+};
+
+function CryptMessageIntoFolder(destFolder, move, targetKey) {
+ this.destFolder = destFolder;
+ this.move = move;
+ this.targetKey = targetKey;
+ this.cryptoChanged = false;
+ this.decryptFailure = false;
+
+ this.mimeTree = null;
+ this.decryptionTasks = [];
+ this.subject = "";
+}
+
+CryptMessageIntoFolder.prototype = {
+ /** Here is the effective action of a call to cryptMessage.
+ * If no failure is seen when attempting to decrypt (!decryptFailure),
+ * then we copy. (This includes plain messages that didn't need
+ * decryption.)
+ * The cryptoChanged flag is set only after we have successfully
+ * completed a decryption (or encryption) operation, it's used to
+ * decide whether we need a new message ID.
+ */
+ async messageParseCallback(mimeTree, msgHdr) {
+ this.mimeTree = mimeTree;
+ this.hdr = msgHdr;
+
+ if (mimeTree.headers.has("subject")) {
+ this.subject = mimeTree.headers.get("subject");
+ }
+
+ await this.decryptMimeTree(mimeTree);
+
+ let msg = "";
+
+ // Encrypt the message if a target key is given.
+ if (this.targetKey) {
+ msg = this.encryptToKey(mimeTree);
+ if (!msg) {
+ throw new Error("Failure to encrypt message");
+ }
+ this.cryptoChanged = true;
+ } else {
+ msg = this.mimeToString(mimeTree, true);
+ }
+
+ if (this.decryptFailure) {
+ throw new Error("Failure to decrypt message");
+ }
+ return EnigmailPersistentCrypto.copyMessageToFolder(
+ this.hdr,
+ this.destFolder,
+ this.move,
+ msg,
+ this.cryptoChanged ? "decrypted-" + new Date().valueOf() : null
+ );
+ },
+
+ encryptToKey(mimeTree) {
+ let exitCodeObj = {};
+ let statusFlagsObj = {};
+ let errorMsgObj = {};
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: Encrypting message.\n");
+
+ let inputMsg = this.mimeToString(mimeTree, false);
+
+ let encmsg = "";
+ try {
+ encmsg = lazy.EnigmailEncryption.encryptMessage(
+ null,
+ 0,
+ inputMsg,
+ "0x" + this.targetKey.fpr,
+ "0x" + this.targetKey.fpr,
+ "",
+ lazy.EnigmailConstants.SEND_ENCRYPTED |
+ lazy.EnigmailConstants.SEND_ALWAYS_TRUST,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ );
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: Encryption failed: " + ex + "\n"
+ );
+ return null;
+ }
+
+ // Build the pgp-encrypted mime structure
+ let msg = "";
+
+ let rfc822Headers = []; // FIXME
+
+ // First the original headers
+ for (let header in rfc822Headers) {
+ if (
+ header != "content-type" &&
+ header != "content-transfer-encoding" &&
+ header != "content-disposition"
+ ) {
+ msg += prettyPrintHeader(header, rfc822Headers[header]) + "\n";
+ }
+ }
+ // Then multipart/encrypted ct
+ let boundary = lazy.EnigmailMime.createBoundary();
+ msg += "Content-Transfer-Encoding: 7Bit\n";
+ msg += "Content-Type: multipart/encrypted; ";
+ msg +=
+ 'boundary="' + boundary + '"; protocol="application/pgp-encrypted"\n\n';
+ msg += "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n";
+
+ // pgp-encrypted part
+ msg += "--" + boundary + "\n";
+ msg += "Content-Type: application/pgp-encrypted\n";
+ msg += "Content-Disposition: attachment\n";
+ msg += "Content-Transfer-Encoding: 7Bit\n\n";
+ msg += "Version: 1\n\n";
+
+ // the octet stream
+ msg += "--" + boundary + "\n";
+ msg += 'Content-Type: application/octet-stream; name="encrypted.asc"\n';
+ msg += "Content-Description: OpenPGP encrypted message\n";
+ msg += 'Content-Disposition: inline; filename="encrypted.asc"\n';
+ msg += "Content-Transfer-Encoding: 7Bit\n\n";
+ msg += encmsg;
+
+ // Bottom boundary
+ msg += "\n--" + boundary + "--\n";
+
+ // Fix up the line endings to be a proper dosish mail
+ msg = msg.replace(/\r/gi, "").replace(/\n/gi, "\r\n");
+
+ return msg;
+ },
+
+ /**
+ * Walk through the MIME message structure and decrypt the body if there is something to decrypt
+ */
+ async decryptMimeTree(mimePart) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: decryptMimeTree:\n");
+
+ if (this.isBrokenByExchange(mimePart)) {
+ this.fixExchangeMessage(mimePart);
+ }
+
+ if (this.isSMIME(mimePart)) {
+ this.decryptSMIME(mimePart);
+ } else if (this.isPgpMime(mimePart)) {
+ this.decryptPGPMIME(mimePart);
+ } else if (isAttachment(mimePart)) {
+ this.pgpDecryptAttachment(mimePart);
+ } else {
+ this.decryptINLINE(mimePart);
+ }
+
+ for (let i in mimePart.subParts) {
+ await this.decryptMimeTree(mimePart.subParts[i]);
+ }
+ },
+
+ /***
+ *
+ * Detect if mime part is PGP/MIME message that got modified by MS-Exchange:
+ *
+ * - multipart/mixed Container with
+ * - application/pgp-encrypted Attachment with name "PGPMIME Version Identification"
+ * - application/octet-stream Attachment with name "encrypted.asc" having the encrypted content in base64
+ * - see:
+ * - https://doesnotexist-openpgp-integration.thunderbird/forum/viewtopic.php?f=4&t=425
+ * - https://sourceforge.net/p/enigmail/forum/support/thread/4add2b69/
+ */
+
+ isBrokenByExchange(mime) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: isBrokenByExchange:\n");
+
+ try {
+ if (
+ mime.subParts &&
+ mime.subParts.length === 3 &&
+ mime.fullContentType.toLowerCase().includes("multipart/mixed") &&
+ mime.subParts[0].subParts.length === 0 &&
+ mime.subParts[0].fullContentType.search(/multipart\/encrypted/i) < 0 &&
+ mime.subParts[0].fullContentType.toLowerCase().includes("text/plain") &&
+ mime.subParts[1].fullContentType
+ .toLowerCase()
+ .includes("application/pgp-encrypted") &&
+ mime.subParts[1].fullContentType
+ .toLowerCase()
+ .search(/multipart\/encrypted/i) < 0 &&
+ mime.subParts[1].fullContentType
+ .toLowerCase()
+ .search(/PGPMIME Versions? Identification/i) >= 0 &&
+ mime.subParts[2].fullContentType
+ .toLowerCase()
+ .includes("application/octet-stream") &&
+ mime.subParts[2].fullContentType.toLowerCase().includes("encrypted.asc")
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: isBrokenByExchange: found message broken by MS-Exchange\n"
+ );
+ return true;
+ }
+ } catch (ex) {}
+
+ return false;
+ },
+
+ decryptSMIME(mimePart) {
+ let encrypted = lazy.MailCryptoUtils.binaryStringToTypedArray(
+ mimePart.body
+ );
+
+ let cmsDecoderJS = Cc["@mozilla.org/nsCMSDecoderJS;1"].createInstance(
+ Ci.nsICMSDecoderJS
+ );
+ let decrypted = cmsDecoderJS.decrypt(encrypted);
+
+ if (decrypted.length === 0) {
+ // fail if no data found
+ this.decryptFailure = true;
+ return;
+ }
+
+ let data = "";
+ for (let c of decrypted) {
+ data += String.fromCharCode(c);
+ }
+
+ if (lazy.EnigmailLog.getLogLevel() > 5) {
+ lazy.EnigmailLog.DEBUG(
+ "*** start data ***\n'" + data + "'\n***end data***\n"
+ );
+ }
+
+ // Search for the separator between headers and message body.
+ let bodyIndex = data.search(/\n\s*\r?\n/);
+ if (bodyIndex < 0) {
+ // not found, body starts at beginning.
+ bodyIndex = 0;
+ } else {
+ // found, body starts after the headers.
+ let wsSize = data.match(/\n\s*\r?\n/);
+ bodyIndex += wsSize[0].length;
+ }
+
+ if (data.substr(bodyIndex).search(/\r?\n$/) === 0) {
+ return;
+ }
+
+ let m = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ // headers are found from the beginning up to the start of the body
+ m.initialize(data.substr(0, bodyIndex));
+
+ mimePart.headers._rawHeaders.set("content-type", [
+ m.extractHeader("content-type", false) || "",
+ ]);
+
+ mimePart.headers._rawHeaders.delete("content-transfer-encoding");
+ mimePart.headers._rawHeaders.delete("content-disposition");
+ mimePart.headers._rawHeaders.delete("content-description");
+
+ mimePart.subParts = [];
+ mimePart.body = data.substr(bodyIndex);
+
+ this.cryptoChanged = true;
+ },
+
+ isSMIME(mimePart) {
+ if (!mimePart.headers.has("content-type")) {
+ return false;
+ }
+
+ return (
+ mimePart.headers.get("content-type").type.toLowerCase() ===
+ "application/pkcs7-mime" &&
+ mimePart.headers.get("content-type").get("smime-type").toLowerCase() ===
+ "enveloped-data" &&
+ mimePart.subParts.length === 0
+ );
+ },
+
+ isPgpMime(mimePart) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: isPgpMime()\n");
+
+ try {
+ if (mimePart.headers.has("content-type")) {
+ if (
+ mimePart.headers.get("content-type").type.toLowerCase() ===
+ "multipart/encrypted" &&
+ mimePart.headers.get("content-type").get("protocol").toLowerCase() ===
+ "application/pgp-encrypted" &&
+ mimePart.subParts.length === 2
+ ) {
+ return true;
+ }
+ }
+ } catch (x) {}
+ return false;
+ },
+
+ async decryptPGPMIME(mimePart) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: decryptPGPMIME(" + mimePart.partNum + ")\n"
+ );
+
+ if (!mimePart.subParts[1]) {
+ throw new Error("Not a correct PGP/MIME message");
+ }
+
+ const uiFlags =
+ lazy.EnigmailConstants.UI_INTERACTIVE |
+ lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK |
+ lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR;
+ let exitCodeObj = {};
+ let statusFlagsObj = {};
+ let userIdObj = {};
+ let sigDetailsObj = {};
+ let errorMsgObj = {};
+ let keyIdObj = {};
+ let blockSeparationObj = {
+ value: "",
+ };
+ let encToDetailsObj = {};
+ var signatureObj = {};
+ signatureObj.value = "";
+
+ let data = lazy.EnigmailDecryption.decryptMessage(
+ null,
+ uiFlags,
+ mimePart.subParts[1].body,
+ null, // date
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ );
+
+ if (!data || data.length === 0) {
+ if (statusFlagsObj.value & lazy.EnigmailConstants.DISPLAY_MESSAGE) {
+ lazy.EnigmailDialog.alert(null, errorMsgObj.value);
+ throw new Error("Decryption impossible");
+ }
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: analyzeDecryptedData: got " +
+ data.length +
+ " bytes\n"
+ );
+
+ if (lazy.EnigmailLog.getLogLevel() > 5) {
+ lazy.EnigmailLog.DEBUG(
+ "*** start data ***\n'" + data + "'\n***end data***\n"
+ );
+ }
+
+ if (data.length === 0) {
+ // fail if no data found
+ this.decryptFailure = true;
+ return;
+ }
+
+ let bodyIndex = data.search(/\n\s*\r?\n/);
+ if (bodyIndex < 0) {
+ bodyIndex = 0;
+ } else {
+ let wsSize = data.match(/\n\s*\r?\n/);
+ bodyIndex += wsSize[0].length;
+ }
+
+ if (data.substr(bodyIndex).search(/\r?\n$/) === 0) {
+ return;
+ }
+
+ let m = Cc["@mozilla.org/messenger/mimeheaders;1"].createInstance(
+ Ci.nsIMimeHeaders
+ );
+ m.initialize(data.substr(0, bodyIndex));
+ let ct = m.extractHeader("content-type", false) || "";
+ let part = mimePart.partNum;
+
+ if (part.length > 0 && part.search(/[^01.]/) < 0) {
+ if (ct.search(/protected-headers/i) >= 0) {
+ if (m.hasHeader("subject")) {
+ let subject = m.extractHeader("subject", false) || "";
+ subject = subject.replace(/^(Re: )+/, "Re: ");
+ this.mimeTree.headers._rawHeaders.set("subject", [subject]);
+ }
+ } else if (this.mimeTree.headers.get("subject") === "p≡p") {
+ let subject = getPepSubject(data);
+ if (subject) {
+ subject = subject.replace(/^(Re: )+/, "Re: ");
+ this.mimeTree.headers._rawHeaders.set("subject", [subject]);
+ }
+ } else if (
+ !(statusFlagsObj.value & lazy.EnigmailConstants.GOOD_SIGNATURE) &&
+ /^multipart\/signed/i.test(ct)
+ ) {
+ // RFC 3156, Section 6.1 message
+ let innerMsg = lazy.EnigmailMime.getMimeTree(data, false);
+ if (innerMsg.subParts.length > 0) {
+ ct = innerMsg.subParts[0].fullContentType;
+ let hdrMap = innerMsg.subParts[0].headers._rawHeaders;
+ if (ct.search(/protected-headers/i) >= 0 && hdrMap.has("subject")) {
+ let subject = innerMsg.subParts[0].headers._rawHeaders
+ .get("subject")
+ .join("");
+ subject = subject.replace(/^(Re: )+/, "Re: ");
+ this.mimeTree.headers._rawHeaders.set("subject", [subject]);
+ }
+ }
+ }
+ }
+
+ let boundary = getBoundary(mimePart);
+ if (!boundary) {
+ boundary = lazy.EnigmailMime.createBoundary();
+ }
+
+ // append relevant headers
+ mimePart.headers.get("content-type").type = "multipart/mixed";
+ mimePart.headers._rawHeaders.set("content-type", [
+ 'multipart/mixed; boundary="' + boundary + '"',
+ ]);
+ mimePart.subParts = [
+ {
+ body: data,
+ decryptedPgpMime: true,
+ partNum: mimePart.partNum + ".1",
+ headers: {
+ _rawHeaders: new Map(),
+ get() {
+ return null;
+ },
+ has() {
+ return false;
+ },
+ },
+ subParts: [],
+ },
+ ];
+
+ this.cryptoChanged = true;
+ },
+
+ pgpDecryptAttachment(mimePart) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: pgpDecryptAttachment()\n");
+ let attachmentHead = mimePart.body.substr(0, 30);
+ if (attachmentHead.search(/-----BEGIN PGP \w{5,10} KEY BLOCK-----/) >= 0) {
+ // attachment appears to be a PGP key file, skip
+ return;
+ }
+
+ const uiFlags =
+ lazy.EnigmailConstants.UI_INTERACTIVE |
+ lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK |
+ lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR;
+ let exitCodeObj = {};
+ let statusFlagsObj = {};
+ let userIdObj = {};
+ let sigDetailsObj = {};
+ let errorMsgObj = {};
+ let keyIdObj = {};
+ let blockSeparationObj = {
+ value: "",
+ };
+ let encToDetailsObj = {};
+ var signatureObj = {};
+ signatureObj.value = "";
+
+ let attachmentName = getAttachmentName(mimePart);
+ attachmentName = attachmentName
+ ? attachmentName.replace(/\.(pgp|asc|gpg)$/, "")
+ : "";
+
+ let data = lazy.EnigmailDecryption.decryptMessage(
+ null,
+ uiFlags,
+ mimePart.body,
+ null, // date
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ );
+
+ if (data || statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_OKAY) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: pgpDecryptAttachment: decryption OK\n"
+ );
+ } else if (
+ statusFlagsObj.value &
+ (lazy.EnigmailConstants.DECRYPTION_FAILED |
+ lazy.EnigmailConstants.MISSING_MDC)
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: pgpDecryptAttachment: decryption without MDC protection\n"
+ );
+ } else if (
+ statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_FAILED
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: pgpDecryptAttachment: decryption failed\n"
+ );
+ // Enigmail prompts the user here, but we just keep going.
+ } else if (
+ statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_INCOMPLETE
+ ) {
+ // failure; message not complete
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: pgpDecryptAttachment: decryption incomplete\n"
+ );
+ return;
+ } else {
+ // there is nothing to be decrypted
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: pgpDecryptAttachment: no decryption required\n"
+ );
+ return;
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: pgpDecryptAttachment: decrypted to " +
+ data.length +
+ " bytes\n"
+ );
+ if (statusFlagsObj.encryptedFileName) {
+ attachmentName = statusFlagsObj.encryptedFileName;
+ }
+
+ this.decryptedMessage = true;
+ mimePart.body = data;
+ mimePart.headers._rawHeaders.set(
+ "content-disposition",
+ `attachment; filename="${attachmentName}"`
+ );
+ mimePart.headers._rawHeaders.set("content-transfer-encoding", ["base64"]);
+ let origCt = mimePart.headers.get("content-type");
+ let ct = origCt.type;
+
+ for (let i of origCt.entries()) {
+ if (i[0].toLowerCase() === "name") {
+ i[1] = i[1].replace(/\.(pgp|asc|gpg)$/, "");
+ }
+ ct += `; ${i[0]}="${i[1]}"`;
+ }
+
+ mimePart.headers._rawHeaders.set("content-type", [ct]);
+ },
+
+ async decryptINLINE(mimePart) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: decryptINLINE()\n");
+
+ if ("decryptedPgpMime" in mimePart && mimePart.decryptedPgpMime) {
+ return 0;
+ }
+
+ if ("body" in mimePart && mimePart.body.length > 0) {
+ let ct = getContentType(mimePart);
+
+ if (ct === "text/html") {
+ mimePart.body = this.stripHTMLFromArmoredBlocks(mimePart.body);
+ }
+
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var userIdObj = {};
+ var sigDetailsObj = {};
+ var errorMsgObj = {};
+ var keyIdObj = {};
+ var blockSeparationObj = {
+ value: "",
+ };
+ var encToDetailsObj = {};
+ var signatureObj = {};
+ signatureObj.value = "";
+
+ const uiFlags =
+ lazy.EnigmailConstants.UI_INTERACTIVE |
+ lazy.EnigmailConstants.UI_UNVERIFIED_ENC_OK |
+ lazy.EnigmailConstants.UI_IGNORE_MDC_ERROR;
+
+ var plaintexts = [];
+ var blocks = lazy.EnigmailArmor.locateArmoredBlocks(mimePart.body);
+ var tmp = [];
+
+ for (let i = 0; i < blocks.length; i++) {
+ if (blocks[i].blocktype == "MESSAGE") {
+ tmp.push(blocks[i]);
+ }
+ }
+
+ blocks = tmp;
+
+ if (blocks.length < 1) {
+ return 0;
+ }
+
+ let charset = "utf-8";
+
+ for (let i = 0; i < blocks.length; i++) {
+ let plaintext = null;
+ do {
+ let ciphertext = mimePart.body.substring(
+ blocks[i].begin,
+ blocks[i].end + 1
+ );
+
+ if (ciphertext.length === 0) {
+ break;
+ }
+
+ let hdr = ciphertext.search(/(\r\r|\n\n|\r\n\r\n)/);
+ if (hdr > 0) {
+ let chset = ciphertext.substr(0, hdr).match(/^(charset:)(.*)$/im);
+ if (chset && chset.length == 3) {
+ charset = chset[2].trim();
+ }
+ }
+ plaintext = lazy.EnigmailDecryption.decryptMessage(
+ null,
+ uiFlags,
+ ciphertext,
+ null, // date
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ );
+ if (!plaintext || plaintext.length === 0) {
+ if (statusFlagsObj.value & lazy.EnigmailConstants.DISPLAY_MESSAGE) {
+ lazy.EnigmailDialog.alert(null, errorMsgObj.value);
+ this.cryptoChanged = false;
+ this.decryptFailure = true;
+ return -1;
+ }
+
+ if (
+ statusFlagsObj.value &
+ (lazy.EnigmailConstants.DECRYPTION_FAILED |
+ lazy.EnigmailConstants.MISSING_MDC)
+ ) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: decryptINLINE: no MDC protection, decrypting anyway\n"
+ );
+ }
+ if (
+ statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_FAILED
+ ) {
+ // since we cannot find out if the user wants to cancel
+ // we should ask
+ let msg = await lazy.l10n.formatValue(
+ "converter-decrypt-body-failed",
+ {
+ subject: this.subject,
+ }
+ );
+
+ if (
+ !lazy.EnigmailDialog.confirmDlg(
+ null,
+ msg,
+ lazy.l10n.formatValueSync("dlg-button-retry"),
+ lazy.l10n.formatValueSync("dlg-button-skip")
+ )
+ ) {
+ this.cryptoChanged = false;
+ this.decryptFailure = true;
+ return -1;
+ }
+ } else if (
+ statusFlagsObj.value &
+ lazy.EnigmailConstants.DECRYPTION_INCOMPLETE
+ ) {
+ this.cryptoChanged = false;
+ this.decryptFailure = true;
+ return -1;
+ } else {
+ plaintext = " ";
+ }
+ }
+
+ if (ct === "text/html") {
+ plaintext = plaintext.replace(/\n/gi, "<br/>\n");
+ }
+
+ let subject = "";
+ if (this.mimeTree.headers.has("subject")) {
+ subject = this.mimeTree.headers.get("subject");
+ }
+
+ if (
+ i == 0 &&
+ subject === "pEp" &&
+ mimePart.partNum.length > 0 &&
+ mimePart.partNum.search(/[^01.]/) < 0
+ ) {
+ let m = lazy.EnigmailMime.extractSubjectFromBody(plaintext);
+ if (m) {
+ plaintext = m.messageBody;
+ this.mimeTree.headers._rawHeaders.set("subject", [m.subject]);
+ }
+ }
+
+ if (plaintext) {
+ plaintexts.push(plaintext);
+ }
+ } while (!plaintext || plaintext === "");
+ }
+
+ var decryptedMessage =
+ mimePart.body.substring(0, blocks[0].begin) + plaintexts[0];
+ for (let i = 1; i < blocks.length; i++) {
+ decryptedMessage +=
+ mimePart.body.substring(blocks[i - 1].end + 1, blocks[i].begin + 1) +
+ plaintexts[i];
+ }
+
+ decryptedMessage += mimePart.body.substring(
+ blocks[blocks.length - 1].end + 1
+ );
+
+ // enable base64 encoding if non-ASCII character(s) found
+ let j = decryptedMessage.search(/[^\x01-\x7F]/); // eslint-disable-line no-control-regex
+ if (j >= 0) {
+ mimePart.headers._rawHeaders.set("content-transfer-encoding", [
+ "base64",
+ ]);
+ } else {
+ mimePart.headers._rawHeaders.set("content-transfer-encoding", ["8bit"]);
+ }
+ mimePart.body = decryptedMessage;
+
+ let origCharset = getCharset(mimePart, "content-type");
+ if (origCharset) {
+ mimePart.headers_rawHeaders.set(
+ "content-type",
+ getHeaderValue(mimePart, "content-type").replace(origCharset, charset)
+ );
+ } else {
+ mimePart.headers._rawHeaders.set(
+ "content-type",
+ getHeaderValue(mimePart, "content-type") + "; charset=" + charset
+ );
+ }
+
+ this.cryptoChanged = true;
+ return 1;
+ }
+
+ let ct = getContentType(mimePart);
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: Decryption skipped: " + ct + "\n"
+ );
+
+ return 0;
+ },
+
+ stripHTMLFromArmoredBlocks(text) {
+ var index = 0;
+ var begin = text.indexOf("-----BEGIN PGP");
+ var end = text.indexOf("-----END PGP");
+
+ while (begin > -1 && end > -1) {
+ let sub = text.substring(begin, end);
+
+ sub = sub.replace(/(<([^>]+)>)/gi, "");
+ sub = sub.replace(/&[A-z]+;/gi, "");
+
+ text = text.substring(0, begin) + sub + text.substring(end);
+
+ index = end + 10;
+ begin = text.indexOf("-----BEGIN PGP", index);
+ end = text.indexOf("-----END PGP", index);
+ }
+
+ return text;
+ },
+
+ /******
+ *
+ * We have the technology we can rebuild.
+ *
+ * Function to reassemble the message from the MIME Tree
+ * into a String.
+ *
+ ******/
+
+ mimeToString(mimePart, includeHeaders) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: mimeToString: part: '" + mimePart.partNum + "'\n"
+ );
+
+ let msg = "";
+ let rawHdr = mimePart.headers._rawHeaders;
+
+ if (includeHeaders && rawHdr.size > 0) {
+ for (let hdr of rawHdr.keys()) {
+ let formatted = formatMimeHeader(hdr, rawHdr.get(hdr));
+ msg += formatted;
+ if (!formatted.endsWith("\r\n")) {
+ msg += "\r\n";
+ }
+ }
+
+ msg += "\r\n";
+ }
+
+ if (mimePart.body.length > 0) {
+ let encoding = getTransferEncoding(mimePart);
+ if (!encoding) {
+ encoding = "8bit";
+ }
+
+ if (encoding === "base64") {
+ msg += lazy.EnigmailData.encodeBase64(mimePart.body);
+ } else {
+ let charset = getCharset(mimePart, "content-type");
+ if (charset) {
+ msg += lazy.EnigmailData.convertFromUnicode(mimePart.body, charset);
+ } else {
+ msg += mimePart.body;
+ }
+ }
+ }
+
+ if (mimePart.subParts.length > 0) {
+ let boundary = lazy.EnigmailMime.getBoundary(
+ rawHdr.get("content-type").join("")
+ );
+
+ for (let i in mimePart.subParts) {
+ msg += `--${boundary}\r\n`;
+ msg += this.mimeToString(mimePart.subParts[i], true);
+ if (msg.search(/[\r\n]$/) < 0) {
+ msg += "\r\n";
+ }
+ msg += "\r\n";
+ }
+
+ msg += `--${boundary}--\r\n`;
+ }
+ return msg;
+ },
+
+ fixExchangeMessage(mimePart) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: fixExchangeMessage()\n");
+
+ let msg = this.mimeToString(mimePart, true);
+
+ try {
+ let fixedMsg = lazy.EnigmailFixExchangeMsg.getRepairedMessage(msg);
+ let replacement = lazy.EnigmailMime.getMimeTree(fixedMsg, true);
+
+ for (let i in replacement) {
+ mimePart[i] = replacement[i];
+ }
+ } catch (ex) {}
+ },
+};
+
+/**
+ * Format a mime header
+ *
+ * e.g. content-type -> Content-Type
+ */
+
+function formatHeader(headerLabel) {
+ return headerLabel.replace(/^.|(-.)/g, function (match) {
+ return match.toUpperCase();
+ });
+}
+
+function formatMimeHeader(headerLabel, headerValue) {
+ if (Array.isArray(headerValue)) {
+ return headerValue
+ .map(v => formatHeader(headerLabel) + ": " + v)
+ .join("\r\n");
+ }
+ return formatHeader(headerLabel) + ": " + headerValue + "\r\n";
+}
+
+function prettyPrintHeader(headerLabel, headerData) {
+ if (Array.isArray(headerData)) {
+ let h = [];
+ for (let i in headerData) {
+ h.push(
+ formatMimeHeader(headerLabel, lazy.GlodaUtils.deMime(headerData[i]))
+ );
+ }
+ return h.join("\r\n");
+ }
+ return formatMimeHeader(
+ headerLabel,
+ lazy.GlodaUtils.deMime(String(headerData))
+ );
+}
+
+function getHeaderValue(mimeStruct, header) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: getHeaderValue: '" + header + "'\n"
+ );
+
+ try {
+ if (mimeStruct.headers.has(header)) {
+ let hdrVal = mimeStruct.headers.get(header);
+ if (typeof hdrVal == "string") {
+ return hdrVal;
+ }
+ return mimeStruct.headers[header].join(" ");
+ }
+ return "";
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: getHeaderValue: header not present\n"
+ );
+ return "";
+ }
+}
+
+function getContentType(mime) {
+ try {
+ if (mime && "headers" in mime && mime.headers.has("content-type")) {
+ return mime.headers.get("content-type").type.toLowerCase();
+ }
+ } catch (e) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getContentType: " + e + "\n");
+ }
+ return null;
+}
+
+// return the content of the boundary parameter
+function getBoundary(mime) {
+ try {
+ if (mime && "headers" in mime && mime.headers.has("content-type")) {
+ return mime.headers.get("content-type").get("boundary");
+ }
+ } catch (e) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getBoundary: " + e + "\n");
+ }
+ return null;
+}
+
+function getCharset(mime) {
+ try {
+ if (mime && "headers" in mime && mime.headers.has("content-type")) {
+ let c = mime.headers.get("content-type").get("charset");
+ if (c) {
+ return c.toLowerCase();
+ }
+ }
+ } catch (e) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getCharset: " + e + "\n");
+ }
+ return null;
+}
+
+function getTransferEncoding(mime) {
+ try {
+ if (
+ mime &&
+ "headers" in mime &&
+ mime.headers._rawHeaders.has("content-transfer-encoding")
+ ) {
+ let c = mime.headers._rawHeaders.get("content-transfer-encoding")[0];
+ if (c) {
+ return c.toLowerCase();
+ }
+ }
+ } catch (e) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: getTransferEncoding: " + e + "\n"
+ );
+ }
+ return "8Bit";
+}
+
+function isAttachment(mime) {
+ try {
+ if (mime && "headers" in mime) {
+ if (mime.fullContentType.search(/^multipart\//i) === 0) {
+ return false;
+ }
+ if (mime.fullContentType.search(/^text\//i) < 0) {
+ return true;
+ }
+
+ if (mime.headers.has("content-disposition")) {
+ let c = mime.headers.get("content-disposition")[0];
+ if (c) {
+ if (c.search(/^attachment/i) === 0) {
+ return true;
+ }
+ }
+ }
+ }
+ } catch (x) {}
+ return false;
+}
+
+/**
+ * If the given MIME part is an attachment, return its filename.
+ *
+ * @param mime: a MIME part
+ * @return: the filename or null
+ */
+function getAttachmentName(mime) {
+ if ("headers" in mime && mime.headers.has("content-disposition")) {
+ let c = mime.headers.get("content-disposition")[0];
+ if (/^attachment/i.test(c)) {
+ return lazy.EnigmailMime.getParameter(c, "filename");
+ }
+ }
+ return null;
+}
+
+function getPepSubject(mimeString) {
+ lazy.EnigmailLog.DEBUG("persistentCrypto.jsm: getPepSubject()\n");
+
+ let subject = null;
+
+ let emitter = {
+ ct: "",
+ firstPlainText: false,
+ startPart(partNum, headers) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: getPepSubject.startPart: partNum=" +
+ partNum +
+ "\n"
+ );
+ try {
+ this.ct = String(headers.getRawHeader("content-type")).toLowerCase();
+ if (!subject && !this.firstPlainText) {
+ let s = headers.getRawHeader("subject");
+ if (s) {
+ subject = String(s);
+ this.firstPlainText = true;
+ }
+ }
+ } catch (ex) {
+ this.ct = "";
+ }
+ },
+
+ endPart(partNum) {},
+
+ deliverPartData(partNum, data) {
+ lazy.EnigmailLog.DEBUG(
+ "persistentCrypto.jsm: getPepSubject.deliverPartData: partNum=" +
+ partNum +
+ " ct=" +
+ this.ct +
+ "\n"
+ );
+ if (!this.firstPlainText && this.ct.search(/^text\/plain/) === 0) {
+ // check data
+ this.firstPlainText = true;
+
+ let o = lazy.EnigmailMime.extractSubjectFromBody(data);
+ if (o) {
+ subject = o.subject;
+ }
+ }
+ },
+ };
+
+ let opt = {
+ strformat: "unicode",
+ bodyformat: "decode",
+ };
+
+ try {
+ let p = new lazy.jsmime.MimeParser(emitter, opt);
+ p.deliverData(mimeString);
+ } catch (ex) {}
+
+ return subject;
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/pgpmimeHandler.jsm b/comm/mail/extensions/openpgp/content/modules/pgpmimeHandler.jsm
new file mode 100644
index 0000000000..7c16489aa5
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/pgpmimeHandler.jsm
@@ -0,0 +1,299 @@
+/* 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";
+
+/**
+ * Module for handling PGP/MIME encrypted and/or signed messages
+ * implemented as an XPCOM object
+ */
+
+const EXPORTED_SYMBOLS = ["EnigmailPgpmimeHander"];
+
+const { manager: Cm } = Components;
+Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailMimeDecrypt: "chrome://openpgp/content/modules/mimeDecrypt.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailWksMimeHandler: "chrome://openpgp/content/modules/wksMimeHandler.jsm",
+});
+
+const PGPMIME_JS_DECRYPTOR_CONTRACTID =
+ "@mozilla.org/mime/pgp-mime-js-decrypt;1";
+const PGPMIME_JS_DECRYPTOR_CID = Components.ID(
+ "{7514cbeb-2bfd-4b2c-829b-1a4691fa0ac8}"
+);
+
+////////////////////////////////////////////////////////////////////
+// handler for PGP/MIME encrypted and PGP/MIME signed messages
+// data is processed from libmime -> nsPgpMimeProxy
+
+var gConv;
+var inStream;
+
+var gLastEncryptedUri = "";
+
+const throwErrors = {
+ onDataAvailable() {
+ throw new Error("error");
+ },
+ onStartRequest() {
+ throw new Error("error");
+ },
+ onStopRequest() {
+ throw new Error("error");
+ },
+};
+
+/**
+ * UnknownProtoHandler is a default handler for unknown protocols. It ensures that the
+ * signed message part is always displayed without any further action.
+ */
+function UnknownProtoHandler() {
+ if (!gConv) {
+ gConv = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ }
+
+ if (!inStream) {
+ inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ }
+}
+
+UnknownProtoHandler.prototype = {
+ onStartRequest(request, ctxt) {
+ this.mimeSvc = request.QueryInterface(Ci.nsIPgpMimeProxy);
+ if (!("outputDecryptedData" in this.mimeSvc)) {
+ this.mimeSvc.onStartRequest(null, ctxt);
+ }
+ this.bound = lazy.EnigmailMime.getBoundary(this.mimeSvc.contentType);
+ /*
+ readMode:
+ 0: before message
+ 1: inside message
+ 2: after message
+ */
+ this.readMode = 0;
+ },
+
+ onDataAvailable(p1, p2, p3, p4) {
+ this.processData(p1, p2, p3, p4);
+ },
+
+ processData(req, stream, offset, count) {
+ if (count > 0) {
+ inStream.init(stream);
+ let data = inStream.read(count);
+ let l = data.replace(/\r\n/g, "\n").split(/\n/);
+
+ if (data.search(/\n$/) >= 0) {
+ l.pop();
+ }
+
+ let startIndex = 0;
+ let endIndex = l.length;
+
+ if (this.readMode < 2) {
+ for (let i = 0; i < l.length; i++) {
+ if (l[i].indexOf("--") === 0 && l[i].indexOf(this.bound) === 2) {
+ ++this.readMode;
+ if (this.readMode === 1) {
+ startIndex = i + 1;
+ } else if (this.readMode === 2) {
+ endIndex = i - 1;
+ }
+ }
+ }
+
+ if (this.readMode >= 1 && startIndex < l.length) {
+ let out = l.slice(startIndex, endIndex).join("\n") + "\n";
+
+ if ("outputDecryptedData" in this.mimeSvc) {
+ // TB >= 57
+ this.mimeSvc.outputDecryptedData(out, out.length);
+ } else {
+ gConv.setData(out, out.length);
+ this.mimeSvc.onDataAvailable(null, null, gConv, 0, out.length);
+ }
+ }
+ }
+ }
+ },
+
+ onStopRequest() {
+ if (!("outputDecryptedData" in this.mimeSvc)) {
+ this.mimeSvc.onStopRequest(null, 0);
+ }
+ },
+};
+
+function PgpMimeHandler() {
+ lazy.EnigmailLog.DEBUG("pgpmimeHandler.js: PgpMimeHandler()\n"); // always log this one
+}
+
+PgpMimeHandler.prototype = {
+ classDescription: "Enigmail JS Decryption Handler",
+ classID: PGPMIME_JS_DECRYPTOR_CID,
+ contractID: PGPMIME_JS_DECRYPTOR_CONTRACTID,
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ inStream: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+
+ onStartRequest(request, ctxt) {
+ let mimeSvc = request.QueryInterface(Ci.nsIPgpMimeProxy);
+ let ct = mimeSvc.contentType;
+
+ let uri = null;
+ if ("messageURI" in mimeSvc) {
+ uri = mimeSvc.messageURI;
+ } else {
+ uri = ctxt;
+ }
+
+ if (!lazy.EnigmailCore.getService()) {
+ // Ensure Enigmail is initialized
+ if (ct.search(/application\/(x-)?pkcs7-signature/i) > 0) {
+ return this.handleSmime(uri);
+ }
+ return null;
+ }
+
+ lazy.EnigmailLog.DEBUG("pgpmimeHandler.js: onStartRequest\n");
+ lazy.EnigmailLog.DEBUG("pgpmimeHandler.js: ct= " + ct + "\n");
+
+ let cth = null;
+
+ if (ct.search(/^multipart\/encrypted/i) === 0) {
+ if (uri) {
+ let u = uri.QueryInterface(Ci.nsIURI);
+ gLastEncryptedUri = u.spec;
+ }
+ // PGP/MIME encrypted message
+
+ cth = lazy.EnigmailMimeDecrypt.newPgpMimeHandler();
+ } else if (ct.search(/^multipart\/signed/i) === 0) {
+ if (ct.search(/application\/pgp-signature/i) > 0) {
+ // PGP/MIME signed message
+ cth = lazy.EnigmailVerify.newVerifier();
+ } else if (ct.search(/application\/(x-)?pkcs7-signature/i) > 0) {
+ let lastUriSpec = "";
+ if (uri) {
+ let u = uri.QueryInterface(Ci.nsIURI);
+ lastUriSpec = u.spec;
+ }
+ // S/MIME signed message
+ if (
+ lastUriSpec !== gLastEncryptedUri &&
+ lazy.EnigmailVerify.lastWindow
+ ) {
+ // if message is displayed then handle like S/MIME message
+ return this.handleSmime(uri);
+ }
+
+ // otherwise just make sure message body is returned
+ cth = lazy.EnigmailVerify.newVerifier(
+ "application/(x-)?pkcs7-signature"
+ );
+ }
+ } else if (ct.search(/application\/vnd.gnupg.wks/i) === 0) {
+ cth = lazy.EnigmailWksMimeHandler.newHandler();
+ }
+
+ if (!cth) {
+ lazy.EnigmailLog.ERROR(
+ "pgpmimeHandler.js: unknown protocol for content-type: " + ct + "\n"
+ );
+ cth = new UnknownProtoHandler();
+ }
+
+ if (cth) {
+ this.onDataAvailable = cth.onDataAvailable.bind(cth);
+ this.onStopRequest = cth.onStopRequest.bind(cth);
+ return cth.onStartRequest(request, uri);
+ }
+
+ return null;
+ },
+
+ onDataAvailable(req, stream, offset, count) {},
+
+ onStopRequest(request, status) {},
+
+ handleSmime(uri) {
+ this.contentHandler = throwErrors;
+
+ if (uri) {
+ uri = uri.QueryInterface(Ci.nsIURI);
+ }
+
+ let headerSink = lazy.EnigmailSingletons.messageReader;
+ headerSink?.handleSMimeMessage(uri);
+ },
+
+ getMessengerWindow() {
+ let windowManager = Services.wm;
+
+ for (let win of windowManager.getEnumerator(null)) {
+ if (win.location.href.search(/\/messenger.xhtml$/) > 0) {
+ return win;
+ }
+ }
+
+ return null;
+ },
+};
+
+class Factory {
+ constructor(component) {
+ this.component = component;
+ this.register();
+ Object.freeze(this);
+ }
+
+ createInstance(iid) {
+ return new this.component();
+ }
+
+ register() {
+ Cm.registerFactory(
+ this.component.prototype.classID,
+ this.component.prototype.classDescription,
+ this.component.prototype.contractID,
+ this
+ );
+ }
+
+ unregister() {
+ Cm.unregisterFactory(this.component.prototype.classID, this);
+ }
+}
+
+var EnigmailPgpmimeHander = {
+ startup(reason) {
+ try {
+ this.factory = new Factory(PgpMimeHandler);
+ } catch (ex) {}
+ },
+
+ shutdown(reason) {
+ if (this.factory) {
+ this.factory.unregister();
+ }
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/singletons.jsm b/comm/mail/extensions/openpgp/content/modules/singletons.jsm
new file mode 100644
index 0000000000..eb1d6f45df
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/singletons.jsm
@@ -0,0 +1,54 @@
+/* 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 = ["EnigmailSingletons"];
+
+var EnigmailSingletons = {
+ // handle to most recent message reader window
+ messageReader: null,
+
+ // information about the last PGP/MIME decrypted message (mimeDecrypt)
+ lastDecryptedMessage: {},
+ lastMessageDecryptTime: 0,
+
+ clearLastDecryptedMessage() {
+ let lm = this.lastDecryptedMessage;
+ lm.lastMessageData = "";
+ lm.lastMessageURI = null;
+ lm.mimePartNumber = "";
+ lm.lastStatus = {};
+ lm.gossip = [];
+ },
+
+ isLastDecryptedMessagePart(folder, msgNum, mimePartNumber) {
+ let reval =
+ this.lastDecryptedMessage.lastMessageURI &&
+ this.lastDecryptedMessage.lastMessageURI.folder == folder &&
+ this.lastDecryptedMessage.lastMessageURI.msgNum == msgNum &&
+ this.lastDecryptedMessage.mimePartNumber == mimePartNumber;
+ return reval;
+ },
+
+ urisWithNestedEncryptedParts: [],
+
+ maxRecentSubEncryptionUrisToRemember: 10,
+
+ addUriWithNestedEncryptedPart(uri) {
+ if (
+ this.urisWithNestedEncryptedParts.length >
+ this.maxRecentSubEncryptionUrisToRemember
+ ) {
+ this.urisWithNestedEncryptedParts.shift(); // remove oldest
+ }
+ this.urisWithNestedEncryptedParts.push(uri);
+ },
+
+ isRecentUriWithNestedEncryptedPart(uri) {
+ return this.urisWithNestedEncryptedParts.includes(uri);
+ },
+};
+
+EnigmailSingletons.clearLastDecryptedMessage();
diff --git a/comm/mail/extensions/openpgp/content/modules/sqliteDb.jsm b/comm/mail/extensions/openpgp/content/modules/sqliteDb.jsm
new file mode 100644
index 0000000000..29f8b9c0b8
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/sqliteDb.jsm
@@ -0,0 +1,477 @@
+/* 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";
+
+/**
+ * Module that provides generic functions for the Enigmail SQLite database
+ */
+
+const EXPORTED_SYMBOLS = ["PgpSqliteDb2"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+});
+
+var PgpSqliteDb2 = {
+ openDatabase() {
+ lazy.EnigmailLog.DEBUG("sqliteDb.jsm: PgpSqliteDb2 openDatabase()\n");
+ return new Promise((resolve, reject) => {
+ openDatabaseConn(
+ "openpgp.sqlite",
+ resolve,
+ reject,
+ 100,
+ Date.now() + 10000
+ );
+ });
+ },
+
+ async checkDatabaseStructure() {
+ lazy.EnigmailLog.DEBUG(
+ `sqliteDb.jsm: PgpSqliteDb2 checkDatabaseStructure()\n`
+ );
+ let conn;
+ try {
+ conn = await this.openDatabase();
+ await checkAcceptanceTable(conn);
+ await conn.close();
+ lazy.EnigmailLog.DEBUG(
+ `sqliteDb.jsm: PgpSqliteDb2 checkDatabaseStructure - success\n`
+ );
+ } catch (ex) {
+ lazy.EnigmailLog.ERROR(
+ `sqliteDb.jsm: PgpSqliteDb2 checkDatabaseStructure: ERROR: ${ex}\n`
+ );
+ if (conn) {
+ await conn.close();
+ }
+ throw ex;
+ }
+ },
+
+ accCacheFingerprint: "",
+ accCacheValue: "",
+ accCacheEmails: null,
+
+ async getFingerprintAcceptance(conn, fingerprint) {
+ // 40 is for modern fingerprints, 32 for older fingerprints.
+ if (fingerprint.length != 40 && fingerprint.length != 32) {
+ throw new Error(
+ "internal error, invalid fingerprint value: " + fingerprint
+ );
+ }
+
+ fingerprint = fingerprint.toLowerCase();
+ if (fingerprint == this.accCacheFingerprint) {
+ return this.accCacheValue;
+ }
+
+ let myConn = false;
+ let rv = "";
+
+ try {
+ if (!conn) {
+ myConn = true;
+ conn = await this.openDatabase();
+ }
+
+ await conn
+ .execute("select decision from acceptance_decision where fpr = :fpr", {
+ fpr: fingerprint,
+ })
+ .then(result => {
+ if (result.length) {
+ rv = result[0].getResultByName("decision");
+ }
+ });
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ if (myConn && conn) {
+ await conn.close();
+ }
+ return rv;
+ },
+
+ async hasAnyPositivelyAcceptedKeyForEmail(email) {
+ email = email.toLowerCase();
+ let count = 0;
+
+ let conn;
+ try {
+ conn = await this.openDatabase();
+
+ let result = await conn.execute(
+ "select count(decision) as hits from acceptance_email" +
+ " inner join acceptance_decision on" +
+ " acceptance_decision.fpr = acceptance_email.fpr" +
+ " where (decision = 'verified' or decision = 'unverified')" +
+ " and lower(email) = :email",
+ { email }
+ );
+ if (result.length) {
+ count = result[0].getResultByName("hits");
+ }
+ await conn.close();
+ } catch (ex) {
+ if (conn) {
+ await conn.close();
+ }
+ throw ex;
+ }
+
+ if (!count) {
+ return Boolean(await lazy.EnigmailKeyRing.getSecretKeyByEmail(email));
+ }
+ return true;
+ },
+
+ async getAcceptance(fingerprint, email, rv) {
+ fingerprint = fingerprint.toLowerCase();
+ email = email.toLowerCase();
+
+ rv.emailDecided = false;
+ rv.fingerprintAcceptance = "";
+
+ if (fingerprint == this.accCacheFingerprint) {
+ if (
+ this.accCacheValue.length &&
+ this.accCacheValue != "undecided" &&
+ this.accCacheEmails &&
+ this.accCacheEmails.has(email)
+ ) {
+ rv.emailDecided = true;
+ rv.fingerprintAcceptance = this.accCacheValue;
+ }
+ return;
+ }
+
+ let conn;
+ try {
+ conn = await this.openDatabase();
+
+ rv.fingerprintAcceptance = await this.getFingerprintAcceptance(
+ conn,
+ fingerprint
+ );
+
+ if (rv.fingerprintAcceptance) {
+ await conn
+ .execute(
+ "select count(*) from acceptance_email where fpr = :fpr and email = :email",
+ {
+ fpr: fingerprint,
+ email,
+ }
+ )
+ .then(result => {
+ if (result.length) {
+ let count = result[0].getResultByName("count(*)");
+ rv.emailDecided = count > 0;
+ }
+ });
+ }
+ await conn.close();
+ } catch (ex) {
+ if (conn) {
+ await conn.close();
+ }
+ throw ex;
+ }
+ },
+
+ // fingerprint must be lowercase already
+ async internalDeleteAcceptanceNoTransaction(conn, fingerprint) {
+ let delObj = { fpr: fingerprint };
+ await conn.execute(
+ "delete from acceptance_decision where fpr = :fpr",
+ delObj
+ );
+ await conn.execute("delete from acceptance_email where fpr = :fpr", delObj);
+ },
+
+ async deleteAcceptance(fingerprint) {
+ fingerprint = fingerprint.toLowerCase();
+ this.accCacheFingerprint = fingerprint;
+ this.accCacheValue = "";
+ this.accCacheEmails = null;
+ let conn;
+ try {
+ conn = await this.openDatabase();
+ await conn.execute("begin transaction");
+ await this.internalDeleteAcceptanceNoTransaction(conn, fingerprint);
+ await conn.execute("commit transaction");
+ await conn.close();
+ Services.obs.notifyObservers(null, "openpgp-acceptance-change");
+ } catch (ex) {
+ if (conn) {
+ await conn.close();
+ }
+ throw ex;
+ }
+ },
+
+ /**
+ * Convenience function that will add one accepted email address,
+ * either to an already accepted key, or as unverified to an undecided
+ * key. It is an error to call this API for a rejected key, or for
+ * an already accepted email address.
+ */
+ async addAcceptedEmail(fingerprint, email) {
+ fingerprint = fingerprint.toLowerCase();
+ email = email.toLowerCase();
+ let conn;
+ try {
+ conn = await this.openDatabase();
+
+ let fingerprintAcceptance = await this.getFingerprintAcceptance(
+ conn,
+ fingerprint
+ );
+
+ let fprAlreadyAccepted = false;
+
+ switch (fingerprintAcceptance) {
+ case "undecided":
+ case "":
+ case undefined:
+ break;
+
+ case "unverified":
+ case "verified":
+ fprAlreadyAccepted = true;
+ break;
+
+ default:
+ throw new Error(
+ "invalid use of addAcceptedEmail() with existing acceptance " +
+ fingerprintAcceptance
+ );
+ }
+
+ this.accCacheFingerprint = "";
+ this.accCacheValue = "";
+ this.accCacheEmails = null;
+
+ if (!fprAlreadyAccepted) {
+ await conn.execute("begin transaction");
+ // start fresh, clean up old potential email decisions
+ this.internalDeleteAcceptanceNoTransaction(conn, fingerprint);
+
+ await conn.execute(
+ "insert into acceptance_decision values (:fpr, :decision)",
+ {
+ fpr: fingerprint,
+ decision: "unverified",
+ }
+ );
+ } else {
+ await conn
+ .execute(
+ "select count(*) from acceptance_email where fpr = :fpr and email = :email",
+ {
+ fpr: fingerprint,
+ email,
+ }
+ )
+ .then(result => {
+ if (result.length && result[0].getResultByName("count(*)") > 0) {
+ throw new Error(
+ `${email} already has acceptance for ${fingerprint}`
+ );
+ }
+ });
+
+ await conn.execute("begin transaction");
+ }
+
+ await conn.execute("insert into acceptance_email values (:fpr, :email)", {
+ fpr: fingerprint,
+ email,
+ });
+
+ await conn.execute("commit transaction");
+ await conn.close();
+ Services.obs.notifyObservers(null, "openpgp-acceptance-change");
+ } catch (ex) {
+ if (conn) {
+ await conn.close();
+ }
+ throw ex;
+ }
+ },
+
+ async updateAcceptance(fingerprint, emailArray, decision) {
+ fingerprint = fingerprint.toLowerCase();
+ let conn;
+ try {
+ let uniqueEmails = new Set();
+ if (decision !== "undecided") {
+ if (emailArray) {
+ for (let email of emailArray) {
+ if (!email) {
+ continue;
+ }
+ email = email.toLowerCase();
+ if (uniqueEmails.has(email)) {
+ continue;
+ }
+ uniqueEmails.add(email);
+ }
+ }
+ }
+
+ this.accCacheFingerprint = fingerprint;
+ this.accCacheValue = decision;
+ this.accCacheEmails = uniqueEmails;
+
+ conn = await this.openDatabase();
+ await conn.execute("begin transaction");
+ await this.internalDeleteAcceptanceNoTransaction(conn, fingerprint);
+
+ if (decision !== "undecided") {
+ let decisionObj = {
+ fpr: fingerprint,
+ decision,
+ };
+ await conn.execute(
+ "insert into acceptance_decision values (:fpr, :decision)",
+ decisionObj
+ );
+
+ // Rejection is global for a fingerprint, don't need to
+ // store email address records.
+
+ if (decision !== "rejected") {
+ // A key might contain multiple user IDs with the same email
+ // address. We add each email only once.
+ for (let email of uniqueEmails) {
+ await conn.execute(
+ "insert into acceptance_email values (:fpr, :email)",
+ {
+ fpr: fingerprint,
+ email,
+ }
+ );
+ }
+ }
+ }
+ await conn.execute("commit transaction");
+ await conn.close();
+ Services.obs.notifyObservers(null, "openpgp-acceptance-change");
+ } catch (ex) {
+ if (conn) {
+ await conn.close();
+ }
+ throw ex;
+ }
+ },
+
+ async acceptAsPersonalKey(fingerprint) {
+ this.updateAcceptance(fingerprint, null, "personal");
+ },
+
+ async deletePersonalKeyAcceptance(fingerprint) {
+ this.deleteAcceptance(fingerprint);
+ },
+
+ async isAcceptedAsPersonalKey(fingerprint) {
+ let result = await this.getFingerprintAcceptance(null, fingerprint);
+ return result === "personal";
+ },
+};
+
+/**
+ * use a promise to open the Enigmail database.
+ *
+ * it's possible that there will be an NS_ERROR_STORAGE_BUSY
+ * so we're willing to retry for a little while.
+ *
+ * @param {Function} resolve: function to call when promise succeeds
+ * @param {Function} reject: - function to call when promise fails
+ * @param {number} waitms: Integer - number of milliseconds to wait before trying again in case of NS_ERROR_STORAGE_BUSY
+ * @param {number} maxtime: Integer - unix epoch (in milliseconds) of the point at which we should give up.
+ */
+function openDatabaseConn(filename, resolve, reject, waitms, maxtime) {
+ lazy.EnigmailLog.DEBUG("sqliteDb.jsm: openDatabaseConn()\n");
+ lazy.Sqlite.openConnection({
+ path: filename,
+ sharedMemoryCache: false,
+ })
+ .then(connection => {
+ resolve(connection);
+ })
+ .catch(error => {
+ let now = Date.now();
+ if (now > maxtime) {
+ reject(error);
+ return;
+ }
+ lazy.setTimeout(function () {
+ openDatabaseConn(filename, resolve, reject, waitms, maxtime);
+ }, waitms);
+ });
+}
+
+async function checkAcceptanceTable(connection) {
+ try {
+ let exists = await connection.tableExists("acceptance_email");
+ let exists2 = await connection.tableExists("acceptance_decision");
+ lazy.EnigmailLog.DEBUG("sqliteDB.jsm: checkAcceptanceTable - success\n");
+ if (!exists || !exists2) {
+ await createAcceptanceTable(connection);
+ }
+ } catch (error) {
+ lazy.EnigmailLog.DEBUG(
+ `sqliteDB.jsm: checkAcceptanceTable - error ${error}\n`
+ );
+ throw error;
+ }
+
+ return true;
+}
+
+async function createAcceptanceTable(connection) {
+ lazy.EnigmailLog.DEBUG("sqliteDB.jsm: createAcceptanceTable()\n");
+
+ await connection.execute(
+ "create table acceptance_email (" +
+ "fpr text not null, " +
+ "email text not null, " +
+ "unique(fpr, email));"
+ );
+
+ await connection.execute(
+ "create table acceptance_decision (" +
+ "fpr text not null, " +
+ "decision text not null, " +
+ "unique(fpr));"
+ );
+
+ lazy.EnigmailLog.DEBUG("sqliteDB.jsm: createAcceptanceTable - index1\n");
+ await connection.execute(
+ "create unique index acceptance_email_i1 on acceptance_email(fpr, email);"
+ );
+
+ lazy.EnigmailLog.DEBUG("sqliteDB.jsm: createAcceptanceTable - index2\n");
+ await connection.execute(
+ "create unique index acceptance__decision_i1 on acceptance_decision(fpr);"
+ );
+
+ return null;
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/streams.jsm b/comm/mail/extensions/openpgp/content/modules/streams.jsm
new file mode 100644
index 0000000000..e5c40224d7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/streams.jsm
@@ -0,0 +1,155 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailStreams"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+var EnigmailStreams = {
+ /**
+ * Create a new channel from a URL or URI.
+ *
+ * @param url: String, nsIURI or nsIFile - URL specification
+ *
+ * @return: channel
+ */
+ createChannel(url) {
+ let c = lazy.NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ });
+
+ return c;
+ },
+
+ /**
+ * create an nsIStreamListener object to read String data from an nsIInputStream
+ *
+ * @onStopCallback: Function - function(data) that is called when the stream has stopped
+ * string data is passed as |data|
+ *
+ * @return: the nsIStreamListener to pass to the stream
+ */
+ newStringStreamListener(onStopCallback) {
+ let listener = {
+ data: "",
+ inStream: Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ ),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(channel) {},
+
+ onStopRequest(channel, status) {
+ this.inStream = null;
+ onStopCallback(this.data);
+ },
+ };
+
+ listener.onDataAvailable = function (req, stream, offset, count) {
+ this.inStream.setInputStream(stream);
+ this.data += this.inStream.readBytes(count);
+ };
+
+ return listener;
+ },
+
+ /**
+ * create a nsIInputStream object that is fed with string data
+ *
+ * @uri: nsIURI - object representing the URI that will deliver the data
+ * @contentType: String - the content type as specified in nsIChannel
+ * @contentCharset: String - the character set; automatically determined if null
+ * @data: String - the data to feed to the stream
+ * @loadInfo nsILoadInfo - loadInfo (optional)
+ *
+ * @returns nsIChannel object
+ */
+ newStringChannel(uri, contentType, contentCharset, data, loadInfo) {
+ if (!loadInfo) {
+ loadInfo = createLoadInfo();
+ }
+
+ let inputStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ inputStream.setData(data, -1);
+
+ if (!contentCharset || contentCharset.length === 0) {
+ let netUtil = Services.io.QueryInterface(Ci.nsINetUtil);
+ const newCharset = {};
+ const hadCharset = {};
+ netUtil.parseResponseContentType(contentType, newCharset, hadCharset);
+ contentCharset = newCharset.value;
+ }
+
+ let isc = Cc["@mozilla.org/network/input-stream-channel;1"].createInstance(
+ Ci.nsIInputStreamChannel
+ );
+ isc.QueryInterface(Ci.nsIChannel);
+ isc.setURI(uri);
+ isc.loadInfo = loadInfo;
+ isc.contentStream = inputStream;
+
+ if (contentType && contentType.length) {
+ isc.contentType = contentType;
+ }
+ if (contentCharset && contentCharset.length) {
+ isc.contentCharset = contentCharset;
+ }
+
+ return isc;
+ },
+
+ newFileChannel(uri, file, contentType, deleteOnClose) {
+ let inputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ let behaviorFlags = Ci.nsIFileInputStream.CLOSE_ON_EOF;
+ if (deleteOnClose) {
+ behaviorFlags |= Ci.nsIFileInputStream.DELETE_ON_CLOSE;
+ }
+ const ioFlags = 0x01; // readonly
+ const perm = 0;
+ inputStream.init(file, ioFlags, perm, behaviorFlags);
+
+ let isc = Cc["@mozilla.org/network/input-stream-channel;1"].createInstance(
+ Ci.nsIInputStreamChannel
+ );
+ isc.QueryInterface(Ci.nsIChannel);
+ isc.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
+ isc.loadInfo = createLoadInfo();
+ isc.setURI(uri);
+ isc.contentStream = inputStream;
+
+ if (contentType && contentType.length) {
+ isc.contentType = contentType;
+ }
+ return isc;
+ },
+};
+
+function createLoadInfo() {
+ let c = lazy.NetUtil.newChannel({
+ uri: "chrome://openpgp/content/",
+ loadUsingSystemPrincipal: true,
+ });
+
+ return c.loadInfo;
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/trust.jsm b/comm/mail/extensions/openpgp/content/modules/trust.jsm
new file mode 100644
index 0000000000..37e0014b59
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/trust.jsm
@@ -0,0 +1,94 @@
+/*
+ * 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 = ["EnigmailTrust"];
+
+var l10n;
+
+// trust flags according to GPG documentation:
+// - https://www.gnupg.org/documentation/manuals/gnupg.pdf
+// - sources: doc/DETAILS
+// In the order of trustworthy:
+// ---------------------------------------------------------
+// i = The key is invalid (e.g. due to a missing self-signature)
+// n = The key is not valid / Never trust this key
+// d/D = The key has been disabled
+// r = The key has been revoked
+// e = The key has expired
+// g = group (???)
+// ---------------------------------------------------------
+// ? = INTERNAL VALUE to separate invalid from unknown keys
+// ---------------------------------------------------------
+// o = Unknown (this key is new to the system)
+// - = Unknown validity (i.e. no value assigned)
+// q = Undefined validity (Not enough information for calculation)
+// '-' and 'q' may safely be treated as the same value for most purposes
+// ---------------------------------------------------------
+// m = Marginally trusted
+// ---------------------------------------------------------
+// f = Fully trusted / valid key
+// u = Ultimately trusted
+// ---------------------------------------------------------
+const TRUSTLEVELS_SORTED = "indDreg?o-qmfu";
+const TRUSTLEVELS_SORTED_IDX_UNKNOWN = 7; // index of '?'
+
+var EnigmailTrust = {
+ /**
+ * @returns - |string| containing the order of trust/validity values
+ */
+ trustLevelsSorted() {
+ return TRUSTLEVELS_SORTED;
+ },
+
+ /**
+ * @returns - |boolean| whether the flag is invalid (neither unknown nor valid)
+ */
+ isInvalid(flag) {
+ return TRUSTLEVELS_SORTED.indexOf(flag) < TRUSTLEVELS_SORTED_IDX_UNKNOWN;
+ },
+
+ getTrustCode(keyObj) {
+ return keyObj.keyTrust;
+ },
+
+ getTrustLabel(trustCode) {
+ if (!l10n) {
+ l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+ }
+ let keyTrust;
+ switch (trustCode) {
+ case "q":
+ return l10n.formatValueSync("key-valid-unknown");
+ case "i":
+ return l10n.formatValueSync("key-valid-invalid");
+ case "d":
+ case "D":
+ return l10n.formatValueSync("key-valid-disabled");
+ case "r":
+ return l10n.formatValueSync("key-valid-revoked");
+ case "e":
+ return l10n.formatValueSync("key-valid-expired");
+ case "n":
+ return l10n.formatValueSync("key-trust-untrusted");
+ case "m":
+ return l10n.formatValueSync("key-trust-marginal");
+ case "f":
+ return l10n.formatValueSync("key-trust-full");
+ case "u":
+ return l10n.formatValueSync("key-trust-ultimate");
+ case "g":
+ return l10n.formatValueSync("key-trust-group");
+ case "-":
+ keyTrust = "-";
+ break;
+ default:
+ keyTrust = "";
+ }
+ return keyTrust;
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/uris.jsm b/comm/mail/extensions/openpgp/content/modules/uris.jsm
new file mode 100644
index 0000000000..f579195d03
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/uris.jsm
@@ -0,0 +1,124 @@
+/*
+ * 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 = ["EnigmailURIs"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "EnigmailLog",
+ "chrome://openpgp/content/modules/log.jsm"
+);
+
+const encryptedUris = [];
+
+var EnigmailURIs = {
+ /*
+ * remember the fact a URI is encrypted
+ *
+ * @param String msgUri
+ *
+ * @return null
+ */
+ rememberEncryptedUri(uri) {
+ lazy.EnigmailLog.DEBUG("uris.jsm: rememberEncryptedUri: uri=" + uri + "\n");
+ if (!encryptedUris.includes(uri)) {
+ encryptedUris.push(uri);
+ }
+ },
+
+ /*
+ * unremember the fact a URI is encrypted
+ *
+ * @param String msgUri
+ *
+ * @return null
+ */
+ forgetEncryptedUri(uri) {
+ lazy.EnigmailLog.DEBUG("uris.jsm: forgetEncryptedUri: uri=" + uri + "\n");
+ const pos = encryptedUris.indexOf(uri);
+ if (pos >= 0) {
+ encryptedUris.splice(pos, 1);
+ }
+ },
+
+ /*
+ * determine if a URI was remembered as encrypted
+ *
+ * @param String msgUri
+ *
+ * @return: Boolean true if yes, false otherwise
+ */
+ isEncryptedUri(uri) {
+ lazy.EnigmailLog.DEBUG("uris.jsm: isEncryptedUri: uri=" + uri + "\n");
+ return encryptedUris.includes(uri);
+ },
+
+ /**
+ * Determine message number and folder from mailnews URI
+ *
+ * @param url - nsIURI object
+ *
+ * @returns Object:
+ * - msgNum: String - the message number, or "" if no URI Scheme fits
+ * - folder: String - the folder (or newsgroup) name
+ */
+ msgIdentificationFromUrl(url) {
+ // sample URLs in Thunderbird
+ // Local folder: mailbox:///some/path/to/folder?number=359360
+ // IMAP: imap://user@host:port/fetch>some>path>111
+ // NNTP: news://some.host/some.service.com?group=some.group.name&key=3510
+ // also seen: e.g. mailbox:///some/path/to/folder?number=4455522&part=1.1.2&filename=test.eml
+ // mailbox:///...?number=4455522&part=1.1.2&filename=test.eml&type=application/x-message-display&filename=test.eml
+ // imap://user@host:port>UID>some>path>10?header=filter&emitter=js&examineEncryptedParts=true
+
+ if (!url) {
+ return null;
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "uris.jsm: msgIdentificationFromUrl: url.pathQueryRef=" +
+ ("path" in url ? url.path : url.pathQueryRef) +
+ "\n"
+ );
+
+ let msgNum = "";
+ let msgFolder = "";
+
+ let pathQueryRef = "path" in url ? url.path : url.pathQueryRef;
+
+ if (url.schemeIs("mailbox")) {
+ msgNum = pathQueryRef.replace(/(.*[?&]number=)([0-9]+)([^0-9].*)?/, "$2");
+ msgFolder = pathQueryRef.replace(/\?.*/, "");
+ } else if (url.schemeIs("file")) {
+ msgNum = "0";
+ msgFolder = pathQueryRef.replace(/\?.*/, "");
+ } else if (url.schemeIs("imap")) {
+ let p = unescape(pathQueryRef);
+ msgNum = p.replace(/(.*>)([0-9]+)([^0-9].*)?/, "$2");
+ msgFolder = p.replace(/\?.*$/, "").replace(/>[^>]+$/, "");
+ } else if (url.schemeIs("news")) {
+ msgNum = pathQueryRef.replace(/(.*[?&]key=)([0-9]+)([^0-9].*)?/, "$2");
+ msgFolder = pathQueryRef.replace(/(.*[?&]group=)([^&]+)(&.*)?/, "$2");
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "uris.jsm: msgIdentificationFromUrl: msgNum=" +
+ msgNum +
+ " / folder=" +
+ msgFolder +
+ "\n"
+ );
+
+ return {
+ msgNum,
+ folder: msgFolder.toLowerCase(),
+ };
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/webKey.jsm b/comm/mail/extensions/openpgp/content/modules/webKey.jsm
new file mode 100644
index 0000000000..76bd316e63
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/webKey.jsm
@@ -0,0 +1,293 @@
+/*
+ * 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/.
+ */
+
+/**
+ * This module serves to integrate WKS (Webkey service) into Enigmail
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["EnigmailWks"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+var EnigmailWks = {
+ wksClientPath: null,
+
+ /**
+ * Get WKS Client path (gpg-wks-client)
+ *
+ * @param window : Object - parent window for dialog display
+ * @param cb : Function(retValue) - callback function.
+ * retValue: nsIFile Object to gpg-wks-client executable or NULL
+ * @returns : Object - NULL or a process handle
+ */
+ getWksClientPathAsync(window, cb) {
+ lazy.EnigmailLog.DEBUG("webKey.jsm: getWksClientPathAsync\n");
+ throw new Error("Not implemented");
+ },
+
+ /**
+ * Determine if WKS is supported by email provider
+ *
+ * @param email : String - user's email address
+ * @param window: Object - parent window of dialog display
+ * @param cb : Function(retValue) - callback function.
+ * retValue: Boolean: true if WKS is supported / false otherwise
+ * @returns : Object - process handle
+ */
+ isWksSupportedAsync(email, window, cb) {
+ lazy.EnigmailLog.DEBUG(
+ "webKey.jsm: isWksSupportedAsync: email = " + email + "\n"
+ );
+ throw new Error("Not implemented");
+ },
+
+ /**
+ * Submit a set of keys to the Web Key Server (WKD)
+ *
+ * @param keys: Array of KeyObj
+ * @param win: parent Window for displaying dialogs
+ * @param observer: Object (KeySrvListener API)
+ * Object implementing:
+ * - onProgress: function(percentComplete) [only implemented for download()]
+ * - onCancel: function() - the body will be set by the callee
+ *
+ * @returns Promise<...>
+ */
+ wksUpload(keys, win, observer = null) {
+ lazy.EnigmailLog.DEBUG(`webKey.jsm: wksUpload(): keys = ${keys.length}\n`);
+ let ids = getWkdIdentities(keys);
+
+ if (observer === null) {
+ observer = {
+ onProgress() {},
+ };
+ }
+
+ observer.isCanceled = false;
+ observer.onCancel = function () {
+ this.isCanceled = true;
+ };
+
+ if (!ids) {
+ throw new Error("error");
+ }
+
+ if (ids.senderIdentities.length === 0) {
+ return new Promise(resolve => {
+ resolve([]);
+ });
+ }
+
+ return performWkdUpload(ids.senderIdentities, win, observer);
+ },
+
+ /**
+ * Submit a key to the email provider (= send publication request)
+ *
+ * @param ident : nsIMsgIdentity - user's ID
+ * @param key : Enigmail KeyObject of user's key
+ * @param window: Object - parent window of dialog display
+ * @param cb : Function(retValue) - callback function.
+ * retValue: Boolean: true if submit was successful / false otherwise
+ * @returns : Object - process handle
+ */
+
+ submitKey(ident, key, window, cb) {
+ lazy.EnigmailLog.DEBUG(
+ "webKey.jsm: submitKey(): email = " + ident.email + "\n"
+ );
+ throw new Error("Not implemented");
+ },
+
+ /**
+ * Submit a key to the email provider (= send publication request)
+ *
+ * @param ident : nsIMsgIdentity - user's ID
+ * @param body : String - complete message source of the confirmation-request email obtained
+ * from the email provider
+ * @param window: Object - parent window of dialog display
+ * @param cb : Function(retValue) - callback function.
+ * retValue: Boolean: true if submit was successful / false otherwise
+ * @returns : Object - process handle
+ */
+
+ confirmKey(ident, body, window, cb) {
+ lazy.EnigmailLog.DEBUG(
+ "webKey.jsm: confirmKey: ident=" + ident.email + "\n"
+ );
+ throw new Error("Not implemented");
+ },
+};
+
+/**
+ * Check if a file exists and is executable
+ *
+ * @param path: String - directory name
+ * @param execFileName: String - executable name
+ *
+ * @returns Object - nsIFile if file exists; NULL otherwise
+ */
+
+function getWkdIdentities(keys) {
+ lazy.EnigmailLog.DEBUG(
+ `webKey.jsm: getWkdIdentities(): keys = ${keys.length}\n`
+ );
+ let senderIdentities = [],
+ notFound = [];
+
+ for (let key of keys) {
+ try {
+ let found = false;
+ for (let uid of key.userIds) {
+ let email = lazy.EnigmailFuncs.stripEmail(uid.userId).toLowerCase();
+ let identity = MailServices.accounts.allIdentities.find(
+ id => id.email?.toLowerCase() == email
+ );
+
+ if (identity) {
+ senderIdentities.push({
+ identity,
+ fpr: key.fpr,
+ });
+ }
+ }
+ if (!found) {
+ notFound.push(key);
+ }
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(ex + "\n");
+ return null;
+ }
+ }
+
+ return {
+ senderIdentities,
+ notFound,
+ };
+}
+
+/**
+ * Do the WKD upload and interact with a progress receiver
+ *
+ * @param keyList: Object:
+ * - fprList (String - fingerprint)
+ * - senderIdentities (nsIMsgIdentity)
+ * @param win: nsIWindow - parent window
+ * @param observer: Object:
+ * - onProgress: function(percentComplete [0 .. 100])
+ * called after processing of every key (independent of status)
+ * - onUpload: function(fpr)
+ * called after successful uploading of a key
+ * - onFinished: function(completionStatus, errorMessage, displayError)
+ * - isCanceled: Boolean - used to determine if process is canceled
+ */
+function performWkdUpload(keyList, win, observer) {
+ lazy.EnigmailLog.DEBUG(
+ `webKey.jsm: performWkdUpload: keyList.length=${keyList.length}\n`
+ );
+
+ let uploads = [];
+
+ let numKeys = keyList.length;
+
+ // For each key fpr/sender identity pair, check whenever WKS is supported
+ // Result is an array of booleans
+ for (let i = 0; i < numKeys; i++) {
+ let keyFpr = keyList[i].fpr;
+ let senderIdent = keyList[i].identity;
+
+ let was_uploaded = new Promise(function (resolve, reject) {
+ lazy.EnigmailLog.DEBUG(
+ "webKey.jsm: performWkdUpload: _isSupported(): ident=" +
+ senderIdent.email +
+ ", key=" +
+ keyFpr +
+ "\n"
+ );
+ EnigmailWks.isWksSupportedAsync(
+ senderIdent.email,
+ win,
+ function (is_supported) {
+ if (observer.isCanceled) {
+ lazy.EnigmailLog.DEBUG(
+ "webKey.jsm: performWkdUpload: canceled by user\n"
+ );
+ reject("canceled");
+ }
+
+ lazy.EnigmailLog.DEBUG(
+ "webKey.jsm: performWkdUpload: ident=" +
+ senderIdent.email +
+ ", supported=" +
+ is_supported +
+ "\n"
+ );
+ resolve(is_supported);
+ }
+ );
+ }).then(function (is_supported) {
+ lazy.EnigmailLog.DEBUG(
+ `webKey.jsm: performWkdUpload: _submitKey ${is_supported}\n`
+ );
+ if (is_supported) {
+ return new Promise(function (resolve, reject) {
+ EnigmailWks.submitKey(
+ senderIdent,
+ {
+ fpr: keyFpr,
+ },
+ win,
+ function (success) {
+ observer.onProgress(((i + 1) / numKeys) * 100);
+ if (success) {
+ resolve(senderIdent);
+ } else {
+ reject("submitFailed");
+ }
+ }
+ );
+ });
+ }
+
+ observer.onProgress(((i + 1) / numKeys) * 100);
+ return Promise.resolve(null);
+ });
+
+ uploads.push(was_uploaded);
+ }
+
+ return Promise.all(uploads)
+ .catch(function (reason) {
+ //let errorMsg = "Could not upload your key to the Web Key Service";
+ return [];
+ })
+ .then(function (senders) {
+ let uploaded_uids = [];
+ if (senders) {
+ senders.forEach(function (val) {
+ if (val !== null) {
+ uploaded_uids.push(val.email);
+ }
+ });
+ }
+ observer.onProgress(100);
+
+ return uploaded_uids;
+ });
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/windows.jsm b/comm/mail/extensions/openpgp/content/modules/windows.jsm
new file mode 100644
index 0000000000..baf2e1e5f0
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/windows.jsm
@@ -0,0 +1,518 @@
+/*
+ * 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailWindows"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var EnigmailWindows = {
+ /**
+ * Open a window, or focus it if it is already open
+ *
+ * @winName : String - name of the window; used to identify if it is already open
+ * @spec : String - window URL (e.g. chrome://openpgp/content/ui/test.xhtml)
+ * @winOptions: String - window options as defined in nsIWindow.open
+ * @optObj : any - an Object, Array, String, etc. that is passed as parameter
+ * to the window
+ */
+ openWin(winName, spec, winOptions, optObj) {
+ var windowManager = Services.wm;
+
+ var recentWin = null;
+ for (let win of windowManager.getEnumerator(null)) {
+ if (win.location.href == spec) {
+ recentWin = win;
+ break;
+ }
+ if (winName && win.name && win.name == winName) {
+ win.focus();
+ break;
+ }
+ }
+
+ if (recentWin) {
+ recentWin.focus();
+ } else {
+ var appShellSvc = Services.appShell;
+ var domWin = appShellSvc.hiddenDOMWindow;
+ try {
+ domWin.open(spec, winName, "chrome," + winOptions, optObj);
+ } catch (ex) {
+ domWin = windowManager.getMostRecentWindow(null);
+ domWin.open(spec, winName, "chrome," + winOptions, optObj);
+ }
+ }
+ },
+
+ /**
+ * Determine the best possible window to serve as parent window for dialogs.
+ *
+ * @return: nsIWindow object
+ */
+ getBestParentWin() {
+ var windowManager = Services.wm;
+
+ var bestFit = null;
+
+ for (let win of windowManager.getEnumerator(null)) {
+ if (win.location.href.search(/\/messenger.xhtml$/) > 0) {
+ bestFit = win;
+ }
+ if (
+ !bestFit &&
+ win.location.href.search(/\/messengercompose.xhtml$/) > 0
+ ) {
+ bestFit = win;
+ }
+ }
+
+ if (!bestFit) {
+ var winEnum = windowManager.getEnumerator(null);
+ bestFit = winEnum.getNext();
+ }
+
+ return bestFit;
+ },
+
+ /**
+ * Iterate through the frames of a window and return the first frame with a
+ * matching name.
+ *
+ * @win: nsIWindow - XUL window to search
+ * @frameName: String - name of the frame to search
+ *
+ * @return: the frame object or null if not found
+ */
+ getFrame(win, frameName) {
+ lazy.EnigmailLog.DEBUG("windows.jsm: getFrame: name=" + frameName + "\n");
+ for (var j = 0; j < win.frames.length; j++) {
+ if (win.frames[j].name == frameName) {
+ return win.frames[j];
+ }
+ }
+ return null;
+ },
+
+ getMostRecentWindow() {
+ var windowManager = Services.wm;
+ return windowManager.getMostRecentWindow(null);
+ },
+
+ /**
+ * Display the key help window
+ *
+ * @source - |string| containing the name of the file to display
+ *
+ * no return value
+ */
+
+ openHelpWindow(source) {
+ EnigmailWindows.openWin(
+ "enigmail:help",
+ "chrome://openpgp/content/ui/enigmailHelp.xhtml?src=" + source,
+ "centerscreen,resizable"
+ );
+ },
+
+ /**
+ * Open the Enigmail Documentation page in a new window
+ *
+ * no return value
+ */
+ openEnigmailDocu(parent) {
+ if (!parent) {
+ parent = this.getMostRecentWindow();
+ }
+
+ parent.open(
+ "https://doesnotexist-openpgp-integration.thunderbird/faq/docu.php",
+ "",
+ "chrome,width=600,height=500,resizable"
+ );
+ },
+
+ /**
+ * Display the OpenPGP key manager window
+ *
+ * no return value
+ */
+ openKeyManager(win) {
+ lazy.EnigmailCore.getService(win);
+
+ EnigmailWindows.openWin(
+ "enigmail:KeyManager",
+ "chrome://openpgp/content/ui/enigmailKeyManager.xhtml",
+ "resizable"
+ );
+ },
+
+ /**
+ * Display the OpenPGP key manager window
+ *
+ * no return value
+ */
+ openImportSettings(win) {
+ lazy.EnigmailCore.getService(win);
+
+ EnigmailWindows.openWin(
+ "",
+ "chrome://openpgp/content/ui/importSettings.xhtml",
+ "chrome,dialog,centerscreen,resizable,modal"
+ );
+ },
+
+ /**
+ * If the Key Manager is open, dispatch an event to tell the key
+ * manager to refresh the displayed keys
+ */
+ keyManReloadKeys() {
+ for (let thisWin of Services.wm.getEnumerator(null)) {
+ if (thisWin.name && thisWin.name == "enigmail:KeyManager") {
+ let evt = new thisWin.Event("reload-keycache", {
+ bubbles: true,
+ cancelable: false,
+ });
+ thisWin.dispatchEvent(evt);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Display the card details window
+ *
+ * no return value
+ */
+ openCardDetails() {
+ EnigmailWindows.openWin(
+ "enigmail:cardDetails",
+ "chrome://openpgp/content/ui/enigmailCardDetails.xhtml",
+ "centerscreen"
+ );
+ },
+
+ /**
+ * Display the console log window
+ *
+ * @win - |object| holding the parent window for the dialog
+ *
+ * no return value
+ */
+ openConsoleWindow() {
+ EnigmailWindows.openWin(
+ "enigmail:console",
+ "chrome://openpgp/content/ui/enigmailConsole.xhtml",
+ "resizable,centerscreen"
+ );
+ },
+
+ /**
+ * Display the window for the debug log file
+ *
+ * @win - |object| holding the parent window for the dialog
+ *
+ * no return value
+ */
+ openDebugLog(win) {
+ EnigmailWindows.openWin(
+ "enigmail:logFile",
+ "chrome://openpgp/content/ui/enigmailViewFile.xhtml?viewLog=1&title=" +
+ escape(lazy.l10n.formatValueSync("debug-log-title")),
+ "centerscreen"
+ );
+ },
+
+ /**
+ * Display the dialog for changing the expiry date of one or several keys
+ *
+ * @win - |object| holding the parent window for the dialog
+ * @userIdArr - |array| of |strings| containing the User IDs
+ * @keyIdArr - |array| of |strings| containing the key IDs (eg. "0x12345678") to change
+ *
+ * @returns Boolean - true if expiry date was changed; false otherwise
+ */
+ editKeyExpiry(win, userIdArr, keyIdArr) {
+ const inputObj = {
+ keyId: keyIdArr,
+ userId: userIdArr,
+ };
+ const resultObj = {
+ refresh: false,
+ };
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailEditKeyExpiryDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable",
+ inputObj,
+ resultObj
+ );
+ return resultObj.refresh;
+ },
+
+ /**
+ * Display the dialog for changing key trust of one or several keys
+ *
+ * @win - |object| holding the parent window for the dialog
+ * @userIdArr - |array| of |strings| containing the User IDs
+ * @keyIdArr - |array| of |strings| containing the key IDs (eg. "0x12345678") to change
+ *
+ * @returns Boolean - true if key trust was changed; false otherwise
+ */
+ editKeyTrust(win, userIdArr, keyIdArr) {
+ const inputObj = {
+ keyId: keyIdArr,
+ userId: userIdArr,
+ };
+ const resultObj = {
+ refresh: false,
+ };
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailEditKeyTrustDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable",
+ inputObj,
+ resultObj
+ );
+ return resultObj.refresh;
+ },
+
+ /**
+ * Display the dialog for signing a key
+ *
+ * @win - |object| holding the parent window for the dialog
+ * @userId - |string| containing the User ID (for displaing in the dialog only)
+ * @keyId - |string| containing the key ID (eg. "0x12345678")
+ *
+ * @returns Boolean - true if key was signed; false otherwise
+ */
+ signKey(win, userId, keyId) {
+ const inputObj = {
+ keyId,
+ userId,
+ };
+ const resultObj = {
+ refresh: false,
+ };
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailSignKeyDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable",
+ inputObj,
+ resultObj
+ );
+ return resultObj.refresh;
+ },
+
+ /**
+ * Display the OpenPGP Key Details window
+ *
+ * @win - |object| holding the parent window for the dialog
+ * @keyId - |string| containing the key ID (eg. "0x12345678")
+ * @refresh - |boolean| if true, cache is cleared and the key data is loaded from GnuPG
+ *
+ * @returns Boolean - true: keylist needs to be refreshed
+ * - false: no need to refresh keylist
+ */
+ async openKeyDetails(win, keyId, refresh) {
+ if (!win) {
+ win = this.getBestParentWin();
+ }
+
+ keyId = keyId.replace(/^0x/, "");
+
+ if (refresh) {
+ lazy.EnigmailKeyRing.clearCache();
+ }
+
+ const resultObj = {
+ refresh: false,
+ };
+ win.openDialog(
+ "chrome://openpgp/content/ui/keyDetailsDlg.xhtml",
+ "KeyDetailsDialog",
+ "dialog,modal,centerscreen,resizable",
+ { keyId, modified: lazy.EnigmailKeyRing.clearCache },
+ resultObj
+ );
+
+ return resultObj.refresh;
+ },
+
+ /**
+ * Display the dialog to search and/or download key(s) from a keyserver
+ *
+ * @win - |object| holding the parent window for the dialog
+ * @inputObj - |object| with member searchList (|string| containing the keys to search)
+ * @resultObj - |object| with member importedKeys (|number| containing the number of imporeted keys)
+ *
+ * no return value
+ */
+ downloadKeys(win, inputObj, resultObj) {
+ lazy.EnigmailLog.DEBUG(
+ "windows.jsm: downloadKeys: searchList=" + inputObj.searchList + "\n"
+ );
+
+ resultObj.importedKeys = 0;
+
+ const ioService = Services.io;
+ if (ioService && ioService.offline) {
+ lazy.l10n.formatValue("need-online").then(value => {
+ lazy.EnigmailDialog.alert(win, value);
+ });
+ return;
+ }
+
+ let valueObj = {};
+ if (inputObj.searchList) {
+ valueObj = {
+ keyId: "<" + inputObj.searchList.join("> <") + ">",
+ };
+ }
+
+ const keysrvObj = {};
+
+ if (inputObj.searchList && inputObj.autoKeyServer) {
+ keysrvObj.value = inputObj.autoKeyServer;
+ } else {
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailKeyserverDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen",
+ valueObj,
+ keysrvObj
+ );
+ }
+
+ if (!keysrvObj.value) {
+ return;
+ }
+
+ inputObj.keyserver = keysrvObj.value;
+
+ if (!inputObj.searchList) {
+ const searchval = keysrvObj.email
+ .replace(/^(\s*)(.*)/, "$2")
+ .replace(/\s+$/, ""); // trim spaces
+ // special handling to convert fingerprints with spaces into fingerprint without spaces
+ if (
+ searchval.length == 49 &&
+ searchval.match(/^[0-9a-fA-F ]*$/) &&
+ searchval[4] == " " &&
+ searchval[9] == " " &&
+ searchval[14] == " " &&
+ searchval[19] == " " &&
+ searchval[24] == " " &&
+ searchval[29] == " " &&
+ searchval[34] == " " &&
+ searchval[39] == " " &&
+ searchval[44] == " "
+ ) {
+ inputObj.searchList = ["0x" + searchval.replace(/ /g, "")];
+ } else if (searchval.length == 40 && searchval.match(/^[0-9a-fA-F ]*$/)) {
+ inputObj.searchList = ["0x" + searchval];
+ } else if (searchval.length == 8 && searchval.match(/^[0-9a-fA-F]*$/)) {
+ // special handling to add the required leading 0x when searching for keys
+ inputObj.searchList = ["0x" + searchval];
+ } else if (searchval.length == 16 && searchval.match(/^[0-9a-fA-F]*$/)) {
+ inputObj.searchList = ["0x" + searchval];
+ } else {
+ inputObj.searchList = searchval.split(/[,; ]+/);
+ }
+ }
+
+ win.openDialog(
+ "chrome://openpgp/content/ui/enigmailSearchKey.xhtml",
+ "",
+ "dialog,modal,centerscreen",
+ inputObj,
+ resultObj
+ );
+ },
+
+ /**
+ * Display Autocrypt Setup Passwd dialog.
+ *
+ * @param dlgMode: String - dialog mode: "input" / "display"
+ * @param passwdType: String - type of password ("numeric9x4" / "generic")
+ * @param password: String - password or initial two digits of password
+ *
+ * @returns String entered password (in input mode) or NULL
+ */
+ autocryptSetupPasswd(window, dlgMode, passwdType = "numeric9x4", password) {
+ if (!window) {
+ window = this.getBestParentWin();
+ }
+
+ let inputObj = {
+ password: null,
+ passwdType,
+ dlgMode,
+ };
+
+ if (password) {
+ inputObj.initialPasswd = password;
+ }
+
+ window.openDialog(
+ "chrome://openpgp/content/ui/autocryptSetupPasswd.xhtml",
+ "",
+ "dialog,modal,centerscreen",
+ inputObj
+ );
+
+ return inputObj.password;
+ },
+
+ /**
+ * Display dialog to initiate the Autocrypt Setup Message.
+ *
+ */
+ inititateAcSetupMessage(window) {
+ if (!window) {
+ window = this.getBestParentWin();
+ }
+
+ window.openDialog(
+ "chrome://openpgp/content/ui/autocryptInitiateBackup.xhtml",
+ "",
+ "dialog,centerscreen"
+ );
+ },
+
+ shutdown(reason) {
+ lazy.EnigmailLog.DEBUG("windows.jsm: shutdown()\n");
+
+ let tabs = Services.wm
+ .getMostRecentWindow("mail:3pane")
+ .document.getElementById("tabmail");
+
+ for (let i = tabs.tabInfo.length - 1; i >= 0; i--) {
+ if (
+ "openedUrl" in tabs.tabInfo[i] &&
+ tabs.tabInfo[i].openedUrl.startsWith("chrome://openpgp/")
+ ) {
+ tabs.closeTab(tabs.tabInfo[i]);
+ }
+ }
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/modules/wkdLookup.jsm b/comm/mail/extensions/openpgp/content/modules/wkdLookup.jsm
new file mode 100644
index 0000000000..bf5fd25845
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/wkdLookup.jsm
@@ -0,0 +1,363 @@
+/* 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";
+
+/**
+ * Lookup keys by email addresses using WKD. A an email address is lookep up at most
+ * once a day. (see https://tools.ietf.org/html/draft-koch-openpgp-webkey-service)
+ */
+
+var EXPORTED_SYMBOLS = ["EnigmailWkdLookup"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ DNS: "resource:///modules/DNS.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailZBase32: "chrome://openpgp/content/modules/zbase32.jsm",
+});
+
+// Those domains are not expected to have WKD:
+var EXCLUDE_DOMAINS = [
+ /* Default domains included */
+ "aol.com",
+ "att.net",
+ "comcast.net",
+ "facebook.com",
+ "gmail.com",
+ "gmx.com",
+ "googlemail.com",
+ "google.com",
+ "hotmail.com",
+ "hotmail.co.uk",
+ "mac.com",
+ "me.com",
+ "mail.com",
+ "msn.com",
+ "live.com",
+ "sbcglobal.net",
+ "verizon.net",
+ "yahoo.com",
+ "yahoo.co.uk",
+
+ /* Other global domains */
+ "email.com",
+ "games.com" /* AOL */,
+ "gmx.net",
+ "icloud.com",
+ "iname.com",
+ "inbox.com",
+ "lavabit.com",
+ "love.com" /* AOL */,
+ "outlook.com",
+ "pobox.com",
+ "tutanota.de",
+ "tutanota.com",
+ "tutamail.com",
+ "tuta.io",
+ "keemail.me",
+ "rocketmail.com" /* Yahoo */,
+ "safe-mail.net",
+ "wow.com" /* AOL */,
+ "ygm.com" /* AOL */,
+ "ymail.com" /* Yahoo */,
+ "zoho.com",
+ "yandex.com",
+
+ /* United States ISP domains */
+ "bellsouth.net",
+ "charter.net",
+ "cox.net",
+ "earthlink.net",
+ "juno.com",
+
+ /* British ISP domains */
+ "btinternet.com",
+ "virginmedia.com",
+ "blueyonder.co.uk",
+ "freeserve.co.uk",
+ "live.co.uk",
+ "ntlworld.com",
+ "o2.co.uk",
+ "orange.net",
+ "sky.com",
+ "talktalk.co.uk",
+ "tiscali.co.uk",
+ "virgin.net",
+ "wanadoo.co.uk",
+ "bt.com",
+
+ /* Domains used in Asia */
+ "sina.com",
+ "sina.cn",
+ "qq.com",
+ "naver.com",
+ "hanmail.net",
+ "daum.net",
+ "nate.com",
+ "yahoo.co.jp",
+ "yahoo.co.kr",
+ "yahoo.co.id",
+ "yahoo.co.in",
+ "yahoo.com.sg",
+ "yahoo.com.ph",
+ "163.com",
+ "yeah.net",
+ "126.com",
+ "21cn.com",
+ "aliyun.com",
+ "foxmail.com",
+
+ /* French ISP domains */
+ "hotmail.fr",
+ "live.fr",
+ "laposte.net",
+ "yahoo.fr",
+ "wanadoo.fr",
+ "orange.fr",
+ "gmx.fr",
+ "sfr.fr",
+ "neuf.fr",
+ "free.fr",
+
+ /* German ISP domains */
+ "gmx.de",
+ "hotmail.de",
+ "live.de",
+ "online.de",
+ "t-online.de" /* T-Mobile */,
+ "web.de",
+ "yahoo.de",
+
+ /* Italian ISP domains */
+ "libero.it",
+ "virgilio.it",
+ "hotmail.it",
+ "aol.it",
+ "tiscali.it",
+ "alice.it",
+ "live.it",
+ "yahoo.it",
+ "email.it",
+ "tin.it",
+ "poste.it",
+ "teletu.it",
+
+ /* Russian ISP domains */
+ "mail.ru",
+ "rambler.ru",
+ "yandex.ru",
+ "ya.ru",
+ "list.ru",
+
+ /* Belgian ISP domains */
+ "hotmail.be",
+ "live.be",
+ "skynet.be",
+ "voo.be",
+ "tvcablenet.be",
+ "telenet.be",
+
+ /* Argentinian ISP domains */
+ "hotmail.com.ar",
+ "live.com.ar",
+ "yahoo.com.ar",
+ "fibertel.com.ar",
+ "speedy.com.ar",
+ "arnet.com.ar",
+
+ /* Domains used in Mexico */
+ "yahoo.com.mx",
+ "live.com.mx",
+ "hotmail.es",
+ "hotmail.com.mx",
+ "prodigy.net.mx",
+
+ /* Domains used in Canada */
+ "yahoo.ca",
+ "hotmail.ca",
+ "bell.net",
+ "shaw.ca",
+ "sympatico.ca",
+ "rogers.com",
+
+ /* Domains used in Brazil */
+ "yahoo.com.br",
+ "hotmail.com.br",
+ "outlook.com.br",
+ "uol.com.br",
+ "bol.com.br",
+ "terra.com.br",
+ "ig.com.br",
+ "itelefonica.com.br",
+ "r7.com",
+ "zipmail.com.br",
+ "globo.com",
+ "globomail.com",
+ "oi.com.br",
+];
+
+var EnigmailWkdLookup = {
+ /**
+ * get the download URL for an email address for WKD or domain-specific locations
+ *
+ * @param {string} email: email address
+ *
+ * @returns {Promise<string>}: URL (or null if not possible)
+ */
+ async getDownloadUrlFromEmail(email, advancedMethod) {
+ email = email.toLowerCase().trim();
+
+ let url = await getSiteSpecificUrl(email);
+ if (url) {
+ return url;
+ }
+
+ let at = email.indexOf("@");
+
+ let domain = email.substr(at + 1);
+ let user = email.substr(0, at);
+
+ let data = [...new TextEncoder().encode(user)];
+ let ch = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ ch.init(ch.SHA1);
+ ch.update(data, data.length);
+ let gotHash = ch.finish(false);
+ let encodedHash = lazy.EnigmailZBase32.encode(gotHash);
+
+ if (advancedMethod) {
+ url =
+ "https://openpgpkey." +
+ domain +
+ "/.well-known/openpgpkey/" +
+ domain +
+ "/hu/" +
+ encodedHash +
+ "?l=" +
+ escape(user);
+ } else {
+ url =
+ "https://" +
+ domain +
+ "/.well-known/openpgpkey/hu/" +
+ encodedHash +
+ "?l=" +
+ escape(user);
+ }
+
+ return url;
+ },
+
+ /**
+ * Download a key for an email address
+ *
+ * @param {string} email: email address
+ * @param {string} url: url from getDownloadUrlFromEmail()
+ *
+ * @returns {Promise<string>}: Key data (or null if not possible)
+ */
+ async downloadKey(url) {
+ let padLen = (url.length % 512) + 1;
+ let hdrs = new Headers({
+ Authorization: "Basic " + btoa("no-user:"),
+ });
+ hdrs.append("Content-Type", "application/octet-stream");
+ hdrs.append("X-Enigmail-Padding", "x".padEnd(padLen, "x"));
+
+ let myRequest = new Request(url, {
+ method: "GET",
+ headers: hdrs,
+ mode: "cors",
+ //redirect: 'error',
+ redirect: "follow",
+ cache: "default",
+ });
+
+ let response;
+ try {
+ lazy.EnigmailLog.DEBUG(
+ "wkdLookup.jsm: downloadKey: requesting " + url + "\n"
+ );
+ response = await fetch(myRequest);
+ if (!response.ok) {
+ return null;
+ }
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(
+ "wkdLookup.jsm: downloadKey: error " + ex.toString() + "\n"
+ );
+ return null;
+ }
+
+ try {
+ if (
+ response.headers.has("content-type") &&
+ response.headers.get("content-type").search(/^text\/html/i) === 0
+ ) {
+ // if we get HTML output, we return nothing (for example redirects to error catching pages)
+ return null;
+ }
+ return lazy.EnigmailData.arrayBufferToString(
+ Cu.cloneInto(await response.arrayBuffer(), {})
+ );
+ } catch (ex) {
+ lazy.EnigmailLog.DEBUG(
+ "wkdLookup.jsm: downloadKey: error " + ex.toString() + "\n"
+ );
+ return null;
+ }
+ },
+
+ isWkdAvailable(email) {
+ let domain = email.toLowerCase().replace(/^.*@/, "");
+
+ return !EXCLUDE_DOMAINS.includes(domain);
+ },
+};
+
+/**
+ * Get special URLs for specific sites that don't use WKD, but still provide
+ * public keys of their users in
+ *
+ * @param {string}: emailAddr: email address in lowercase
+ *
+ * @returns {Promise<string>}: URL or null of no URL relevant
+ */
+async function getSiteSpecificUrl(emailAddr) {
+ let domain = emailAddr.replace(/^.+@/, "");
+ let url = null;
+
+ switch (domain) {
+ case "protonmail.ch":
+ case "protonmail.com":
+ case "pm.me":
+ url =
+ "https://api.protonmail.ch/pks/lookup?op=get&options=mr&search=" +
+ escape(emailAddr);
+ break;
+ }
+ if (!url) {
+ let records = await lazy.DNS.mx(domain);
+ const mxHosts = records.filter(record => record.host);
+
+ if (
+ mxHosts &&
+ (mxHosts.includes("mail.protonmail.ch") ||
+ mxHosts.includes("mailsec.protonmail.ch"))
+ ) {
+ url =
+ "https://api.protonmail.ch/pks/lookup?op=get&options=mr&search=" +
+ escape(emailAddr);
+ }
+ }
+ return url;
+}
diff --git a/comm/mail/extensions/openpgp/content/modules/wksMimeHandler.jsm b/comm/mail/extensions/openpgp/content/modules/wksMimeHandler.jsm
new file mode 100644
index 0000000000..40a8d221f0
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/wksMimeHandler.jsm
@@ -0,0 +1,262 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["EnigmailWksMimeHandler"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+/**
+ * Module for handling response messages from OpenPGP Web Key Service
+ */
+
+var gDebugLog = false;
+
+var EnigmailWksMimeHandler = {
+ /***
+ * register a PGP/MIME verify object the same way PGP/MIME encrypted mail is handled
+ */
+ registerContentTypeHandler() {
+ lazy.EnigmailLog.DEBUG(
+ "wksMimeHandler.jsm: registerContentTypeHandler()\n"
+ );
+ let reg = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+ let pgpMimeClass = Cc["@mozilla.org/mimecth;1?type=multipart/encrypted"];
+
+ reg.registerFactory(
+ pgpMimeClass,
+ "Enigmail WKD Response Handler",
+ "@mozilla.org/mimecth;1?type=application/vnd.gnupg.wks",
+ null
+ );
+ },
+
+ newHandler() {
+ lazy.EnigmailLog.DEBUG("wksMimeHandler.jsm: newHandler()\n");
+
+ let v = new PgpWkdHandler();
+ return v;
+ },
+};
+
+// MimeVerify Constructor
+function PgpWkdHandler(protocol) {
+ this.inStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+}
+
+// PgpWkdHandler implementation
+PgpWkdHandler.prototype = {
+ data: "",
+ mimePartNumber: "",
+ uri: null,
+ backgroundJob: false,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ onStartRequest(request, ctxt) {
+ lazy.EnigmailLog.DEBUG("wksMimeHandler.jsm: onStartRequest\n"); // always log this one
+
+ this.mimeSvc = request.QueryInterface(Ci.nsIPgpMimeProxy);
+ if ("messageURI" in this.mimeSvc) {
+ this.uri = this.mimeSvc.messageURI;
+ } else {
+ this.uri = ctxt;
+ }
+
+ if ("mimePart" in this.mimeSvc) {
+ this.mimePartNumber = this.mimeSvc.mimePart;
+ } else {
+ this.mimePartNumber = "";
+ }
+ this.data = "";
+ this.msgWindow = lazy.EnigmailVerify.lastWindow;
+ this.backgroundJob = false;
+
+ if (this.uri) {
+ this.backgroundJob =
+ this.uri.spec.search(/[&?]header=(print|quotebody)/) >= 0;
+ }
+ },
+
+ onDataAvailable(req, dummy, stream, offset, count) {
+ if ("messageURI" in this.mimeSvc) {
+ // TB >= 67
+ stream = dummy;
+ count = offset;
+ }
+
+ LOCAL_DEBUG("wksMimeHandler.jsm: onDataAvailable: " + count + "\n");
+ if (count > 0) {
+ this.inStream.init(stream);
+ let data = this.inStream.read(count);
+ this.data += data;
+ }
+ },
+
+ onStopRequest() {
+ lazy.EnigmailLog.DEBUG("wksMimeHandler.jsm: onStopRequest\n");
+
+ if (this.data.search(/-----BEGIN PGP MESSAGE-----/i) >= 0) {
+ this.decryptChallengeData();
+ }
+
+ let jsonStr = this.requestToJsonString(this.data);
+
+ if (this.data.search(/^\s*type:\s+confirmation-request/im) >= 0) {
+ lazy.l10n.formatValue("wkd-message-body-req").then(value => {
+ this.returnData(value);
+ });
+ } else {
+ lazy.l10n.formatValue("wkd-message-body-process").then(value => {
+ this.returnData(value);
+ });
+ }
+
+ this.displayStatus(jsonStr);
+ },
+
+ decryptChallengeData() {
+ lazy.EnigmailLog.DEBUG("wksMimeHandler.jsm: decryptChallengeData()\n");
+ let windowManager = Services.wm;
+ let win = windowManager.getMostRecentWindow(null);
+ let statusFlagsObj = {};
+
+ let res = lazy.EnigmailDecryption.decryptMessage(
+ win,
+ 0,
+ this.data,
+ null, // date
+ {},
+ {},
+ statusFlagsObj,
+ {},
+ {},
+ {},
+ {},
+ {},
+ {}
+ );
+
+ if (statusFlagsObj.value & lazy.EnigmailConstants.DECRYPTION_OKAY) {
+ this.data = res;
+ }
+ lazy.EnigmailLog.DEBUG(
+ "wksMimeHandler.jsm: decryptChallengeData: decryption result: " +
+ res +
+ "\n"
+ );
+ },
+
+ // convert request data into JSON-string and parse it
+ requestToJsonString() {
+ // convert
+ let lines = this.data.split(/\r?\n/);
+ let s = "{";
+ for (let l of lines) {
+ let m = l.match(/^([^\s:]+)(:\s*)([^\s].+)$/);
+ if (m && m.length >= 4) {
+ s += '"' + m[1].trim().toLowerCase() + '": "' + m[3].trim() + '",';
+ }
+ }
+
+ s = s.substr(0, s.length - 1) + "}";
+
+ return s;
+ },
+
+ // return data to libMime
+ returnData(message) {
+ lazy.EnigmailLog.DEBUG("wksMimeHandler.jsm: returnData():\n");
+
+ let msg =
+ 'Content-Type: text/plain; charset="utf-8"\r\n' +
+ "Content-Transfer-Encoding: 8bit\r\n\r\n" +
+ message +
+ "\r\n";
+
+ if ("outputDecryptedData" in this.mimeSvc) {
+ this.mimeSvc.outputDecryptedData(msg, msg.length);
+ } else {
+ let gConv = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ gConv.setData(msg, msg.length);
+ try {
+ this.mimeSvc.onStartRequest(null);
+ this.mimeSvc.onDataAvailable(null, gConv, 0, msg.length);
+ this.mimeSvc.onStopRequest(null, 0);
+ } catch (ex) {
+ lazy.EnigmailLog.ERROR(
+ "wksMimeHandler.jsm: returnData(): mimeSvc.onDataAvailable failed:\n" +
+ ex.toString()
+ );
+ }
+ }
+ },
+
+ displayStatus(jsonStr) {
+ lazy.EnigmailLog.DEBUG("wksMimeHandler.jsm: displayStatus\n");
+ if (this.msgWindow === null || this.backgroundJob) {
+ return;
+ }
+
+ try {
+ LOCAL_DEBUG("wksMimeHandler.jsm: displayStatus displaying result\n");
+ let headerSink = lazy.EnigmailSingletons.messageReader;
+
+ if (headerSink) {
+ headerSink.processDecryptionResult(
+ this.uri,
+ "wksConfirmRequest",
+ jsonStr,
+ this.mimePartNumber
+ );
+ }
+ } catch (ex) {
+ lazy.EnigmailLog.writeException("wksMimeHandler.jsm", ex);
+ }
+ },
+};
+
+////////////////////////////////////////////////////////////////////
+// General-purpose functions, not exported
+
+function LOCAL_DEBUG(str) {
+ if (gDebugLog) {
+ lazy.EnigmailLog.DEBUG(str);
+ }
+}
+
+function initModule() {
+ let nspr_log_modules = Services.env.get("NSPR_LOG_MODULES");
+ let matches = nspr_log_modules.match(/wksMimeHandler:(\d+)/);
+
+ if (matches && matches.length > 1) {
+ if (matches[1] > 2) {
+ gDebugLog = true;
+ }
+ }
+}
+
+initModule();
diff --git a/comm/mail/extensions/openpgp/content/modules/zbase32.jsm b/comm/mail/extensions/openpgp/content/modules/zbase32.jsm
new file mode 100644
index 0000000000..c5587fde3f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/zbase32.jsm
@@ -0,0 +1,108 @@
+/* eslint no-invalid-this: 0 */
+/*
+ * 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 = ["EnigmailZBase32"];
+
+const ZBase32Alphabet = "ybndrfg8ejkmcpqxot1uwisza345h769";
+
+var EnigmailZBase32 = {
+ a: ZBase32Alphabet,
+ pad: "=",
+
+ /**
+ * Encode a string in Z-Base-32 encoding
+ *
+ * @param str String - input string
+ *
+ * @returns String - econded string
+ */
+ encode(str) {
+ let a = this.a;
+ let pad = this.pad;
+ let len = str.length;
+ let o = "";
+ let w,
+ c,
+ r = 0,
+ sh = 0;
+
+ for (let i = 0; i < len; i += 5) {
+ // mask top 5 bits
+ c = str.charCodeAt(i);
+ w = 0xf8 & c;
+ o += a.charAt(w >> 3);
+ r = 0x07 & c;
+ sh = 2;
+
+ if (i + 1 < len) {
+ c = str.charCodeAt(i + 1);
+ // mask top 2 bits
+ w = 0xc0 & c;
+ o += a.charAt((r << 2) + (w >> 6));
+ o += a.charAt((0x3e & c) >> 1);
+ r = c & 0x01;
+ sh = 4;
+ }
+
+ if (i + 2 < len) {
+ c = str.charCodeAt(i + 2);
+ // mask top 4 bits
+ w = 0xf0 & c;
+ o += a.charAt((r << 4) + (w >> 4));
+ r = 0x0f & c;
+ sh = 1;
+ }
+
+ if (i + 3 < len) {
+ c = str.charCodeAt(i + 3);
+ // mask top 1 bit
+ w = 0x80 & c;
+ o += a.charAt((r << 1) + (w >> 7));
+ o += a.charAt((0x7c & c) >> 2);
+ r = 0x03 & c;
+ sh = 3;
+ }
+
+ if (i + 4 < len) {
+ c = str.charCodeAt(i + 4);
+ // mask top 3 bits
+ w = 0xe0 & c;
+ o += a.charAt((r << 3) + (w >> 5));
+ o += a.charAt(0x1f & c);
+ r = 0;
+ sh = 0;
+ }
+ }
+ // Calculate length of pad by getting the
+ // number of words to reach an 8th octet.
+ if (r != 0) {
+ o += a.charAt(r << sh);
+ }
+ var padlen = 8 - (o.length % 8);
+
+ if (padlen === 8) {
+ return o;
+ }
+
+ if (padlen === 1 || padlen === 3 || padlen === 4 || padlen === 6) {
+ return o + pad.repeat(padlen);
+ }
+
+ throw new Error(
+ "there was some kind of error:\npadlen:" +
+ padlen +
+ " ,r:" +
+ r +
+ " ,sh:" +
+ sh +
+ ", w:" +
+ w
+ );
+ },
+};