summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp')
-rw-r--r--comm/mail/extensions/openpgp/README.md22
-rw-r--r--comm/mail/extensions/openpgp/content/BondOpenPGP.jsm86
-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
-rw-r--r--comm/mail/extensions/openpgp/content/strings/enigmail.properties348
-rw-r--r--comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml17
-rw-r--r--comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js123
-rw-r--r--comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml67
-rw-r--r--comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js152
-rw-r--r--comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml73
-rw-r--r--comm/mail/extensions/openpgp/content/ui/commonWorkflows.js194
-rw-r--r--comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js222
-rw-r--r--comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml94
-rw-r--r--comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js102
-rw-r--r--comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml55
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailCommon.js69
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js172
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml42
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js1442
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml406
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js3460
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js181
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml71
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js3034
-rw-r--r--comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js1214
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml119
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyAssistant.js956
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js1119
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml405
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyWizard.js1195
-rw-r--r--comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml506
-rw-r--r--comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js177
-rw-r--r--comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml86
-rw-r--r--comm/mail/extensions/openpgp/jar.mn14
-rw-r--r--comm/mail/extensions/openpgp/moz.build7
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpgbin0 -> 9727 bytes
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.asc200
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.gpgbin0 -> 9387 bytes
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-key-and-windows-1252-encoded-eml-attachment.eml109
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-windows-1252-encoded-eml-attachment.eml39
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/test_alias.js321
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/test_badKeys.js69
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/test_encryptAndOrSign.js278
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/test_secretKeys.js384
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/test_strip.js137
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/test_uid.js132
-rw-r--r--comm/mail/extensions/openpgp/test/unit/rnp/xpcshell.ini14
91 files changed, 44431 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/README.md b/comm/mail/extensions/openpgp/README.md
new file mode 100644
index 0000000000..5e7ae35fe2
--- /dev/null
+++ b/comm/mail/extensions/openpgp/README.md
@@ -0,0 +1,22 @@
+This directory contains an incomplete OpenPGP email integration,
+which is based on an initial import of Enigmail Add-on code.
+
+- The code is disabled by default, and can be enabled using
+ build time configuration --enable-openpgp
+
+- Care must be taken that any changes to this directory have no
+ functional effect on the default behavior of TB.
+
+- Any commits to this directory that accidentally cause the automated
+ tests of TB to break may be backed out immediately.
+
+- All commits will be done with DONTBUILD in the commit comment,
+ to avoid unnecessary load on the infrastructure.
+
+- For questions or changes, consult:
+ Kai Engert, Patrick Brunschwig, Magnus Melin
+
+- Prior to enabling this code, all code must be enabled for
+ eslint and must be fully reviewd, as tracked in:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1595319
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1595325
diff --git a/comm/mail/extensions/openpgp/content/BondOpenPGP.jsm b/comm/mail/extensions/openpgp/content/BondOpenPGP.jsm
new file mode 100644
index 0000000000..e9d1086cc7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/BondOpenPGP.jsm
@@ -0,0 +1,86 @@
+/* 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 file is a thin interface on top of the rest of the OpenPGP
+ * integration ot minimize the amount of code that must be
+ * included in files outside the extensions/openpgp directory. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["BondOpenPGP"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+ RNP: "chrome://openpgp/content/modules/RNP.jsm",
+ GPGME: "chrome://openpgp/content/modules/GPGME.jsm",
+});
+
+/*
+// Enable this block to view syntax errors in these files, which are
+// difficult to see when lazy loading.
+var { GPGME } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/GPGME.jsm"
+);
+var { RNP } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNP.jsm"
+);
+var { GPGMELibLoader } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/GPGMELib.jsm"
+);
+var { RNPLibLoader } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNPLib.jsm"
+);
+*/
+
+var BondOpenPGP = {
+ logException(exc) {
+ try {
+ Services.console.logStringMessage(exc.toString() + "\n" + exc.stack);
+ } catch (x) {}
+ },
+
+ _alreadyTriedInit: false, // if already true, we will not try again
+
+ async init() {
+ if (this._alreadyTriedInit) {
+ // We have previously attempted to init, don't try again.
+ return;
+ }
+
+ this._alreadyTriedInit = true;
+
+ lazy.EnigmailKeyRing.init();
+ lazy.EnigmailVerify.init();
+
+ let initDone = await lazy.RNP.init({});
+ if (!initDone) {
+ let { error } = this.getRNPLibStatus();
+ throw new Error(error);
+ }
+
+ if (Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg")) {
+ lazy.GPGME.init({});
+ }
+
+ // trigger service init
+ await lazy.EnigmailCore.getService();
+ },
+
+ getRNPLibStatus() {
+ return lazy.RNP.getRNPLibStatus();
+ },
+
+ openKeyManager(window) {
+ lazy.EnigmailWindows.openKeyManager(window);
+ },
+};
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
+ );
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/strings/enigmail.properties b/comm/mail/extensions/openpgp/content/strings/enigmail.properties
new file mode 100644
index 0000000000..c1daee4244
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/strings/enigmail.properties
@@ -0,0 +1,348 @@
+# 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/.
+
+#####################################################################
+# Strings used within enigmailCommon.js and enigmailCommon.jsm
+#####################################################################
+
+### dlgYes=&Yes
+### dlgNo=&No
+### dlg.button.overwrite=&Overwrite
+### dlg.button.ignore=&Ignore
+### dlg.button.install=&Install
+
+#####################################################################
+### Strings in enigmailAbout.js
+#####################################################################
+
+### usingAgent=Using %1$S executable %2$S to encrypt and decrypt
+
+#####################################################################
+# Strings in enigmailKeygen.js
+#####################################################################
+
+### onlyGPG=Key generation only works with GnuPG (not with PGP)!
+
+### keygenComplete=Key generation completed! Identity <%S> will be used for signing.
+### revokeCertRecommended=We highly recommend to create a revocation certificate for your key. This certificate can be used to invalidate your key, e.g. in case your secret key gets lost or compromised. Do you want to create such a revocation certificate now?
+### keyMan.button.generateCert=&Generate Certificate
+### genCompleteNoSign=Key generation completed!
+
+### passNoMatch=Passphrase entries do not match; please re-enter
+### passCheckBox=Please check box if specifying no passphrase for key
+### passUserName=Please specify user name for this identity
+### keygen.passCharProblem=You are using special characters in your passphrase. Unfortunately, this can cause troubles for other applications. We recommend you choose a passphrase consisting only of any of these characters:\na-z A-Z 0-9 /.;:-,!?(){}[]%*
+### passSpaceProblem=Due to technical reasons, your passphrase may not start or end with a space character.
+### changePassFailed=Changing the passphrase failed.
+
+### expiryTooLongShorter=You cannot create a key that expires in more than 90 years.
+### setKeyExpirationDateFailed=The expiration date could not be changed
+
+### notePartEncrypted2=*Parts of the message have NOT been signed nor encrypted*
+### noteCutMessage2=*Multiple message blocks found -- decryption/verification aborted*
+
+### detailsDlg.importKey=Import key
+### wksNoIdentity=This key is not linked to any of your email accounts. Please add an account for at least one of the following email addresse(s):\n\n%S
+### wksConfirmSuccess=Confirmation email sent.
+### wksConfirmFailure=Sending the confirmation email failed.
+
+#####################################################################
+# Strings in enigmailMsgComposeOverlay.js
+#####################################################################
+
+### keysToUse=Select OpenPGP Key(s) to use for %S
+### pubKey=Public key for %S\n
+
+### composeSpecifyEmail=Please specify your primary email address, which will be used to choose the signing key for outgoing messages.\n If you leave it blank, the FROM address of the message will be used to choose the signing key.
+### attachWarning=Attachments to this message are not local, they cannot be encrypted. In order to encrypt the attachments, store them as local files first and attach these files. Do you wish to send the message anyway?
+### warning=Warning
+### signIconClicked=You have manually modified signing. Therefore, while you are composing this message, (de)activating signing does not depend anymore on (de)activating encryption.
+
+### msgCompose.internalEncryptionError=Internal Error: promised encryption disabled
+
+### msgCompose.protectSubject.tooltip=Protect the message subject
+### msgCompose.noSubjectProtection.tooltip=Do not protect the message subject
+### msgCompose.protectSubject.dialogTitle=Enable Protection of Subject?
+### msgCompose.protectSubject.question=Regular encrypted emails contain the unredacted subject.\n\nWe have established a standard to hide the original subject in the encrypted message\nand replace it with a dummy text, such that the subject is only visible after the email is decrypted.\n\nDo you want to protect the subject in encrypted messages?
+### msgCompose.protectSubject.yesButton=&Protect subject
+### msgCompose.protectSubject.noButton=&Leave subject unprotected
+
+# note: should end with double newline:
+
+# details:
+
+### statPGPMIME=PGP/MIME
+### statSMIME=S/MIME
+### statSigned=SIGNED
+### statEncrypted=ENCRYPTED
+### statPlain=UNSIGNED and UNENCRYPTED
+
+### offlineSave=Save %1$S message to %2$S in Unsent Messages folder?
+
+### onlineSend=Send %1$S message to %2$S?
+### encryptKeysNote=Note: The message is encrypted for the following User IDs / Keys: %S
+### hiddenKey=<hidden key>
+
+### msgCompose.button.sendUnencrypted=&Send Unencrypted Message
+### recipientsSelectionHdr=Select Recipients for Encryption
+
+# encryption/signing status and associated reasons:
+### encryptMessageAuto=Encrypt Message (auto)
+### encryptMessageNorm=Encrypt Message
+### signMessageAuto=Sign Message (auto)
+### signMessageNorm=Sign Message
+
+### encryptOff=Encryption: OFF
+### encryptOnWithReason=Encryption: ON (%S)
+### encryptOffWithReason=Encryption: OFF (%S)
+### encryptOn=Encryption: ON
+### signOn=Signing: ON
+### signOff=Signing: OFF
+### signOnWithReason=Signing: ON (%S)
+### signOffWithReason=Signing: OFF (%S)
+### reasonEnabledByDefault=enabled by default
+### reasonManuallyForced=manually forced
+### reasonByRecipientRules=forced by per-recipient rules
+### reasonByAutoEncryption=forced by auto encryption
+### reasonByConflict=due to conflict in per-recipient rules
+### reasonByEncryptionMode=due to encryption mode
+
+# should not be used anymore:
+### encryptYes=Message will be encrypted
+
+# should not be used anymore:
+### signYes=Message will be signed
+
+
+# PGP/MIME status:
+### pgpmimeNormal=Protocol: PGP/MIME
+### inlinePGPNormal=Protocol: Inline PGP
+### smimeNormal=Protocol: S/MIME
+### pgpmimeAuto=Protocol: PGP/MIME (auto)
+### inlinePGPAuto=Protocol: Inline PGP (auto)
+### smimeAuto=Protocol: S/MIME (auto)
+
+# should not be used anymore
+### pgpmimeYes=PGP/MIME will be used
+### pgpmimeNo=Inline PGP will be used
+
+# Attach own key status (tooltip strings):
+### attachOwnKeyYes=Your own public key will be attached
+### attachOwnKeyDisabled=Your own public key cannot be attached. You have to select a specific key\nin the OpenPGP section of the Account Settings to enable this feature.
+
+### rulesConflict=Conflicting per-recipient rules detected\n%S\n\nSend message with these settings?
+### msgCompose.button.configure=&Configure
+### msgCompose.button.save=&Save Message
+
+# Strings in enigmailMsgHdrViewOverlay.js
+### signatureFrom=Signature from public key %S
+### clickDecrypt=; use 'Decrypt/Verify' function
+### clickDecryptRetry=; use 'Decrypt/Verify' function to retry
+### clickDetailsButton=; click on 'Details' button for more information
+### clickImportButton=; click on the 'Import Key' button to import the key
+### keyTypeUnsupported=; the key type is not supported by your version of GnuPG
+### decryptManually=; click on the 'Decrypt' button to decrypt the message
+### verifyManually=; click on the 'Verify' button to verify the signature
+### headerView.button.verify=Verify
+### headerView.button.decrypt=Decrypt
+### msgPart=Part of the message %S
+### msgSigned=signed
+### msgSignedUnkownKey=signed with unknown key
+### msgEncrypted=encrypted
+### msgSignedAndEnc=signed and encrypted
+
+### goodSig=Good signature
+### uncertainSig=Uncertain signature
+### badSig=Bad signature
+### incompleteDecrypt=Decryption incomplete
+### needKey=Error - no matching secret key found to decrypt message
+### badPhrase=Error - bad passphrase
+### missingMdcError=Error - missing or broken integrity protection (MDC)
+### failedDecryptVerify=Error - decryption/verification failed
+### brokenExchangeMessage=Broken PGP/MIME message from MS-Exchange.
+
+### decryptedMsg=Decrypted message
+
+### usedAlgorithms=Used Algorithms: %1$S and %2$S
+
+### wksConfirmationReq=Web Key Directory Confirmation Request
+### wksConfirmationReq.message=This message has been sent by your email provider to confirm deployment of your OpenPGP public key\nin their Web Key Directory.\nProviding your public key helps others to discover your key and thus being able to encrypt messages to you.\n\nIf you want to deploy your key in the Web Key Directory now, please click on the button "Confirm Request" in the status bar.\nOtherwise, simply ignore this message.
+### wksConfirmationReq.button.label=Confirm Request
+
+### autocryptSetupReq.setupMsg.desc=This message contains all information to transfer your Autocrypt settings along with your secret key securely from your original device.
+### autocryptSetupReq.setupMsg.backup=You can keep this message and use it as a backup for your secret key. If you want to do this, you should write down the password and store it securely.
+### autocryptSetupReq.message.sent=Please click on the message on your new device and follow the instuctions to import the settings.
+
+# strings in pref-enigmail.js
+### locateGpg=Locate GnuPG program
+### warningsAreReset=All warnings have been reset.
+### prefs.gpgFound=GnuPG was found in %S
+### prefs.gpgNotFound=Could not find GnuPG
+### prefEnigmail.oneKeyserverOnly=Error - you can only specify one keyserver for automatic downloading of missing OpenPGP keys.
+### acSetupMessage.desc=Transfer your key to another Autocrypt-enabled device. (<html:span class='enigmailLink' href='https://autocrypt.org'>What is Autocrypt</html:span>)
+
+# Strings used in core.jsm
+# (said file also re-uses some strings from above)
+
+### enterAdminPin=Please type in the ADMIN PIN of your SmartCard
+### enterCardPin=Please type your SmartCard PIN
+
+### badCommand=Error - encryption command failed
+### cmdLine=command line and output:
+### notComplete=Error - key generation not yet completed
+
+### noPassphrase=Error - no passphrase supplied
+
+# Strings used in enigmailSingleRcptSettings.js
+### noEncryption=You have activated encryption, but you did not select a key. In order to encrypt emails sent to %1$S, you need to specify one or several valid key(s) from your key list. Do you want to disable encryption for %2$S?
+### noKeyToUse=(none - no encryption)
+### noEmptyRule=The Rule may not be empty! Please set an email address in the Rule field.
+### invalidAddress=The email address(es) you have entered are not valid. You should not set the names of the recipients, just the email addresses. E.g.:\nInvalid: Some Name <some.name@address.net>\nValid: some.name@address.net
+### noCurlyBrackets=The curly brackets {} have a special meaning and should not be used in an email address. If you want to modify the matching behavior for this rule, use the 'Apply rule if recipient ...' option.\nMore information is available from the Help button.
+
+# Strings used in enigmailRulesEditor.js
+### never=Never
+### always=Always
+### possible=Possible
+### deleteRule=Really delete the selected rule?
+### nextRcpt=(Next recipient)
+### negateRule=Not
+### addKeyToRule=Add key %1$S (%2$S) to per-recipient rule
+
+# Strings used in enigmailSearchKey.js
+### noKeyserverConn=Could not connect to keyserver at %S.
+### internalError=An internal error occurred. The keys could not be downloaded or imported.
+### keyDownload.keyUnavailable=The key with ID %S is not available on the keyserver. Most likely, the owner of the key did not upload their key to the keyserver.\n\nPlease ask the sender of the message to send you their public key by email.
+
+# Strings in enigmailEditKeyTrustDlg.xhtml
+### setKeyTrustFailed=Setting owner trust failed
+
+# Strings in enigmailSignKeyDlg.js
+### signKeyFailed=Key signing failed
+### alreadySigned.label=Note: the key %S is already signed with the selected secret key.
+### alreadySignedexportable.label=Note: the key %S is already signed exportable with the selected secret key. A local signature does not make sense.
+### partlySigned.label=Note: some user IDs of key %S are already signed with the selected secret key.
+### noTrustedOwnKeys=No eligible key found for signing! You need at least one fully trusted secret key in order to sign keys.
+
+# Strings in enigmailKeyManager.js
+### keyValid.noSubkey=no valid subkey
+
+### keyType.public=pub
+### keyType.publicAndSec=pub/sec
+
+### addUidOK=User ID added successfully
+### addUidFailed=Adding the User ID failed
+
+### sendKeysOk=Key(s) sent successfully
+### sendKeysFailed=Sending of keys failed
+### receiveKeysOk=Key(s) updated successfully
+### receiveKeysFailed=Downloading of keys failed
+### keyUpload.verifyEmails=The keyserver will send you an email for each email address of your uploaded key. To confirm publication of your key, you'll need to click on the link in each of the emails you'll receive.
+
+### deleteKeyFailed=The key could not be deleted.
+### revokeKeyOk=The key has been revoked. If your key is available on a key server, it is recommended to re-upload it, so that others can see the revocation.
+### revokeKeyFailed=The key could not be revoked.
+### refreshKeyServiceOn.warn=Warning: Your keys are currently being refreshed in the background as safely as possible.\nRefreshing all your keys at once will unnecessarily reveal information about you.\nDo you really want to do this?
+### downloadContactsKeys.importFrom=Import contacts from address book '%S'?
+
+### keylist.noOtherUids=Has no other identities
+### keylist.hasOtherUids=Also known as
+### keylist.noPhotos=No photo available
+### keylist.hasPhotos=Photos
+
+### keyMan.addphoto.filepicker.title=Select photo to add
+### keyMan.addphoto.warnLargeFile=The file you have chosen is larger than 25 kB.\nIt is not recommended to add very large files as it causes very large keys.
+### keyMan.addphoto.noJpegFile=The selected file does not appear to be a JPEG file. Please choose a different file.
+### keyMan.addphoto.failed=The photo could not be added.
+### noWksIdentity=The key %S does not have a WKS identity.
+### wksUpload.noKeySupported=The upload was not successful - your provider does not seem to support WKS.
+
+
+# Strings in enigmailManageUidDlg.xhtml
+### changePrimUidFailed=Changing the primary User ID failed
+### changePrimUidOK=The primary user ID was changed successfully
+### revokeUidFailed=Revoking the user ID %S failed
+### revokeUidOK=User ID %S was revoked successfully. If your key is available on a key server, it is recommended to re-upload it, so that others can see the revocation.
+### revokeUidQuestion=Do you really want to revoke the user ID %S?
+
+# Strings in enigmailGenCardKey.xhtml
+### keygen.started=Please wait while the key is being generated ....
+### keygen.completed=Key Generated. The new Key ID is: 0x%S
+### keygen.keyBackup=The key is backed up as %S
+### keygen.passRequired=Please specify a passphrase if you want to create a backup copy of your key outside your SmartCard.
+
+# Strings in enigmailSetCardPin.xhtml
+### cardPin.processFailed=Failed to change PIN
+
+# Strings in enigRetrieveProgres.js
+### keyserverProgress.refreshing=Refreshing keys, please wait ...
+### keyserverProgress.uploading=Uploading keys, please wait ...
+### keyserverProgress.wksUploadFailed=Could not upload your key to the Web Key Service
+### keyserverProgress.wksUploadCompleted=Your public key was successfully submitted to your provider. You will receive an email to confirm that you initiated the upload.
+### keyserverTitle.refreshing=Refresh Keys
+### keyserverTitle.uploading=Key Upload
+### keyserver.result.download.none=No key downloaded.
+### keyserver.result.download.1of1=Key successfully downloaded.
+### keyserver.result.download.1ofN=Successfully downloaded 1 of %S keys.
+### keyserver.result.download.NofN=Successfully downloaded %1$S of %2$S keys.
+### keyserver.result.uploadOne=Successfully uploaded 1 key.
+### keyserver.result.uploadMany=Successfully uploaded %S keys.
+
+# Strings in installGnuPG.jsm
+### installGnuPG.downloadFailed=An error occurred while trying to download GnuPG. Please check the console log for further details.
+### installGnuPG.installFailed=An error occurred while installing GnuPG. Please check the console log for further details.
+
+# Strings in enigmailAddUidDlg.xhtml
+### addUidDlg.nameOrEmailError=You have to fill in a name and an email address
+### addUidDlg.nameMinLengthError=The name must at least have 5 characters
+### addUidDlg.invalidEmailError=You must specify a valid email address
+
+# Strings in enigmailCardDetails.js
+### Carddetails.NoASCII=OpenPGP Smartcards only support ASCII characters in Firstname/Name.
+
+# network error types
+### errorType.SecurityCertificate=The security certificate presented by the web service is not valid.
+### errorType.SecurityProtocol=The security protocol used by the web service is unknown.
+### errorType.Network=A network error has occurred.
+
+### converter.decryptAtt.failed=Could not decrypt attachment '%1$S'\nof message with subject\n'%2$S'.\nDo you want to retry with a different passphrase or do you want to skip the message?
+
+### saveLogFile.title=Save Log File
+
+#strings in exportSettingsWizard.js
+### cannotWriteToFile=Cannot save to file '%S'. Please select a different file.
+### dataExportError=An error occurred during exporting your data.
+### specifyExportFile=Specify file name for exporting
+### homedirParamNotSUpported=Additional parameters that configure paths such as --homedir and --keyring are not supported for exporting/restoring your settings. Please use alternative methods such as setting the environment variable GNUPGHOME.
+
+#strings in gpgAgent.jsm
+### gpghomedir.notexists=The directory '%S' containing your OpenPGP keys does not exist and cannot be created.
+### gpghomedir.notwritable=The directory '%S' containing your OpenPGP keys is not writable.
+### gpghomedir.notdirectory=The directory '%S' containing your OpenPGP keys is a file instead of a directory.
+### gpghomedir.notusable=Please fix the directory permissions or change the location of your GnuPG "home" directory. GnuPG cannot work correctly otherwise.
+
+### handshakeDlg.button.initHandshake=Handshake...
+### handshakeDlg.button.stopTrust=Stop Trusting
+### handshakeDlg.button.reTrust=Stop Mistrusting
+### handshakeDlg.label.outgoingMessage=Outgoing message
+### handshakeDlg.label.incomingMessage=Incoming message
+### handshakeDlg.error.noPeers=Cannot handshake without any correspondents.
+### handshakeDlg.error.noProtection=Please enable protection in order to use the "Handshake" function.
+
+### enigmail.acSetupPasswd.descEnterPasswd=Please enter the setup code that is displayed on the other device.
+### enigmail.acSetupPasswd.descCopyPasswd=Please enter the setup code below on your other device to proceed with the setup.
+
+#strings in autocrypt.jsm
+
+### autocrypt.setupMsg.subject=Autocrypt Setup Message
+### autocrypt.setupMsg.msgBody=To set up your new device for Autocrypt, please follow the instuctions that should be presented by your new device.
+### autocrypt.setupMsg.fileTxt=This is the Autocrypt setup file used to transfer settings and keys between clients. You can decrypt it using the setup code displayed on your old device, then import the key to your keyring.
+
+#strings in gnupg-key.jsm
+### import.secretKeyImportError=An error has occurred in GnuPG while importing secret keys. The import was not successful.
+
+#strings in importSettings.js
+### importSettings.errorNoFile=The file you specified is not a regular file!
+### importSettings.cancelWhileInProgress=Restoring is in progress. Do you really want to abort the process?
+### importSettings.button.abortImport=&Abort process
diff --git a/comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml b/comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml
new file mode 100644
index 0000000000..060752b497
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml
@@ -0,0 +1,17 @@
+# 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/.
+
+ <menuseparator id="openpgpCtxItemsSeparator"/>
+ <menuitem id="enigmail_ctxImportKey"
+ data-l10n-id="openpgp-ctx-import-key"
+ oncommand="Enigmail.msg.handleAttachmentSel('importKey');"/>
+ <menuitem id="enigmail_ctxDecryptOpen"
+ data-l10n-id="openpgp-ctx-decrypt-open"
+ oncommand="Enigmail.msg.handleAttachmentSel('openAttachment');"/>
+ <menuitem id="enigmail_ctxDecryptSave"
+ data-l10n-id="openpgp-ctx-decrypt-save"
+ oncommand="Enigmail.msg.handleAttachmentSel('saveAttachment');"/>
+ <menuitem id="enigmail_ctxVerifyAtt"
+ data-l10n-id="openpgp-ctx-verify-att"
+ oncommand="Enigmail.msg.handleAttachmentSel('verifySig');"/>
diff --git a/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js
new file mode 100644
index 0000000000..9916429bcb
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js
@@ -0,0 +1,123 @@
+/* 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";
+
+/**
+ * @file Implements the functionality of backupKeyPassword.xhtml:
+ * a dialog that lets the user enter the password used to protect
+ * a backup of OpenPGP secret keys.
+ * Based on setp12password.js and setp12password.xhtml
+ */
+
+/**
+ * @property {boolean} confirmedPassword
+ * Set to true if the user entered two matching passwords and
+ * confirmed the dialog.
+ * @property {string} password
+ * The password the user entered. Undefined value if
+ * |confirmedPassword| is not true.
+ */
+
+let gAcceptButton;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+/**
+ * onload() handler.
+ */
+function onLoad() {
+ // Ensure the first password textbox has focus.
+ document.getElementById("pw1").focus();
+ document.addEventListener("dialogaccept", onDialogAccept);
+ gAcceptButton = document
+ .getElementById("backupKeyPassword")
+ .getButton("accept");
+ gAcceptButton.disabled = true;
+}
+
+/**
+ * ondialogaccept() handler.
+ */
+function onDialogAccept() {
+ window.arguments[0].okCallback(
+ document.getElementById("pw1").value,
+ window.arguments[0].fprArray,
+ window.arguments[0].file,
+ true
+ );
+}
+
+/**
+ * Calculates the strength of the given password, suitable for use in updating
+ * a progress bar that represents said strength.
+ *
+ * The strength of the password is calculated by checking the number of:
+ * - Characters
+ * - Numbers
+ * - Non-alphanumeric chars
+ * - Upper case characters
+ *
+ * @param {string} password
+ * The password to calculate the strength of.
+ * @returns {number}
+ * The strength of the password in the range [0, 100].
+ */
+function getPasswordStrength(password) {
+ let lengthStrength = password.length;
+ if (lengthStrength > 5) {
+ lengthStrength = 5;
+ }
+
+ let nonNumericChars = password.replace(/[0-9]/g, "");
+ let numericStrength = password.length - nonNumericChars.length;
+ if (numericStrength > 3) {
+ numericStrength = 3;
+ }
+
+ let nonSymbolChars = password.replace(/\W/g, "");
+ let symbolStrength = password.length - nonSymbolChars.length;
+ if (symbolStrength > 3) {
+ symbolStrength = 3;
+ }
+
+ let nonUpperAlphaChars = password.replace(/[A-Z]/g, "");
+ let upperAlphaStrength = password.length - nonUpperAlphaChars.length;
+ if (upperAlphaStrength > 3) {
+ upperAlphaStrength = 3;
+ }
+
+ let strength =
+ lengthStrength * 10 -
+ 20 +
+ numericStrength * 10 +
+ symbolStrength * 15 +
+ upperAlphaStrength * 10;
+ if (strength < 0) {
+ strength = 0;
+ }
+ if (strength > 100) {
+ strength = 100;
+ }
+
+ return strength;
+}
+
+/**
+ * oninput() handler for both password textboxes.
+ *
+ * @param {boolean} recalculatePasswordStrength
+ * Whether to recalculate the strength of the first password.
+ */
+function onPasswordInput(recalculatePasswordStrength) {
+ let pw1 = document.getElementById("pw1").value;
+
+ if (recalculatePasswordStrength) {
+ document.getElementById("pwmeter").value = getPasswordStrength(pw1);
+ }
+
+ // Disable the accept button if the two passwords don't match, and enable it
+ // if the passwords do match.
+ let pw2 = document.getElementById("pw2").value;
+ gAcceptButton.disabled = pw1 != pw2 || !pw1.length;
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml
new file mode 100644
index 0000000000..1fe18e1b0f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml
@@ -0,0 +1,67 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/backupKeyPassword.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title data-l10n-id="set-password-window-title"></title>
+ <link rel="localization" href="messenger/openpgp/backupKeyPassword.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js" />
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js" />
+ <script
+ defer="defer"
+ src="chrome://openpgp/content/ui/backupKeyPassword.js"
+ />
+ </head>
+ <body>
+ <xul:dialog id="backupKeyPassword" buttons="accept,cancel">
+ <p data-l10n-id="set-password-message"></p>
+
+ <fieldset>
+ <legend data-l10n-id="set-password-legend"></legend>
+
+ <label for="pw1" data-l10n-id="set-password-backup-pw-label" />
+ <input
+ id="pw1"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput(true);"
+ />
+
+ <label for="pw2" data-l10n-id="set-password-backup-pw2-label" />
+ <input
+ id="pw2"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput(false);"
+ />
+
+ <label for="pwmeter" data-l10n-id="password-quality-meter" />
+ <progress id="pwmeter" value="0" max="100"></progress>
+ </fieldset>
+
+ <div class="inline-notification-container info-container self-center">
+ <img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <p data-l10n-id="set-password-reminder"></p>
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js
new file mode 100644
index 0000000000..f6974ddce9
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js
@@ -0,0 +1,152 @@
+/* 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/. */
+
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { RNP, RnpPrivateKeyUnlockTracker } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNP.jsm"
+);
+
+let gFingerprints = [];
+let gKeyCreated;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+function onLoad() {
+ let keyId = window.arguments[0].keyId;
+ let keyObj = EnigmailKeyRing.getKeyById(window.arguments[0].keyId);
+ if (!keyObj) {
+ throw new Error(`Key not found: ${keyId}`);
+ }
+ if (!keyObj.secretAvailable) {
+ keyObj = null;
+ throw new Error(`Not your key: ${keyId}`);
+ }
+
+ if (!keyObj.iSimpleOneSubkeySameExpiry()) {
+ window.close();
+ return;
+ }
+
+ gFingerprints = [keyObj.fpr, keyObj.subKeys[0].fpr];
+ gKeyCreated = keyObj.keyCreated;
+
+ let currentExpiryInfo = document.getElementById("info-current-expiry");
+
+ if (!keyObj.expiryTime) {
+ document.l10n.setAttributes(currentExpiryInfo, "info-does-not-expire");
+ } else {
+ let nowSeconds = Math.floor(Date.now() / 1000);
+ if (keyObj.expiryTime < nowSeconds) {
+ document.l10n.setAttributes(currentExpiryInfo, "info-already-expired");
+ } else {
+ document.l10n.setAttributes(currentExpiryInfo, "info-will-expire", {
+ date: keyObj.expiry,
+ });
+ }
+ }
+
+ // Don't explain how to use longer, if this key already never expires.
+ document.getElementById("longerUsage").hidden = !keyObj.expiryTime;
+
+ let popup = document.getElementById("expiry-in");
+ let rtf = new Intl.RelativeTimeFormat(undefined, {
+ numeric: "always",
+ style: "long",
+ });
+ let today = new Date();
+ for (let i = 1; i < 24; i++) {
+ let d = new Date(
+ today.getFullYear(),
+ today.getMonth() + i,
+ today.getDate()
+ );
+ let option = document.createElement("option");
+ option.value = Math.floor(d.getTime() / 1000); // In seconds.
+ option.label = rtf.format(i, "month");
+ popup.appendChild(option);
+ }
+ for (let i = 2; i <= 10; i++) {
+ let d = new Date(
+ today.getFullYear() + i,
+ today.getMonth(),
+ today.getDate()
+ );
+ let option = document.createElement("option");
+ option.value = Math.floor(d.getTime() / 1000); // In seconds.
+ option.label = rtf.format(i, "year");
+ popup.appendChild(option);
+ }
+ if (keyObj.expiryTime) {
+ popup.selectedIndex = [...popup.children].findIndex(
+ o => o.value >= keyObj.expiryTime
+ );
+ } else {
+ popup.selectedIndex = 23; // 2 years
+ }
+ document.getElementById("radio-expire-yes").value = popup.value;
+
+ popup.addEventListener("change", event => {
+ document.getElementById("radio-expire-yes").value = event.target.value;
+ document.getElementById("radio-expire-yes").checked = true;
+ });
+}
+
+async function onAccept() {
+ let expirySecs = +document.querySelector("input[name='expiry']:checked")
+ .value;
+ if (expirySecs < 0) {
+ // Keep.
+ return true;
+ }
+ // Key Expiration Time - this is the number of seconds after the key creation
+ // time that the key expires.
+ let keyExpirationTime = expirySecs ? expirySecs - gKeyCreated : 0;
+
+ let pwCache = {
+ passwords: [],
+ };
+
+ let unlockFailed = false;
+ let keyTrackers = [];
+ for (let fp of gFingerprints) {
+ let tracker = RnpPrivateKeyUnlockTracker.constructFromFingerprint(fp);
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ tracker.setPasswordCache(pwCache);
+ await tracker.unlock();
+ keyTrackers.push(tracker);
+ if (!tracker.isUnlocked()) {
+ unlockFailed = true;
+ break;
+ }
+ }
+
+ let rv = false;
+ if (!unlockFailed) {
+ rv = RNP.changeExpirationDate(gFingerprints, keyExpirationTime);
+ }
+
+ for (let t of keyTrackers) {
+ t.release();
+ }
+ return rv;
+}
+
+document.addEventListener("dialogaccept", async function (event) {
+ // Prevent the closing of the dialog to wait until the call
+ // to onAccept() has properly returned.
+ event.preventDefault();
+ let result = await onAccept();
+ // If the change was unsuccessful, leave this dialog open.
+ if (!result) {
+ return;
+ }
+ // Otherwise, update the parent window and close the dialog.
+ window.arguments[0].modified();
+ window.close();
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml
new file mode 100644
index 0000000000..ddfd67ce24
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml
@@ -0,0 +1,73 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/changeExpiryDlg.css" type="text/css"?>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title data-l10n-id="openpgp-change-expiry-title"></title>
+ <link rel="localization" href="messenger/openpgp/changeExpiryDlg.ftl" />
+ <script
+ defer="defer"
+ src="chrome://openpgp/content/ui/changeExpiryDlg.js"
+ ></script>
+ </head>
+ <body>
+ <xul:dialog id="changeExpiryDlg" buttons="cancel,accept">
+ <div class="inline-notification-container info-container self-center">
+ <img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <p data-l10n-id="info-explanation-1"></p>
+ </div>
+
+ <p id="info-current-expiry"></p>
+
+ <p id="longerUsage" data-l10n-id="info-explanation-2"></p>
+
+ <fieldset>
+ <div>
+ <input
+ id="radio-expire-keep"
+ type="radio"
+ name="expiry"
+ value="-1"
+ checked="checked"
+ />
+ <label
+ for="radio-expire-keep"
+ data-l10n-id="expire-no-change-label"
+ />
+ </div>
+
+ <div>
+ <input id="radio-expire-yes" type="radio" name="expiry" value="" />
+ <label for="radio-expire-yes" data-l10n-id="expire-in-time-label" />
+
+ <select id="expiry-in"></select>
+ </div>
+
+ <div>
+ <input id="radio-expire-no" type="radio" name="expiry" value="0" />
+ <label
+ for="radio-expire-no"
+ data-l10n-id="expire-never-expire-label"
+ />
+ </div>
+ </fieldset>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mail/extensions/openpgp/content/ui/commonWorkflows.js b/comm/mail/extensions/openpgp/content/ui/commonWorkflows.js
new file mode 100644
index 0000000000..ac6c054e2f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/commonWorkflows.js
@@ -0,0 +1,194 @@
+/*
+ * 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 { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailArmor } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/armor.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+/**
+ * opens a prompt, asking the user to enter passphrase for given key id
+ * returns: the passphrase if entered (empty string is allowed)
+ * resultFlags.canceled is set to true if the user clicked cancel
+ */
+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;
+}
+
+/**
+ * @param {nsIFile} file
+ * @returns {string} The first block of the wanted type, or empty string.
+ * Skip blocks of wrong type.
+ */
+async function getKeyBlockFromFile(file, wantSecret) {
+ let contents = await IOUtils.readUTF8(file.path).catch(() => "");
+ let searchOffset = 0;
+
+ while (searchOffset < contents.length) {
+ const beginIndexObj = {};
+ const endIndexObj = {};
+ const blockType = EnigmailArmor.locateArmoredBlock(
+ contents,
+ searchOffset,
+ "",
+ beginIndexObj,
+ endIndexObj,
+ {}
+ );
+ if (!blockType) {
+ return "";
+ }
+
+ if (
+ (wantSecret && blockType.search(/^PRIVATE KEY BLOCK$/) !== 0) ||
+ (!wantSecret && blockType.search(/^PUBLIC KEY BLOCK$/) !== 0)
+ ) {
+ searchOffset = endIndexObj.value;
+ continue;
+ }
+
+ return contents.substr(
+ beginIndexObj.value,
+ endIndexObj.value - beginIndexObj.value + 1
+ );
+ }
+ return "";
+}
+
+/**
+ * import OpenPGP keys from file
+ *
+ * @param {string} what - "rev" for revocation, "pub" for public keys
+ */
+async function EnigmailCommon_importObjectFromFile(what) {
+ if (what != "rev" && what != "pub") {
+ throw new Error(`Can't import. Invalid argument: ${what}`);
+ }
+
+ let importingRevocation = what == "rev";
+ let promptStr = importingRevocation ? "import-rev-file" : "import-key-file";
+
+ let files = EnigmailDialog.filePicker(
+ window,
+ l10n.formatValueSync(promptStr),
+ "",
+ false,
+ true,
+ "*.asc",
+ "",
+ [l10n.formatValueSync("gnupg-file"), "*.asc;*.gpg;*.pgp"]
+ );
+
+ if (!files.length) {
+ return;
+ }
+
+ for (let file of files) {
+ if (file.fileSize > 5000000) {
+ document.l10n.formatValue("file-to-big-to-import").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ continue;
+ }
+
+ let errorMsgObj = {};
+
+ if (importingRevocation) {
+ await EnigmailKeyRing.importRevFromFile(file);
+ continue;
+ }
+
+ let importBinary = false;
+ let keyBlock = await getKeyBlockFromFile(file, false);
+
+ // if we don't find an ASCII block, try to import as binary.
+ if (!keyBlock) {
+ importBinary = true;
+ let data = await IOUtils.read(file.path);
+ keyBlock = MailStringUtils.uint8ArrayToByteString(data);
+ }
+
+ // Generate a preview of the imported key.
+ let preview = await EnigmailKey.getKeyListFromKeyBlock(
+ keyBlock,
+ errorMsgObj,
+ true, // interactive
+ true,
+ false // not secret
+ );
+
+ if (!preview || !preview.length || errorMsgObj.value) {
+ document.l10n.formatValue("import-keys-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n\n" + errorMsgObj.value);
+ });
+ continue;
+ }
+
+ if (preview.length > 0) {
+ let confirmImport = false;
+ let autoAcceptance = null;
+ let outParam = {};
+ confirmImport = EnigmailDialog.confirmPubkeyImport(
+ window,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ autoAcceptance = outParam.acceptance;
+ }
+
+ if (confirmImport) {
+ // import
+ let resultKeys = {};
+
+ let importExitCode = EnigmailKeyRing.importKey(
+ window,
+ false, // interactive, we already asked for confirmation
+ keyBlock,
+ importBinary,
+ null, // expected keyId, ignored
+ errorMsgObj,
+ resultKeys,
+ false, // minimize
+ [], // filter
+ true, // allow prompt for permissive
+ autoAcceptance
+ );
+
+ if (importExitCode !== 0) {
+ document.l10n.formatValue("import-keys-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n\n" + errorMsgObj.value);
+ });
+ continue;
+ }
+
+ EnigmailDialog.keyImportDlg(window, resultKeys.value);
+ }
+ }
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js
new file mode 100644
index 0000000000..a6320072ab
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js
@@ -0,0 +1,222 @@
+/* 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/. */
+
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+const { OpenPGPAlias } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/OpenPGPAlias.jsm"
+);
+const { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+var gListBox;
+var gViewButton;
+
+var gEmailAddresses = [];
+var gRowToEmail = [];
+
+// One boolean entry per row. True means it is an alias row.
+// This allows us to use different dialog behavior for alias entries.
+var gAliasRows = [];
+
+var gMapAddressToKeyObjs = null;
+
+function addRecipients(toAddrList, recList) {
+ for (var i = 0; i < recList.length; i++) {
+ try {
+ let entry = EnigmailFuncs.stripEmail(recList[i].replace(/[",]/g, ""));
+ toAddrList.push(entry);
+ } catch (ex) {
+ console.debug(ex);
+ }
+ }
+}
+
+async function setListEntries() {
+ gMapAddressToKeyObjs = new Map();
+
+ for (let addr of gEmailAddresses) {
+ addr = addr.toLowerCase();
+
+ let statusStringID = null;
+ let statusStringDirect = "";
+
+ let aliasKeyList = EnigmailKeyRing.getAliasKeyList(addr);
+ let isAlias = !!aliasKeyList;
+
+ if (isAlias) {
+ let aliasKeys = EnigmailKeyRing.getAliasKeys(aliasKeyList);
+ if (!aliasKeys.length) {
+ // failure, at least one alias key is unusable/unavailable
+ statusStringDirect = await document.l10n.formatValue(
+ "openpgp-compose-alias-status-error"
+ );
+ } else {
+ statusStringDirect = await document.l10n.formatValue(
+ "openpgp-compose-alias-status-direct",
+ {
+ count: aliasKeys.length,
+ }
+ );
+ }
+ } else {
+ // We ask to include keys which are expired, because that's what
+ // our sub dialog oneRecipientStatus needs. This is for
+ // efficiency - because otherwise the sub dialog would have to
+ // query all keys again.
+ // The consequence is, we need to later call isValidForEncryption
+ // for the keys we have obtained, to confirm they are really valid.
+ let foundKeys = await EnigmailKeyRing.getMultValidKeysForOneRecipient(
+ addr,
+ true
+ );
+ if (!foundKeys || !foundKeys.length) {
+ statusStringID = "openpgp-recip-missing";
+ } else {
+ gMapAddressToKeyObjs.set(addr, foundKeys);
+ for (let keyObj of foundKeys) {
+ let goodPersonal = false;
+ if (keyObj.secretAvailable) {
+ goodPersonal = await PgpSqliteDb2.isAcceptedAsPersonalKey(
+ keyObj.fpr
+ );
+ }
+ if (
+ goodPersonal ||
+ (EnigmailKeyRing.isValidForEncryption(keyObj) &&
+ (keyObj.acceptance == "verified" ||
+ keyObj.acceptance == "unverified"))
+ ) {
+ statusStringID = "openpgp-recip-good";
+ break;
+ }
+ }
+ if (!statusStringID) {
+ statusStringID = "openpgp-recip-none-accepted";
+ }
+ }
+ }
+
+ let listitem = document.createXULElement("richlistitem");
+
+ let emailItem = document.createXULElement("label");
+ emailItem.setAttribute("value", addr);
+ emailItem.setAttribute("crop", "end");
+ emailItem.setAttribute("style", "width: var(--recipientWidth)");
+ listitem.appendChild(emailItem);
+
+ let status = document.createXULElement("label");
+
+ if (statusStringID) {
+ document.l10n.setAttributes(status, statusStringID);
+ } else {
+ status.setAttribute("value", statusStringDirect);
+ }
+
+ status.setAttribute("crop", "end");
+ status.setAttribute("style", "width: var(--statusWidth)");
+ listitem.appendChild(status);
+
+ gListBox.appendChild(listitem);
+
+ gRowToEmail.push(addr);
+ gAliasRows.push(isAlias);
+ }
+}
+
+async function onLoad() {
+ let params = window.arguments[0];
+ if (!params) {
+ return;
+ }
+
+ try {
+ await OpenPGPAlias.load();
+ } catch (ex) {
+ console.log("failed to load OpenPGP alias file: " + ex);
+ }
+
+ gListBox = document.getElementById("infolist");
+ gViewButton = document.getElementById("detailsButton");
+
+ var arrLen = {};
+ var recList;
+
+ if (params.compFields.to) {
+ recList = params.compFields.splitRecipients(
+ params.compFields.to,
+ true,
+ arrLen
+ );
+ addRecipients(gEmailAddresses, recList);
+ }
+ if (params.compFields.cc) {
+ recList = params.compFields.splitRecipients(
+ params.compFields.cc,
+ true,
+ arrLen
+ );
+ addRecipients(gEmailAddresses, recList);
+ }
+ if (params.compFields.bcc) {
+ recList = params.compFields.splitRecipients(
+ params.compFields.bcc,
+ true,
+ arrLen
+ );
+ addRecipients(gEmailAddresses, recList);
+ }
+
+ await setListEntries();
+}
+
+async function reloadAndReselect(selIndex = -1) {
+ while (true) {
+ let child = gListBox.lastChild;
+ // keep first child, which is the header
+ if (child == gListBox.firstChild) {
+ break;
+ }
+ gListBox.removeChild(child);
+ }
+ gRowToEmail = [];
+ await setListEntries();
+ gListBox.selectedIndex = selIndex;
+}
+
+function onSelectionChange(event) {
+ // We don't offer detail management/discovery for email addresses
+ // that match an alias rule.
+ gViewButton.disabled =
+ !gListBox.selectedItems.length || gAliasRows[gListBox.selectedIndex];
+}
+
+function viewSelectedEmail() {
+ let selIndex = gListBox.selectedIndex;
+ if (gViewButton.disabled || selIndex == -1) {
+ return;
+ }
+ let email = gRowToEmail[selIndex];
+ window.openDialog(
+ "chrome://openpgp/content/ui/oneRecipientStatus.xhtml",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ {
+ email,
+ keys: gMapAddressToKeyObjs.get(email),
+ }
+ );
+ reloadAndReselect(selIndex);
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml
new file mode 100644
index 0000000000..fc0192a8a6
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml
@@ -0,0 +1,94 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/openPgpComposeStatus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<window
+ data-l10n-id="openpgp-compose-key-status-title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="width: 40em; height: 25em"
+ persist="width height"
+ lightweightthemes="true"
+ onload="onLoad();"
+>
+ <dialog id="composeKeyStatus" buttons="accept">
+ <script src="chrome://openpgp/content/ui/composeKeyStatus.js" />
+ <script>
+ <![CDATA[
+ function resizeColumns() {
+ let list = document.getElementById("infolist");
+ let cols = list.getElementsByTagName("treecol");
+ list.style.setProperty("--recipientWidth", cols[0].getBoundingClientRect().width + "px");
+ list.style.setProperty("--statusWidth", cols[1].getBoundingClientRect().width + "px");
+ }
+ addEventListener("load", resizeColumns, { once: true });
+ addEventListener("resize", resizeColumns);
+ ]]>
+ </script>
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/openpgp/composeKeyStatus.ftl"
+ />
+ </linkset>
+
+ <description data-l10n-id="openpgp-compose-key-status-intro-need-keys" />
+
+ <separator class="thin" />
+ <label
+ data-l10n-id="openpgp-compose-key-status-keys-heading"
+ control="infolist"
+ />
+
+ <richlistbox
+ id="infolist"
+ class="theme-listbox"
+ flex="1"
+ onselect="onSelectionChange(event);"
+ >
+ <treecols>
+ <treecol
+ id="recipientComposeKeyCol"
+ data-l10n-id="openpgp-compose-key-status-recipient"
+ />
+ <treecol
+ id="statusComposeKeyCol"
+ data-l10n-id="openpgp-compose-key-status-status"
+ />
+ </treecols>
+ </richlistbox>
+ <hbox pack="start">
+ <button
+ id="detailsButton"
+ disabled="true"
+ data-l10n-id="openpgp-compose-key-status-open-details"
+ oncommand="viewSelectedEmail();"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <vbox flex="1">
+ <html:span
+ class="tail-with-learn-more"
+ data-l10n-id="openpgp-compose-general-info-alias"
+ ></html:span>
+ <label
+ is="text-link"
+ id="openPgpAliasLearnMore"
+ href="https://support.mozilla.org/kb/openpgp-recipient-alias-configuration"
+ data-l10n-id="openpgp-compose-general-info-alias-learn-more"
+ />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js
new file mode 100644
index 0000000000..d575c111b7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js
@@ -0,0 +1,102 @@
+/* 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";
+
+// Dialog event listeners.
+window.addEventListener("dialogaccept", onAccept);
+window.addEventListener("load", init);
+
+var gUndecided = null;
+var gUnverified = null;
+
+async function init() {
+ let num = window.arguments[0].keys.length;
+ let label1 = document.getElementById("importInfo");
+ document.l10n.setAttributes(label1, "openpgp-pubkey-import-intro", {
+ num,
+ });
+ let label2 = document.getElementById("acceptInfo");
+ document.l10n.setAttributes(label2, "openpgp-pubkey-import-accept", {
+ num,
+ });
+
+ let l10nElements = [];
+ l10nElements.push(label1);
+ l10nElements.push(label2);
+
+ // TODO: This should be changed to use data-l10n-id in the .xhtml
+ // at a later time. We reuse strings on the 78 branch that don't have
+ // the .label definition in the .ftl file.
+
+ let [rUnd, rUnv] = await document.l10n.formatValues([
+ { id: "openpgp-key-undecided" },
+ { id: "openpgp-key-unverified" },
+ ]);
+
+ gUndecided = document.getElementById("acceptUndecided");
+ gUndecided.label = rUnd;
+ gUnverified = document.getElementById("acceptUnverified");
+ gUnverified.label = rUnv;
+
+ let keyList = document.getElementById("importKeyList");
+
+ for (let key of window.arguments[0].keys) {
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row");
+
+ let titleContainer = document.createXULElement("vbox");
+ let headerHBox = document.createXULElement("hbox");
+
+ let idSpan = document.createElement("span");
+ let idLabel = document.createXULElement("label");
+ idSpan.appendChild(idLabel);
+ idSpan.classList.add("openpgp-key-id");
+ headerHBox.appendChild(idSpan);
+
+ document.l10n.setAttributes(idLabel, "openpgp-pubkey-import-id", {
+ kid: "0x" + key.keyId,
+ });
+
+ let fprSpan = document.createElement("span");
+ let fprLabel = document.createXULElement("label");
+ fprSpan.appendChild(fprLabel);
+ fprSpan.classList.add("openpgp-key-fpr");
+ headerHBox.appendChild(fprSpan);
+
+ document.l10n.setAttributes(fprLabel, "openpgp-pubkey-import-fpr", {
+ fpr: key.fpr,
+ });
+
+ titleContainer.appendChild(headerHBox);
+
+ for (let uid of key.userIds) {
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.value = uid.userId;
+ titleContainer.appendChild(name);
+ }
+
+ container.appendChild(titleContainer);
+ keyList.appendChild(container);
+ }
+
+ await document.l10n.translateElements(l10nElements);
+ window.sizeToContent();
+ window.moveTo(
+ (screen.width - window.outerWidth) / 2,
+ (screen.height - window.outerHeight) / 2
+ );
+}
+
+function onAccept(event) {
+ window.arguments[0].confirmed = true;
+ if (gUndecided.selected) {
+ window.arguments[0].acceptance = "undecided";
+ } else if (gUnverified.selected) {
+ window.arguments[0].acceptance = "unverified";
+ } else {
+ throw new Error("internal error, no expected radio button was selected");
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml
new file mode 100644
index 0000000000..35935e0be7
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/confirmPubkeyImport.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <dialog
+ id="confirmPubkeyImportDialog"
+ data-l10n-id="pubkey-import-button"
+ data-l10n-attrs="buttonlabelaccept"
+ buttons="accept,cancel"
+ >
+ <script src="chrome://openpgp/content/ui/confirmPubkeyImport.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/openpgp/oneRecipientStatus.ftl"
+ />
+ </linkset>
+
+ <vbox>
+ <description id="importInfo" />
+
+ <vbox id="importKeyListContainer">
+ <vbox id="importKeyList" />
+ </vbox>
+
+ <separator />
+
+ <vbox id="acceptancePanel">
+ <description id="acceptInfo" />
+ <html:div>
+ <html:fieldset>
+ <radiogroup id="acceptanceRadio" class="indent">
+ <radio id="acceptUndecided" value="undecided" selected="true" />
+ <radio id="acceptUnverified" value="unverified" />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailCommon.js b/comm/mail/extensions/openpgp/content/ui/enigmailCommon.js
new file mode 100644
index 0000000000..ea02824856
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailCommon.js
@@ -0,0 +1,69 @@
+/*
+ * 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 { EnigmailCore } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/core.jsm"
+);
+var { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+
+var l10nCommon = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+var gEnigmailSvc;
+function GetEnigmailSvc() {
+ if (!gEnigmailSvc) {
+ gEnigmailSvc = EnigmailCore.getService(window);
+ }
+ return gEnigmailSvc;
+}
+
+async function EnigRevokeKey(keyObj, callbackFunc) {
+ var enigmailSvc = GetEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ if (keyObj.keyTrust == "r") {
+ Services.prompt.alert(
+ null,
+ document.title,
+ l10nCommon.formatValueSync("already-revoked")
+ );
+ return;
+ }
+
+ let promptFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
+
+ let confirm = Services.prompt.confirmEx(
+ window,
+ l10nCommon.formatValueSync("openpgp-key-revoke-title"),
+ l10nCommon.formatValueSync("revoke-key-question", {
+ identity: `0x${keyObj.keyId} - ${keyObj.userId}`,
+ }),
+ promptFlags,
+ l10nCommon.formatValueSync("key-man-button-revoke-key"),
+ null,
+ null,
+ null,
+ {}
+ );
+
+ if (confirm != 0) {
+ return;
+ }
+
+ await RNP.revokeKey(keyObj.fpr);
+ callbackFunc(true);
+
+ Services.prompt.alert(
+ null,
+ l10nCommon.formatValueSync("openpgp-key-revoke-success"),
+ l10nCommon.formatValueSync("after-revoke-info")
+ );
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js
new file mode 100644
index 0000000000..a0069ed3c2
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js
@@ -0,0 +1,172 @@
+/* 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 EnigmailWindows = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+).EnigmailWindows;
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var EnigmailDialog = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+).EnigmailDialog;
+var EnigmailData = ChromeUtils.import(
+ "chrome://openpgp/content/modules/data.jsm"
+).EnigmailData;
+
+async function onLoad() {
+ let dlg = document.getElementById("enigmailKeyImportInfo");
+
+ let keys = [];
+
+ if (window.screen.width > 500) {
+ dlg.setAttribute("maxwidth", window.screen.width - 150);
+ }
+
+ if (window.screen.height > 300) {
+ dlg.setAttribute("maxheight", window.screen.height - 100);
+ }
+
+ var keyList = window.arguments[0].keyList;
+
+ let onClickFunc = function (event) {
+ let keyId = event.target.getAttribute("keyid");
+ EnigmailWindows.openKeyDetails(window, keyId, false);
+ };
+
+ for (let keyId of keyList) {
+ if (keyId.search(/^0x/) === 0) {
+ keyId = keyId.substr(2).toUpperCase();
+ }
+ let keyObj = EnigmailKeyRing.getKeyById(keyId);
+ if (keyObj && keyObj.fpr) {
+ let keyGroupBox = buildKeyGroupBox(keyObj);
+ keyGroupBox
+ .getElementsByClassName("enigmailKeyImportDetails")[0]
+ .addEventListener("click", onClickFunc, true);
+ keys.push(keyGroupBox);
+ }
+ }
+
+ dlg.getButton("accept").focus();
+
+ if (keys.length) {
+ let keysInfoBox = document.getElementById("keyInfo"),
+ keyBox = document.createXULElement("vbox");
+
+ keyBox.classList.add("grid-three-column");
+ for (let key of keys) {
+ keyBox.appendChild(key);
+ }
+
+ keysInfoBox.appendChild(keyBox);
+ } else {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("import-info-no-keys")
+ );
+ setTimeout(window.close, 0);
+ return;
+ }
+
+ setTimeout(resizeDlg);
+ setTimeout(() => window.sizeToContent());
+}
+
+function buildKeyGroupBox(keyObj) {
+ let groupBox = document.createXULElement("vbox");
+ let userid = document.createXULElement("label");
+
+ groupBox.classList.add("enigmailGroupbox", "enigmailGroupboxMargin");
+ userid.setAttribute("value", keyObj.userId);
+ userid.setAttribute("class", "enigmailKeyImportUserId");
+
+ let infoBox = document.createElement("div");
+ let infoLabelH1 = document.createXULElement("label");
+ let infoLabelH2 = document.createXULElement("label");
+ let infoLabelB1 = document.createXULElement("label");
+ let infoLabelB2 = document.createXULElement("label");
+ let infoLabelB3 = document.createXULElement("label");
+
+ document.l10n.setAttributes(infoLabelH1, "import-info-bits");
+ document.l10n.setAttributes(infoLabelH2, "import-info-created");
+ infoLabelB1.setAttribute("value", keyObj.keySize);
+ infoLabelB2.setAttribute("value", keyObj.created);
+
+ infoLabelH1.classList.add("enigmailKeyImportHeader");
+ infoLabelH2.classList.add("enigmailKeyImportHeader");
+
+ infoBox.classList.add("grid-two-column-fr");
+ infoBox.appendChild(infoLabelH1);
+ infoBox.appendChild(infoLabelH2);
+ infoBox.appendChild(infoLabelB1);
+ infoBox.appendChild(infoLabelB2);
+
+ let fprBox = document.createXULElement("div");
+ let fprLabel = document.createXULElement("label");
+ document.l10n.setAttributes(fprLabel, "import-info-fpr");
+ fprLabel.setAttribute("class", "enigmailKeyImportHeader");
+ let gridTemplateColumns = "";
+ for (let i = 0; i < keyObj.fpr.length; i += 4) {
+ var label = document.createXULElement("label");
+ label.setAttribute("value", keyObj.fpr.substr(i, 4));
+ if (i < keyObj.fpr.length / 2) {
+ gridTemplateColumns += "auto ";
+ }
+ fprBox.appendChild(label);
+ }
+
+ fprBox.style.display = "inline-grid";
+ fprBox.style.gridTemplateColumns = gridTemplateColumns;
+
+ groupBox.appendChild(userid);
+ groupBox.appendChild(infoBox);
+ groupBox.appendChild(fprLabel);
+ groupBox.appendChild(fprBox);
+
+ document.l10n.setAttributes(infoLabelB3, "import-info-details");
+ infoLabelB3.setAttribute("keyid", keyObj.keyId);
+ infoLabelB3.setAttribute("class", "enigmailKeyImportDetails");
+ groupBox.appendChild(infoLabelB3);
+
+ return groupBox;
+}
+
+function resizeDlg() {
+ var txt = document.getElementById("keyInfo");
+ var box = document.getElementById("outerbox");
+
+ var deltaWidth = window.outerWidth - box.clientWidth;
+ var newWidth = txt.scrollWidth + deltaWidth + 20;
+
+ if (newWidth > window.screen.width - 50) {
+ newWidth = window.screen.width - 50;
+ }
+
+ txt.style["white-space"] = "pre-wrap";
+ window.resizeTo(newWidth, window.outerHeight);
+
+ var textHeight = txt.scrollHeight;
+ var boxHeight = box.clientHeight;
+ var deltaHeight = window.outerHeight - boxHeight;
+
+ var newHeight = textHeight + deltaHeight + 25;
+
+ if (newHeight > window.screen.height - 100) {
+ newHeight = window.screen.height - 100;
+ }
+
+ window.resizeTo(newWidth, newHeight);
+}
+
+function dlgClose(buttonNumber) {
+ window.arguments[1].value = buttonNumber;
+ window.close();
+}
+
+document.addEventListener("dialogaccept", function (event) {
+ dlgClose(0);
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml
new file mode 100644
index 0000000000..c5c8436398
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.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/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/enigmail.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/shared/grid-layout.css"?>
+
+<!DOCTYPE window [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD; ]>
+
+<window
+ data-l10n-id="import-info-title"
+ onload="onLoad();"
+ style="min-width: 100px; max-width: 750px"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <dialog id="enigmailKeyImportInfo" buttons="accept">
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailKeyImportInfo.js"
+ />
+ <linkset>
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+
+ <vbox align="center" flex="1" style="overflow: auto" id="outerbox">
+ <hbox align="center" flex="1">
+ <description
+ flex="1"
+ id="keyInfo"
+ class="plain"
+ style="white-space: pre"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js
new file mode 100644
index 0000000000..e72cf8e6bc
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js
@@ -0,0 +1,1442 @@
+/*
+ * 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";
+
+/* global GetEnigmailSvc, EnigRevokeKey */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var { EnigmailCore } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/core.jsm"
+);
+var { EnigmailStreams } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/streams.jsm"
+);
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { EnigmailKeyServer } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyserver.jsm"
+);
+var { EnigmailWks } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/webKey.jsm"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { KeyLookupHelper } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyLookupHelper.jsm"
+);
+var { EnigmailTrust } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/trust.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+var { EnigmailLog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/log.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKeyserverURIs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyserverUris.jsm"
+);
+
+const ENIG_KEY_EXPIRED = "e";
+const ENIG_KEY_REVOKED = "r";
+const ENIG_KEY_INVALID = "i";
+const ENIG_KEY_DISABLED = "d";
+const ENIG_KEY_NOT_VALID =
+ ENIG_KEY_EXPIRED + ENIG_KEY_REVOKED + ENIG_KEY_INVALID + ENIG_KEY_DISABLED;
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"], true);
+
+const INPUT = 0;
+const RESULT = 1;
+
+var gUserList;
+var gKeyList;
+var gEnigLastSelectedKeys = null;
+var gKeySortList = null;
+var gSearchInput = null;
+var gTreeChildren = null;
+var gShowInvalidKeys = null;
+var gShowOthersKeys = null;
+var gTimeoutId = null;
+
+function enigmailKeyManagerLoad() {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: enigmailKeyManagerLoad\n");
+
+ // Close the key manager if GnuPG is not available
+ if (!EnigmailCore.getService(window)) {
+ window.close();
+ return;
+ }
+
+ gUserList = document.getElementById("pgpKeyList");
+ gSearchInput = document.getElementById("filterKey");
+ gShowInvalidKeys = document.getElementById("showInvalidKeys");
+ gShowOthersKeys = document.getElementById("showOthersKeys");
+
+ window.addEventListener("reload-keycache", reloadKeys);
+ gSearchInput.addEventListener("keydown", event => {
+ switch (event.key) {
+ case "Escape":
+ event.target.value = "";
+ // fall through
+ case "Enter":
+ if (gTimeoutId) {
+ clearTimeout(gTimeoutId);
+ gTimeoutId = null;
+ }
+ gKeyListView.applyFilter(0);
+ event.preventDefault();
+ break;
+ default:
+ gTimeoutId = setTimeout(() => {
+ gKeyListView.applyFilter(0);
+ }, 200);
+ break;
+ }
+ });
+
+ gUserList.addEventListener("click", onListClick, true);
+ document.getElementById("statusText").value = l10n.formatValueSync(
+ "key-man-loading-keys"
+ );
+ document.getElementById("progressBar").style.visibility = "visible";
+ setTimeout(loadkeyList, 100);
+
+ gUserList.view = gKeyListView;
+ gSearchInput.focus();
+
+ // Dialog event listeners.
+ document.addEventListener("dialogaccept", onDialogAccept);
+ document.addEventListener("dialogcancel", onDialogClose);
+}
+
+function onDialogAccept() {
+ if (window.arguments[0].okCallback) {
+ window.arguments[0].okCallback();
+ }
+ window.close();
+}
+
+function onDialogClose() {
+ if (window.arguments[0].cancelCallback) {
+ window.arguments[0].cancelCallback();
+ }
+ window.close();
+}
+
+function loadkeyList() {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: loadkeyList\n");
+
+ sortTree();
+ gKeyListView.applyFilter(0);
+ document.getElementById("pleaseWait").hidePopup();
+ document.getElementById("statusText").value = "";
+ document.getElementById("progressBar").style.visibility = "collapse";
+}
+
+function clearKeyCache() {
+ EnigmailKeyRing.clearCache();
+ refreshKeys();
+}
+
+function refreshKeys() {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: refreshKeys\n");
+ var keyList = getSelectedKeys();
+ gEnigLastSelectedKeys = [];
+ for (var i = 0; i < keyList.length; i++) {
+ gEnigLastSelectedKeys[keyList[i]] = 1;
+ }
+
+ buildKeyList(true);
+}
+
+function reloadKeys() {
+ let i = 0;
+ let c = Components.stack;
+
+ while (c) {
+ if (c.name == "reloadKeys") {
+ i++;
+ }
+ c = c.caller;
+ }
+
+ // detect recursion and don't continue if too much recursion
+ // this can happen if the key list is empty
+ if (i < 4) {
+ buildKeyList(true);
+ }
+}
+
+function buildKeyList(refresh) {
+ EnigmailLog.DEBUG("enigmailKeyManager.js: buildKeyList\n");
+
+ var keyListObj = {};
+
+ if (refresh) {
+ EnigmailKeyRing.clearCache();
+ }
+
+ keyListObj = EnigmailKeyRing.getAllKeys(
+ window,
+ getSortColumn(),
+ getSortDirection()
+ );
+
+ if (!keyListObj.keySortList) {
+ return;
+ }
+
+ gKeyList = keyListObj.keyList;
+ gKeySortList = keyListObj.keySortList;
+
+ gKeyListView.keysRefreshed();
+}
+
+function getSelectedKeys() {
+ let selList = [];
+ let rangeCount = gUserList.view.selection.getRangeCount();
+ for (let i = 0; i < rangeCount; i++) {
+ let start = {};
+ let end = {};
+ gUserList.view.selection.getRangeAt(i, start, end);
+ for (let c = start.value; c <= end.value; c++) {
+ try {
+ //selList.push(gUserList.view.getItemAtIndex(c).getAttribute("keyNum"));
+ selList.push(gKeyListView.getFilteredRow(c).keyNum);
+ } catch (ex) {
+ return [];
+ }
+ }
+ }
+ return selList;
+}
+
+function getSelectedKeyIds() {
+ let keyList = getSelectedKeys();
+
+ let a = [];
+ for (let i in keyList) {
+ a.push(gKeyList[keyList[i]].keyId);
+ }
+
+ return a;
+}
+
+function enigmailKeyMenu() {
+ var keyList = getSelectedKeys();
+
+ let haveSecretForAll;
+ if (keyList.length == 0) {
+ haveSecretForAll = false;
+ } else {
+ haveSecretForAll = true;
+ for (let key of keyList) {
+ if (!gKeyList[key].secretAvailable) {
+ haveSecretForAll = false;
+ break;
+ }
+ }
+ }
+
+ let singleSecretSelected = keyList.length == 1 && haveSecretForAll;
+
+ // Make the selected key count available to translations.
+ for (let el of document.querySelectorAll(".enigmail-bulk-key-operation")) {
+ el.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ count: keyList.length })
+ );
+ }
+
+ document.getElementById("backupSecretKey").disabled = !haveSecretForAll;
+ document.getElementById("uploadToServer").disabled = !singleSecretSelected;
+
+ document.getElementById("revokeKey").disabled =
+ keyList.length != 1 || !gKeyList[keyList[0]].secretAvailable;
+ document.getElementById("ctxRevokeKey").hidden =
+ keyList.length != 1 || !gKeyList[keyList[0]].secretAvailable;
+
+ document.getElementById("importFromClipbrd").disabled =
+ !Services.clipboard.hasDataMatchingFlavors(
+ ["text/plain"],
+ Ci.nsIClipboard.kGlobalClipboard
+ );
+
+ for (let item of document.querySelectorAll(
+ ".requires-single-key-selection"
+ )) {
+ item.disabled = keyList.length != 1;
+ }
+
+ for (let item of document.querySelectorAll(".requires-key-selection")) {
+ item.disabled = keyList.length == 0;
+ }
+
+ // Disable the "Generate key" menu item if no mail account is available.
+ document
+ .getElementById("genKey")
+ .setAttribute("disabled", MailServices.accounts.defaultAccount == null);
+
+ // Disable the context menu if no keys are selected.
+ return keyList.length > 0;
+}
+
+function onListClick(event) {
+ if (event.detail > 2) {
+ return;
+ }
+
+ if (event.type === "click") {
+ // Mouse event
+ let { col } = gUserList.getCellAt(event.clientX, event.clientY);
+
+ if (!col) {
+ // not clicked on a valid column (e.g. scrollbar)
+ return;
+ }
+ }
+
+ if (event.detail != 2) {
+ return;
+ }
+
+ // do not propagate double clicks
+ event.stopPropagation();
+ enigmailKeyDetails();
+}
+
+function enigmailSelectAllKeys() {
+ gUserList.view.selection.selectAll();
+}
+
+/**
+ * Open the Key Properties subdialog.
+ *
+ * @param {string|null} keyId - Optional ID of the selected OpenPGP Key.
+ */
+function enigmailKeyDetails(keyId = null) {
+ if (!keyId) {
+ let keyList = getSelectedKeys();
+ // Interrupt if we don't have a single selected key nor a key was passed.
+ if (keyList.length != 1) {
+ return;
+ }
+ keyId = gKeyList[keyList[0]].keyId;
+ }
+
+ if (EnigmailWindows.openKeyDetails(window, keyId, false)) {
+ refreshKeys();
+ }
+}
+
+async function enigmailDeleteKey() {
+ var keyList = getSelectedKeys();
+ var deleteSecret = false;
+
+ if (keyList.length == 1) {
+ // one key selected
+ var userId = gKeyList[keyList[0]].userId;
+ if (gKeyList[keyList[0]].secretAvailable) {
+ if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-secret-key", {
+ userId,
+ }),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ deleteSecret = true;
+ } else if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-pub-key", {
+ userId,
+ }),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ } else {
+ // several keys selected
+ for (var i = 0; i < keyList.length; i++) {
+ if (gKeyList[keyList[i]].secretAvailable) {
+ deleteSecret = true;
+ }
+ }
+
+ if (deleteSecret) {
+ if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-mix"),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ } else if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("delete-selected-pub-key"),
+ l10n.formatValueSync("dlg-button-delete")
+ )
+ ) {
+ return;
+ }
+ }
+
+ const cApi = EnigmailCryptoAPI();
+ for (let j in keyList) {
+ let fpr = gKeyList[keyList[j]].fpr;
+ await cApi.deleteKey(fpr, deleteSecret);
+ await PgpSqliteDb2.deleteAcceptance(fpr);
+ }
+ clearKeyCache();
+ gUserList.view.selection.clearSelection();
+}
+
+async function enigCreateKeyMsg() {
+ var keyList = getSelectedKeyIds();
+ var tmpFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tmpFile.append("key.asc");
+ tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ // save file
+ var exitCodeObj = {};
+ var errorMsgObj = {};
+
+ var keyIdArray = [];
+ for (let id of keyList) {
+ keyIdArray.push("0x" + id);
+ }
+
+ await EnigmailKeyRing.extractPublicKeys(
+ keyIdArray, // full
+ null,
+ null,
+ tmpFile,
+ exitCodeObj,
+ errorMsgObj
+ );
+ if (exitCodeObj.value !== 0) {
+ EnigmailDialog.alert(window, errorMsgObj.value);
+ return;
+ }
+
+ // create attachment
+ var ioServ = Services.io;
+ var tmpFileURI = ioServ.newFileURI(tmpFile);
+ var keyAttachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ keyAttachment.url = tmpFileURI.spec;
+ if (keyList.length == 1) {
+ keyAttachment.name = "0x" + keyList[0] + ".asc";
+ } else {
+ keyAttachment.name = "pgpkeys.asc";
+ }
+ keyAttachment.temporary = true;
+ keyAttachment.contentType = "application/pgp-keys";
+
+ // create Msg
+ var msgCompFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ msgCompFields.addAttachment(keyAttachment);
+
+ var msgCompSvc = Cc["@mozilla.org/messengercompose;1"].getService(
+ Ci.nsIMsgComposeService
+ );
+
+ var msgCompParam = Cc[
+ "@mozilla.org/messengercompose/composeparams;1"
+ ].createInstance(Ci.nsIMsgComposeParams);
+ msgCompParam.composeFields = msgCompFields;
+ msgCompParam.identity = EnigmailFuncs.getDefaultIdentity();
+ msgCompParam.type = Ci.nsIMsgCompType.New;
+ msgCompParam.format = Ci.nsIMsgCompFormat.Default;
+ msgCompParam.originalMsgURI = "";
+ msgCompSvc.OpenComposeWindowWithParams("", msgCompParam);
+}
+
+async function enigmailRevokeKey() {
+ var keyList = getSelectedKeys();
+ let keyInfo = gKeyList[keyList[0]];
+ EnigRevokeKey(keyInfo, function (success) {
+ if (success) {
+ refreshKeys();
+ }
+ });
+}
+
+async function enigmailExportKeys(which) {
+ let exportSecretKey = which == "secret";
+ var keyList = getSelectedKeys();
+ var defaultFileName;
+
+ if (keyList.length == 1) {
+ let extension = exportSecretKey ? "secret.asc" : "public.asc";
+ defaultFileName = gKeyList[keyList[0]].userId.replace(/[<>]/g, "");
+ defaultFileName =
+ defaultFileName +
+ "-" +
+ `(0x${gKeyList[keyList[0]].keyId})` +
+ "-" +
+ extension;
+ } else {
+ let id = exportSecretKey
+ ? "default-pub-sec-key-filename"
+ : "default-pub-key-filename";
+ defaultFileName = l10n.formatValueSync(id) + ".asc";
+ }
+
+ if (exportSecretKey) {
+ var fprArray = [];
+ for (let id of keyList) {
+ fprArray.push(gKeyList[id].fpr);
+ }
+ EnigmailKeyRing.backupSecretKeysInteractive(
+ window,
+ defaultFileName,
+ fprArray
+ );
+ } else {
+ let keyList2 = getSelectedKeyIds();
+ var keyIdArray = [];
+ for (let id of keyList2) {
+ keyIdArray.push("0x" + id);
+ }
+ await EnigmailKeyRing.exportPublicKeysInteractive(
+ window,
+ defaultFileName,
+ keyIdArray
+ );
+ }
+}
+
+async function enigmailImportFromClipbrd() {
+ if (
+ !EnigmailDialog.confirmDlg(
+ window,
+ l10n.formatValueSync("import-from-clip"),
+ l10n.formatValueSync("key-man-button-import")
+ )
+ ) {
+ return;
+ }
+
+ let cBoardContent = await navigator.clipboard.readText();
+ var errorMsgObj = {};
+ var preview = await EnigmailKey.getKeyListFromKeyBlock(
+ cBoardContent,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ // should we allow importing secret keys?
+ if (preview && preview.length > 0) {
+ let confirmImport = false;
+ let outParam = {};
+ confirmImport = EnigmailDialog.confirmPubkeyImport(
+ window,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ // import
+ EnigmailKeyRing.importKey(
+ window,
+ false,
+ cBoardContent,
+ false,
+ "",
+ errorMsgObj,
+ null,
+ false,
+ [],
+ true,
+ outParam.acceptance
+ );
+ var keyList = preview.map(function (a) {
+ return a.id;
+ });
+ EnigmailDialog.keyImportDlg(window, keyList);
+ refreshKeys();
+ }
+ } else {
+ document.l10n.formatValue("preview-failed").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ }
+}
+
+/**
+ * Places the fingerprint of each selected key onto the keyboard.
+ */
+async function copyOpenPGPFingerPrints() {
+ let fprs = getSelectedKeys()
+ .map(idx => gKeyList[idx].fpr)
+ .join("\n");
+ return navigator.clipboard.writeText(fprs);
+}
+
+/**
+ * Places the key id of each key selected onto the clipboard.
+ */
+async function copyOpenPGPKeyIds() {
+ let ids = getSelectedKeyIds();
+ return navigator.clipboard.writeText(ids.map(id => `0x${id}`).join("\n"));
+}
+
+async function enigmailCopyToClipbrd() {
+ var keyList = getSelectedKeyIds();
+ if (keyList.length === 0) {
+ document.l10n.formatValue("no-key-selected").then(value => {
+ EnigmailDialog.info(window, value);
+ });
+ return;
+ }
+ var exitCodeObj = {};
+ var errorMsgObj = {};
+
+ var keyIdArray = [];
+ for (let id of keyList) {
+ keyIdArray.push("0x" + id);
+ }
+
+ let keyData = await EnigmailKeyRing.extractPublicKeys(
+ keyIdArray, // full
+ null,
+ null,
+ null,
+ exitCodeObj,
+ errorMsgObj
+ );
+ if (exitCodeObj.value !== 0) {
+ l10n.formatValue("copy-to-clipbrd-failed").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ return;
+ }
+ navigator.clipboard
+ .writeText(keyData)
+ .then(() => {
+ l10n.formatValue("copy-to-clipbrd-ok").then(value => {
+ EnigmailDialog.info(window, value);
+ });
+ })
+ .catch(err => {
+ l10n.formatValue("copy-to-clipbrd-failed").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ });
+}
+
+async function enigmailSearchKey() {
+ var result = {
+ value: "",
+ };
+ if (
+ !Services.prompt.prompt(
+ window,
+ l10n.formatValueSync("enig-prompt"),
+ l10n.formatValueSync("openpgp-key-man-discover-prompt"),
+ result,
+ "",
+ {}
+ )
+ ) {
+ return;
+ }
+
+ result.value = result.value.trim();
+
+ let imported = false;
+ if (EnigmailFuncs.stringLooksLikeEmailAddress(result.value)) {
+ imported = await KeyLookupHelper.lookupAndImportByEmail(
+ "interactive-import",
+ window,
+ result.value,
+ true
+ );
+ } else {
+ imported = await KeyLookupHelper.lookupAndImportByKeyID(
+ "interactive-import",
+ window,
+ result.value,
+ true
+ );
+ }
+
+ if (imported) {
+ refreshKeys();
+ }
+}
+
+async function enigmailUploadKey() {
+ // Always upload to the first configured keyserver with a supported protocol.
+ let selKeyList = getSelectedKeys();
+ if (selKeyList.length != 1) {
+ return;
+ }
+
+ let keyId = gKeyList[selKeyList[0]].keyId;
+ let ks = EnigmailKeyserverURIs.getUploadKeyServer();
+
+ let ok = await EnigmailKeyServer.upload(keyId, ks);
+ document.l10n
+ .formatValue(ok ? "openpgp-key-publish-ok" : "openpgp-key-publish-fail", {
+ keyserver: ks,
+ })
+ .then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+}
+
+/*
+function enigmailUploadToWkd() {
+ let selKeyList = getSelectedKeys();
+ let keyList = [];
+ for (let i = 0; i < selKeyList.length; i++) {
+ keyList.push(gKeyList[selKeyList[i]]);
+ }
+
+ EnigmailWks.wksUpload(keyList, window)
+ .then(result => {
+ if (result.length > 0) {
+ EnigmailDialog.info(window, "Key(s) sent successfully");
+ } else if (keyList.length === 1) {
+ EnigmailDialog.alert(
+ window,
+ "Sending of keys failed" +
+ "\n\n" +
+ "The key %S does not have a WKS identity.".replace("%S", keyList[0].userId)
+ );
+ } else {
+ EnigmailDialog.alert(
+ window,
+ "The upload was not successful - your provider does not seem to support WKS."
+ );
+ }
+ })
+ .catch(error => {
+ EnigmailDialog.alert(
+ "Sending of keys failed" + "\n" + error
+ );
+ });
+}
+*/
+
+function enigmailImportKeysFromUrl() {
+ var result = {
+ value: "",
+ };
+ if (
+ !Services.prompt.prompt(
+ window,
+ l10n.formatValueSync("enig-prompt"),
+ l10n.formatValueSync("import-from-url"),
+ result,
+ "",
+ {}
+ )
+ ) {
+ return;
+ }
+ var p = new Promise(function (resolve, reject) {
+ var cbFunc = async function (data) {
+ EnigmailLog.DEBUG("enigmailImportKeysFromUrl: _cbFunc()\n");
+ var errorMsgObj = {};
+
+ var preview = await EnigmailKey.getKeyListFromKeyBlock(
+ data,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ // should we allow importing secret keys?
+ if (preview && preview.length > 0) {
+ let confirmImport = false;
+ let outParam = {};
+ confirmImport = EnigmailDialog.confirmPubkeyImport(
+ window,
+ preview,
+ outParam
+ );
+ if (confirmImport) {
+ EnigmailKeyRing.importKey(
+ window,
+ false,
+ data,
+ false,
+ "",
+ errorMsgObj,
+ null,
+ false,
+ [],
+ true,
+ outParam.acceptance
+ );
+ errorMsgObj.preview = preview;
+ resolve(errorMsgObj);
+ }
+ } else {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("preview-failed")
+ );
+ }
+ };
+
+ try {
+ var bufferListener = EnigmailStreams.newStringStreamListener(cbFunc);
+ var msgUri = Services.io.newURI(result.value.trim());
+
+ var channel = EnigmailStreams.createChannel(msgUri);
+ channel.asyncOpen(bufferListener, msgUri);
+ } catch (ex) {
+ var err = {
+ value: ex,
+ };
+ reject(err);
+ }
+ });
+
+ p.then(function (errorMsgObj) {
+ var keyList = errorMsgObj.preview.map(function (a) {
+ return a.id;
+ });
+ EnigmailDialog.keyImportDlg(window, keyList);
+ refreshKeys();
+ }).catch(async function (reason) {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("general-error", {
+ reason: reason.value,
+ })
+ );
+ });
+}
+
+function initiateAcKeyTransfer() {
+ EnigmailWindows.inititateAcSetupMessage();
+}
+
+//
+// ----- key filtering functionality -----
+//
+
+function determineHiddenKeys(keyObj, showInvalidKeys, showOthersKeys) {
+ var show = true;
+
+ const INVALID_KEYS = "ierdD";
+
+ if (
+ !showInvalidKeys &&
+ INVALID_KEYS.includes(EnigmailTrust.getTrustCode(keyObj))
+ ) {
+ show = false;
+ }
+
+ if (!showOthersKeys && !keyObj.secretAvailable) {
+ show = false;
+ }
+
+ return show;
+}
+
+function getSortDirection() {
+ return gUserList.getAttribute("sortDirection") == "ascending" ? 1 : -1;
+}
+
+function sortTree(column) {
+ var columnName;
+ var order = getSortDirection();
+
+ //if the column is passed and it's already sorted by that column, reverse sort
+ if (column) {
+ columnName = column.id;
+ if (gUserList.getAttribute("sortResource") == columnName) {
+ order *= -1;
+ } else {
+ document
+ .getElementById(gUserList.getAttribute("sortResource"))
+ .removeAttribute("sortDirection");
+ order = 1;
+ }
+ } else {
+ columnName = gUserList.getAttribute("sortResource");
+ }
+ gUserList.setAttribute(
+ "sortDirection",
+ order == 1 ? "ascending" : "descending"
+ );
+ let col = document.getElementById(columnName);
+ if (col) {
+ col.setAttribute("sortDirection", order == 1 ? "ascending" : "descending");
+ gUserList.setAttribute("sortResource", columnName);
+ } else {
+ gUserList.setAttribute("sortResource", "enigUserNameCol");
+ }
+ buildKeyList(false);
+}
+
+function getSortColumn() {
+ switch (gUserList.getAttribute("sortResource")) {
+ case "enigUserNameCol":
+ return "userid";
+ case "keyCol":
+ return "keyid";
+ case "createdCol":
+ return "created";
+ case "expCol":
+ return "expiry";
+ case "fprCol":
+ return "fpr";
+ default:
+ return "?";
+ }
+}
+
+/**
+ * Open the OpenPGP Key Wizard to generate a new key or import secret keys.
+ *
+ * @param {boolean} isImport - If the keyWizard should automatically switch to
+ * the import or create screen as requested by the user.
+ */
+function openKeyWizard(isImport = false) {
+ let args = {
+ gSubDialog: null,
+ cancelCallback: clearKeyCache,
+ okCallback: clearKeyCache,
+ okImportCallback: clearKeyCache,
+ okExternalCallback: clearKeyCache,
+ keyDetailsDialog: enigmailKeyDetails,
+ isCreate: !isImport,
+ isImport,
+ };
+
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://openpgp/content/ui/keyWizard.xhtml",
+ "enigmail:KeyWizard",
+ "dialog,modal,centerscreen,resizable",
+ args
+ );
+}
+
+/***************************** TreeView for user list ***********************************/
+/**
+ * gKeyListView implements the nsITreeView interface for the displayed list.
+ *
+ * For speed reasons, we use two lists:
+ * - keyViewList: contains the full list of pointers to all keys and rows that are
+ * potentially displayed ordered according to the sort column
+ * - keyFilterList: contains the indexes to keyViewList of the keys that are displayed
+ * according to the current filter criteria.
+ */
+var gKeyListView = {
+ keyViewList: [],
+ keyFilterList: [],
+
+ //// nsITreeView implementation
+
+ rowCount: 0,
+ selection: null,
+
+ canDrop(index, orientation, dataTransfer) {
+ return false;
+ },
+
+ cycleCell(row, col) {},
+ cycleHeader(col) {},
+ drop(row, orientation, dataTransfer) {},
+
+ getCellProperties(row, col) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return "";
+ }
+
+ let keyObj = gKeyList[r.keyNum];
+ if (!keyObj) {
+ return "";
+ }
+
+ let keyTrustStyle = "";
+
+ switch (r.rowType) {
+ case "key":
+ case "uid":
+ switch (keyObj.keyTrust) {
+ case "q":
+ keyTrustStyle = "enigmail_keyValid_unknown";
+ break;
+ case "r":
+ keyTrustStyle = "enigmail_keyValid_revoked";
+ break;
+ case "e":
+ keyTrustStyle = "enigmail_keyValid_expired";
+ break;
+ case "n":
+ keyTrustStyle = "enigmail_keyTrust_untrusted";
+ break;
+ case "m":
+ keyTrustStyle = "enigmail_keyTrust_marginal";
+ break;
+ case "f":
+ keyTrustStyle = "enigmail_keyTrust_full";
+ break;
+ case "u":
+ keyTrustStyle = "enigmail_keyTrust_ultimate";
+ break;
+ case "-":
+ keyTrustStyle = "enigmail_keyTrust_unknown";
+ break;
+ default:
+ keyTrustStyle = "enigmail_keyTrust_unknown";
+ break;
+ }
+
+ if (
+ keyObj.keyTrust.length > 0 &&
+ ENIG_KEY_NOT_VALID.includes(keyObj.keyTrust.charAt(0))
+ ) {
+ keyTrustStyle += " enigKeyInactive";
+ }
+
+ if (r.rowType === "key" && keyObj.secretAvailable) {
+ keyTrustStyle += " enigmailOwnKey";
+ }
+ break;
+ }
+
+ return keyTrustStyle;
+ },
+
+ getCellText(row, col) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return "";
+ }
+ let keyObj = gKeyList[r.keyNum];
+ if (!keyObj) {
+ return "???";
+ }
+
+ switch (r.rowType) {
+ case "key":
+ switch (col.id) {
+ case "enigUserNameCol":
+ return keyObj.userId;
+ case "keyCol":
+ return `0x${keyObj.keyId}`;
+ case "createdCol":
+ return keyObj.created;
+ case "expCol":
+ return keyObj.effectiveExpiry;
+ case "fprCol":
+ return keyObj.fprFormatted;
+ }
+ break;
+ case "uid":
+ switch (col.id) {
+ case "enigUserNameCol":
+ return keyObj.userIds[r.uidNum].userId;
+ }
+ break;
+ }
+
+ return "";
+ },
+ getCellValue(row, col) {
+ return "";
+ },
+ getColumnProperties(col) {
+ return "";
+ },
+
+ getImageSrc(row, col) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return null;
+ }
+ //let keyObj = gKeyList[r.keyNum];
+
+ return null;
+ },
+
+ /**
+ * indentation level for rows
+ */
+ getLevel(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return 0;
+ }
+
+ switch (r.rowType) {
+ case "key":
+ return 0;
+ case "uid":
+ return 1;
+ }
+
+ return 0;
+ },
+
+ getParentIndex(idx) {
+ return -1;
+ },
+ getProgressMode(row, col) {},
+
+ getRowProperties(row) {
+ return "";
+ },
+ hasNextSibling(rowIndex, afterIndex) {
+ return false;
+ },
+ isContainer(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return false;
+ }
+ switch (r.rowType) {
+ case "key":
+ return true;
+ }
+
+ return false;
+ },
+ isContainerEmpty(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return true;
+ }
+ switch (r.rowType) {
+ case "key":
+ return !r.hasSubUID;
+ }
+ return true;
+ },
+ isContainerOpen(row) {
+ return this.getFilteredRow(row).isOpen;
+ },
+ isEditable(row, col) {
+ return false;
+ },
+ isSelectable(row, col) {
+ return true;
+ },
+ isSeparator(index) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ performAction(action) {},
+ performActionOnCell(action, row, col) {},
+ performActionOnRow(action, row) {},
+ selectionChanged() {},
+ // void setCellText(in long row, in nsITreeColumn col, in AString value);
+ // void setCellValue(in long row, in nsITreeColumn col, in AString value);
+ setTree(treebox) {
+ this.treebox = treebox;
+ },
+
+ toggleOpenState(row) {
+ let r = this.getFilteredRow(row);
+ if (!r) {
+ return;
+ }
+ let realRow = this.keyFilterList[row];
+ switch (r.rowType) {
+ case "key":
+ if (r.isOpen) {
+ let i = 0;
+ while (
+ this.getFilteredRow(row + 1 + i) &&
+ this.getFilteredRow(row + 1 + i).keyNum === r.keyNum
+ ) {
+ ++i;
+ }
+
+ this.keyViewList.splice(realRow + 1, i);
+ r.isOpen = false;
+ this.applyFilter(row);
+ } else {
+ this.appendUids("uid", r.keyNum, realRow, this.keyViewList[row]);
+
+ r.isOpen = true;
+ this.applyFilter(row);
+ }
+ break;
+ }
+ },
+
+ /**
+ * add UIDs for a given key to key view
+ *
+ * @param uidType: String - one of uid (user ID), uat (photo)
+ * @param keyNum: Number - index of key in gKeyList
+ * @param realRow: Number - index of row in keyViewList (i.e. without filter)
+ *
+ * @returns Number: number of UIDs added
+ */
+ appendUids(uidType, keyNum, realRow, parentRow) {
+ let keyObj = gKeyList[keyNum];
+ let uidAdded = 0;
+
+ for (let i = 0; i < keyObj.userIds.length; i++) {
+ if (keyObj.userIds[i].type === uidType) {
+ if (keyObj.userIds[i].userId == keyObj.userId) {
+ continue;
+ }
+ ++uidAdded;
+ this.keyViewList.splice(realRow + uidAdded, 0, {
+ rowType: uidType,
+ keyNum,
+ parent: parentRow,
+ uidNum: i,
+ });
+ }
+ }
+
+ return uidAdded;
+ },
+
+ /**
+ * Reload key list entirely
+ */
+ keysRefreshed() {
+ this.keyViewList = [];
+ this.keyFilterList = [];
+ for (let i = 0; i < gKeySortList.length; i++) {
+ this.keyViewList.push({
+ row: i,
+ rowType: "key",
+ fpr: gKeySortList[i].fpr,
+ keyNum: gKeySortList[i].keyNum,
+ isOpen: false,
+ hasSubUID: gKeyList[gKeySortList[i].keyNum].userIds.length > 1,
+ });
+ }
+
+ this.applyFilter(0);
+ let oldRowCount = this.rowCount;
+ this.rowCount = this.keyViewList.length;
+ gUserList.rowCountChanged(0, this.rowCount - oldRowCount);
+ },
+
+ /**
+ * If no search term is entered, decide which keys to display
+ *
+ * @returns array of keyNums (= display some keys) or null (= display ALL keys)
+ */
+ showOrHideAllKeys() {
+ var showInvalidKeys = gShowInvalidKeys.getAttribute("checked") == "true";
+ var showOthersKeys = gShowOthersKeys.getAttribute("checked") == "true";
+
+ document.getElementById("nothingFound").hidePopup();
+
+ if (showInvalidKeys && showOthersKeys) {
+ return null;
+ }
+
+ let keyShowList = [];
+ for (let i = 0; i < gKeyList.length; i++) {
+ if (determineHiddenKeys(gKeyList[i], showInvalidKeys, showOthersKeys)) {
+ keyShowList.push(i);
+ }
+ }
+
+ return keyShowList;
+ },
+
+ /**
+ * Search for keys that match filter criteria
+ *
+ * @returns array of keyNums (= display some keys) or null (= display ALL keys)
+ */
+ getFilteredKeys() {
+ let searchTxt = gSearchInput.value;
+
+ if (!searchTxt || searchTxt.length === 0) {
+ return this.showOrHideAllKeys();
+ }
+
+ if (!gKeyList) {
+ return [];
+ }
+ let showInvalidKeys = gShowInvalidKeys.getAttribute("checked") == "true";
+ let showOthersKeys = gShowOthersKeys.getAttribute("checked") == "true";
+
+ // skip leading 0x in case we search for a key:
+ if (searchTxt.length > 2 && searchTxt.substr(0, 2).toLowerCase() == "0x") {
+ searchTxt = searchTxt.substr(2);
+ }
+
+ searchTxt = searchTxt.toLowerCase();
+ searchTxt = searchTxt.replace(/^(\s*)(.*)/, "$2").replace(/\s+$/, ""); // trim spaces
+
+ // check if we search for a full fingerprint (with optional spaces every 4 letters)
+ var fpr = null;
+ if (searchTxt.length == 49) {
+ // possible fingerprint with spaces?
+ if (
+ searchTxt.search(/^[0-9a-f ]*$/) >= 0 &&
+ searchTxt[4] == " " &&
+ searchTxt[9] == " " &&
+ searchTxt[14] == " " &&
+ searchTxt[19] == " " &&
+ searchTxt[24] == " " &&
+ searchTxt[29] == " " &&
+ searchTxt[34] == " " &&
+ searchTxt[39] == " " &&
+ searchTxt[44] == " "
+ ) {
+ fpr = searchTxt.replace(/ /g, "");
+ }
+ } else if (searchTxt.length == 40) {
+ // possible fingerprint without spaces
+ if (searchTxt.search(/^[0-9a-f ]*$/) >= 0) {
+ fpr = searchTxt;
+ }
+ }
+
+ let keyShowList = [];
+
+ for (let i = 0; i < gKeyList.length; i++) {
+ let keyObj = gKeyList[i];
+ let uid = keyObj.userId;
+ let showKey = false;
+
+ // does a user ID (partially) match?
+ for (let idx = 0; idx < keyObj.userIds.length; idx++) {
+ uid = keyObj.userIds[idx].userId;
+ if (uid.toLowerCase().includes(searchTxt)) {
+ showKey = true;
+ }
+ }
+
+ // does the full fingerprint (without spaces) match?
+ // - no partial match check because this is special for the collapsed spaces inside the fingerprint
+ if (showKey === false && fpr && keyObj.fpr.toLowerCase() == fpr) {
+ showKey = true;
+ }
+ // does the fingerprint (partially) match?
+ if (showKey === false && keyObj.fpr.toLowerCase().includes(searchTxt)) {
+ showKey = true;
+ }
+ // does a sub key of (partially) match?
+ if (showKey === false) {
+ for (
+ let subKeyIdx = 0;
+ subKeyIdx < keyObj.subKeys.length;
+ subKeyIdx++
+ ) {
+ let subkey = keyObj.subKeys[subKeyIdx].keyId;
+ if (subkey.toLowerCase().includes(searchTxt)) {
+ showKey = true;
+ }
+ }
+ }
+ // take option to show invalid/untrusted... keys into account
+ if (
+ showKey &&
+ determineHiddenKeys(keyObj, showInvalidKeys, showOthersKeys)
+ ) {
+ keyShowList.push(i);
+ }
+ }
+
+ return keyShowList;
+ },
+
+ /**
+ * Trigger re-displaying the list of keys and apply a filter
+ *
+ * @param selectedRow: Number - the row that is currently selected or
+ * clicked on
+ */
+ applyFilter(selectedRow) {
+ let keyDisplayList = this.getFilteredKeys();
+
+ this.keyFilterList = [];
+ if (keyDisplayList === null) {
+ for (let i = 0; i < this.keyViewList.length; i++) {
+ this.keyFilterList.push(i);
+ }
+
+ this.adjustRowCount(this.keyViewList.length, selectedRow);
+ } else {
+ for (let i = 0; i < this.keyViewList.length; i++) {
+ if (keyDisplayList.includes(this.keyViewList[i].keyNum)) {
+ this.keyFilterList.push(i);
+ }
+ }
+
+ this.adjustRowCount(this.keyFilterList.length, selectedRow);
+ }
+ },
+
+ /**
+ * Re-calculate the row count and instruct the view to update
+ */
+ adjustRowCount(newRowCount, selectedRow) {
+ if (this.rowCount === newRowCount) {
+ gUserList.invalidate();
+ return;
+ }
+
+ let delta = newRowCount - this.rowCount;
+ this.rowCount = newRowCount;
+ gUserList.rowCountChanged(selectedRow, delta);
+ },
+
+ /**
+ * Determine the row object from the a filtered row number
+ *
+ * @param row: Number - row number of displayed (=filtered) list
+ * @returns Object: keyViewList entry of corresponding row
+ */
+ getFilteredRow(row) {
+ let r = this.keyFilterList[row];
+ if (r !== undefined) {
+ return this.keyViewList[r];
+ }
+ return null;
+ },
+
+ treebox: null,
+};
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml
new file mode 100644
index 0000000000..3c66224b8f
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml
@@ -0,0 +1,406 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/enigmail.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ id="enigmailKeyManager"
+ data-l10n-id="openpgp-key-man-title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ onload="enigmailKeyManagerLoad();"
+ height="450"
+ width="700"
+ style="min-height: 450px"
+>
+ <dialog
+ id="openPgpKeyManagerDialog"
+ data-l10n-id="openpgp-card-details-close-window-label"
+ data-l10n-attrs="buttonlabelaccept"
+ buttons="accept"
+ >
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailCommon.js"
+ />
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailKeyManager.js"
+ />
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/commonWorkflows.js"
+ />
+ <script
+ type="application/x-javascript"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ />
+
+ <linkset>
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+
+ <commandset id="tasksCommands" />
+
+ <command id="cmd_close" oncommand="window.close()" />
+ <command id="cmd_enigmailDeleteKey" oncommand="enigmailDeleteKey()" />
+
+ <keyset id="winKeys">
+ <key
+ id="key_selectAll"
+ data-l10n-id="openpgp-key-man-select-all-key"
+ oncommand="enigmailSelectAllKeys()"
+ modifiers="accel"
+ />
+
+ <key
+ id="key_keyDetails"
+ data-l10n-id="openpgp-key-man-key-details-key"
+ oncommand="enigmailKeyDetails()"
+ modifiers="accel"
+ />
+
+ <key
+ id="key_enigDelete"
+ keycode="VK_DELETE"
+ command="cmd_enigmailDeleteKey"
+ />
+ <key id="key_close" />
+ <key id="key_quit" />
+ </keyset>
+
+ <toolbar type="menubar" style="margin-inline: -8px -10px; margin-top: -8px">
+ <menubar id="main-menubar">
+ <menu id="menu_File" data-l10n-id="openpgp-key-man-file-menu">
+ <menupopup id="menu_FilePopup" onpopupshowing="enigmailKeyMenu();">
+ <menuitem
+ id="importPubFromFile"
+ data-l10n-id="openpgp-key-man-import-public-from-file"
+ oncommand="EnigmailCommon_importObjectFromFile('pub');"
+ />
+ <menuitem
+ id="importSecFromFile"
+ data-l10n-id="openpgp-key-man-import-secret-from-file"
+ oncommand="openKeyWizard(true)"
+ />
+ <menuitem
+ id="importSigFromFile"
+ data-l10n-id="openpgp-key-man-import-sig-from-file"
+ oncommand="EnigmailCommon_importObjectFromFile('rev');"
+ />
+ <menuseparator />
+ <menuitem
+ id="exportPublicKey"
+ class="requires-key-selection"
+ data-l10n-id="openpgp-key-man-export-to-file"
+ oncommand="enigmailExportKeys('public');"
+ />
+ <menuitem
+ id="sendKey"
+ data-l10n-id="openpgp-key-man-send-keys"
+ class="requires-key-selection"
+ oncommand="enigCreateKeyMsg();"
+ />
+ <menuseparator />
+ <menuitem
+ id="backupSecretKey"
+ data-l10n-id="openpgp-key-man-backup-secret-keys"
+ oncommand="enigmailExportKeys('secret');"
+ />
+ <menuseparator />
+ <menuitem
+ id="refreshKeys"
+ data-l10n-id="openpgp-key-man-reload"
+ oncommand="clearKeyCache();"
+ />
+ <!-- add Close and Exit menu items -->
+ <menuitem
+ id="menu_close"
+ data-l10n-id="openpgp-key-man-close"
+ oncommand="onDialogClose()"
+ />
+ </menupopup>
+ </menu>
+
+ <menu data-l10n-id="openpgp-key-man-edit-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu();">
+ <menuitem
+ id="importFromClipbrd"
+ data-l10n-id="openpgp-key-man-import-from-clipbrd"
+ oncommand="enigmailImportFromClipbrd();"
+ />
+ <menuitem
+ id="importFromUrl"
+ data-l10n-id="openpgp-key-man-import-from-url"
+ oncommand="enigmailImportKeysFromUrl();"
+ />
+ <menuitem
+ id="copyFprs"
+ data-l10n-id="openpgp-key-man-copy-fprs"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPFingerPrints()"
+ />
+ <menuitem
+ id="copyKeyIds"
+ data-l10n-id="openpgp-key-man-copy-key-ids"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPKeyIds()"
+ />
+ <menuitem
+ id="copyToClipbrd"
+ data-l10n-id="openpgp-key-man-copy-to-clipboard"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="enigmailCopyToClipbrd();"
+ />
+ <menuseparator />
+
+ <menuitem
+ id="revokeKey"
+ data-l10n-id="openpgp-key-man-revoke-key"
+ oncommand="enigmailRevokeKey()"
+ />
+
+ <menuitem
+ id="deleteKey"
+ data-l10n-id="openpgp-key-man-del-key"
+ key="key_enigDelete"
+ class="requires-key-selection"
+ oncommand="enigmailDeleteKey();"
+ />
+
+ <menuseparator />
+
+ <menuitem
+ id="selectAll"
+ data-l10n-id="openpgp-key-man-select-all"
+ oncommand="enigmailSelectAllKeys()"
+ key="key_selectAll"
+ />
+ </menupopup>
+ </menu>
+
+ <menu id="viewMenu" data-l10n-id="openpgp-key-man-view-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu()">
+ <!-- view menu -->
+ <menuitem
+ id="keyDetails"
+ data-l10n-id="openpgp-key-man-key-props"
+ class="requires-single-key-selection"
+ key="key_keyDetails"
+ oncommand="enigmailKeyDetails();"
+ />
+ <menuseparator />
+ <menuitem
+ id="showInvalidKeys"
+ data-l10n-id="openpgp-key-man-show-invalid-keys"
+ type="checkbox"
+ checked="true"
+ persist="checked"
+ oncommand="applyFilter();"
+ />
+ <menuitem
+ id="showOthersKeys"
+ data-l10n-id="openpgp-key-man-show-others-keys"
+ type="checkbox"
+ checked="true"
+ persist="checked"
+ oncommand="applyFilter();"
+ />
+ </menupopup>
+ </menu>
+
+ <menu id="keyserverMenu" data-l10n-id="openpgp-key-man-keyserver-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu()">
+ <menuitem
+ id="importFromServer"
+ data-l10n-id="openpgp-key-man-discover-cmd"
+ oncommand="enigmailSearchKey()"
+ />
+ <menuitem
+ id="uploadToServer"
+ data-l10n-id="openpgp-key-man-publish-cmd"
+ oncommand="enigmailUploadKey()"
+ />
+ </menupopup>
+ </menu>
+
+ <menu id="generateMenu" data-l10n-id="openpgp-key-man-generate-menu">
+ <menupopup onpopupshowing="enigmailKeyMenu();">
+ <!-- generate menu -->
+ <menuitem
+ id="genKey"
+ data-l10n-id="openpgp-key-man-generate"
+ oncommand="openKeyWizard()"
+ />
+ </menupopup>
+ </menu>
+ </menubar>
+ </toolbar>
+
+ <popupset>
+ <menupopup id="ctxmenu" onpopupshowing="return enigmailKeyMenu();">
+ <menu id="ctxmenu-copy" data-l10n-id="openpgp-key-man-ctx-copy">
+ <menupopup id="ctxmenu-copy-popup">
+ <menuitem
+ id="ctxCopyFprs"
+ data-l10n-id="openpgp-key-man-ctx-copy-fprs"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPFingerPrints()"
+ />
+ <menuitem
+ id="ctxCopyKeyIds"
+ data-l10n-id="openpgp-key-man-ctx-copy-key-ids"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="copyOpenPGPKeyIds()"
+ />
+ <menuitem
+ id="ctxCopyPublicKeys"
+ data-l10n-id="openpgp-key-man-ctx-copy-public-keys"
+ data-l10n-args='{"count": 0}'
+ class="requires-key-selection enigmail-bulk-key-operation"
+ oncommand="enigmailCopyToClipbrd()"
+ />
+ </menupopup>
+ </menu>
+ <menuitem
+ data-l10n-id="openpgp-key-man-export-to-file"
+ id="ctxExport"
+ oncommand="enigmailExportKeys('public')"
+ />
+ <menuitem
+ data-l10n-id="openpgp-key-man-send-keys"
+ id="ctxSendKey"
+ oncommand="enigCreateKeyMsg()"
+ />
+
+ <menuseparator />
+
+ <menuitem
+ id="ctxRevokeKey"
+ data-l10n-id="openpgp-key-man-revoke-key"
+ oncommand="enigmailRevokeKey()"
+ />
+ <menuitem
+ id="ctxDeleteKey"
+ data-l10n-id="openpgp-key-man-del-key"
+ class="requires-key-selection"
+ oncommand="enigmailDeleteKey()"
+ />
+ <menuitem
+ id="ctxDetails"
+ data-l10n-id="openpgp-key-man-key-props"
+ class="requires-single-key-selection"
+ oncommand="enigmailKeyDetails()"
+ />
+ </menupopup>
+ </popupset>
+
+ <separator class="thin" />
+
+ <hbox flex="0" align="center">
+ <html:input
+ id="filterKey"
+ size="35"
+ data-l10n-id="openpgp-key-man-filter-label"
+ />
+ </hbox>
+
+ <tooltip
+ id="nothingFound"
+ data-l10n-id="openpgp-key-man-nothing-found-tooltip"
+ noautohide="true"
+ />
+ <tooltip
+ id="pleaseWait"
+ data-l10n-id="openpgp-key-man-please-wait-tooltip"
+ noautohide="true"
+ />
+
+ <separator class="thin" />
+
+ <hbox flex="1" style="min-height: 300px">
+ <tree
+ id="pgpKeyList"
+ flex="1"
+ enableColumnDrag="true"
+ seltype="multiple"
+ persist="sortDirection sortResource"
+ sortDirection="ascending"
+ sortResource="enigUserNameCol"
+ hidecolumnpicker="false"
+ context="ctxmenu"
+ >
+ <treecols>
+ <treecol
+ id="enigUserNameCol"
+ primary="true"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ data-l10n-id="openpgp-key-man-user-id-label"
+ style="width: 400px; flex: 1 auto"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="keyCol"
+ style="width: 100px; flex: 1 auto"
+ data-l10n-id="openpgp-key-id-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="createdCol"
+ style="width: 70px; flex: 1 auto"
+ data-l10n-id="openpgp-key-created-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="expCol"
+ style="width: 70px; flex: 1 auto"
+ data-l10n-id="openpgp-key-expiry-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ persist="width ordinal hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="fprCol"
+ style="width: 70px; flex: 1 auto"
+ data-l10n-id="openpgp-key-man-fingerprint-label"
+ class="sortDirectionIndicator"
+ onclick="sortTree(this)"
+ hidden="true"
+ persist="width ordinal hidden"
+ />
+ </treecols>
+
+ <treechildren id="pgpKeyListChildren" properties="" />
+ </tree>
+ </hbox>
+
+ <hbox id="statusLine">
+ <label id="statusText" value="" />
+ <html:progress id="progressBar" style="visibility: collapsed" />
+ </hbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js b/comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
new file mode 100644
index 0000000000..d62c676d25
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js
@@ -0,0 +1,3460 @@
+/* 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";
+
+/* import-globals-from ../../../../base/content/aboutMessage.js */
+/* import-globals-from ../../../../base/content/msgHdrView.js */
+/* import-globals-from ../../../../base/content/msgSecurityPane.js */
+
+// TODO: check if this is safe
+/* eslint-disable no-unsanitized/property */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ CollectedKeysDB: "chrome://openpgp/content/modules/CollectedKeysDB.jsm",
+ EnigmailArmor: "chrome://openpgp/content/modules/armor.jsm",
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.jsm",
+ EnigmailData: "chrome://openpgp/content/modules/data.jsm",
+ EnigmailDecryption: "chrome://openpgp/content/modules/decryption.jsm",
+ EnigmailDialog: "chrome://openpgp/content/modules/dialog.jsm",
+ EnigmailFixExchangeMsg: "chrome://openpgp/content/modules/fixExchangeMsg.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.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",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailMsgRead: "chrome://openpgp/content/modules/msgRead.jsm",
+ EnigmailPersistentCrypto:
+ "chrome://openpgp/content/modules/persistentCrypto.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailStreams: "chrome://openpgp/content/modules/streams.jsm",
+ EnigmailTrust: "chrome://openpgp/content/modules/trust.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+ // EnigmailWks: "chrome://openpgp/content/modules/webKey.jsm",
+ KeyLookupHelper: "chrome://openpgp/content/modules/keyLookupHelper.jsm",
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+ RNP: "chrome://openpgp/content/modules/RNP.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+var Enigmail = {};
+
+Enigmail.getEnigmailSvc = function () {
+ return EnigmailCore.getService(window);
+};
+
+Enigmail.msg = {
+ decryptedMessage: null,
+ securityInfo: null,
+ lastSaveDir: "",
+ messagePane: null,
+ decryptButton: null,
+ savedHeaders: null,
+ removeListener: false,
+ enableExperiments: false,
+ headersList: [
+ "content-transfer-encoding",
+ "x-enigmail-version",
+ "x-pgp-encoding-format",
+ //"autocrypt-setup-message",
+ ],
+ buggyMailType: null,
+ changedAttributes: [],
+ allAttachmentsDone: false,
+ messageDecryptDone: false,
+ showPartialDecryptionReminder: false,
+
+ get notificationBox() {
+ return gMessageNotificationBar.msgNotificationBar;
+ },
+
+ removeNotification(value) {
+ let item = this.notificationBox.getNotificationWithValue(value);
+ // Remove the notification only if the user didn't previously close it.
+ if (item) {
+ this.notificationBox.removeNotification(item, true);
+ }
+ },
+
+ messengerStartup() {
+ Enigmail.msg.messagePane = document.getElementById("messagepane");
+
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: Startup\n");
+
+ Enigmail.msg.savedHeaders = null;
+
+ Enigmail.msg.decryptButton = document.getElementById(
+ "button-enigmail-decrypt"
+ );
+
+ setTimeout(function () {
+ // if nothing happened, then load all keys after 1 hour
+ // to trigger the key check
+ EnigmailKeyRing.getAllKeys();
+ }, 3600 * 1000); // 1 hour
+
+ // Need to add event listener to Enigmail.msg.messagePane to make it work
+ // Adding to msgFrame doesn't seem to work
+ Enigmail.msg.messagePane.addEventListener(
+ "unload",
+ Enigmail.msg.messageFrameUnload.bind(Enigmail.msg),
+ true
+ );
+
+ EnigmailMsgRead.ensureExtraAddonHeaders();
+ gMessageListeners.push(Enigmail.msg.messageListener);
+ Enigmail.msg.messageListener.onEndHeaders();
+ },
+
+ messageListener: {
+ onStartHeaders() {
+ Enigmail.hdrView.reset();
+ Enigmail.msg.mimeParts = null;
+
+ /*
+ if ("autocrypt" in gExpandedHeaderView) {
+ delete gExpandedHeaderView.autocrypt;
+ }
+ */
+ if ("openpgp" in gExpandedHeaderView) {
+ delete gExpandedHeaderView.openpgp;
+ }
+ },
+ onEndHeaders() {},
+ onEndAttachments() {},
+ },
+
+ /*
+ viewSecurityInfo(event, displaySmimeMsg) {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: viewSecurityInfo\n");
+
+ if (event && event.button !== 0) {
+ return;
+ }
+
+ if (gSignatureStatus >= 0 || gEncryptionStatus >= 0) {
+ showMessageReadSecurityInfo();
+ } else if (Enigmail.msg.securityInfo) {
+ this.viewOpenpgpInfo();
+ } else {
+ showMessageReadSecurityInfo();
+ }
+ },
+ */
+
+ clearLastMessage() {
+ EnigmailSingletons.clearLastDecryptedMessage();
+ },
+
+ messageReload(noShowReload) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageReload: " + noShowReload + "\n"
+ );
+
+ this.clearLastMessage();
+ ReloadMessage();
+ },
+
+ messengerClose() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messengerClose()\n");
+ },
+
+ reloadCompleteMsg() {
+ this.clearLastMessage();
+ ReloadMessage();
+ },
+
+ setAttachmentReveal(attachmentList) {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: setAttachmentReveal\n");
+
+ var revealBox = document.getElementById("enigmailRevealAttachments");
+ if (revealBox) {
+ // there are situations when evealBox is not yet present
+ revealBox.setAttribute("hidden", !attachmentList ? "true" : "false");
+ }
+ },
+
+ messageCleanup() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageCleanup\n");
+ for (let value of [
+ "decryptInlinePGReminder",
+ "decryptInlinePG",
+ "brokenExchangeProgress",
+ "hasNestedEncryptedParts",
+ "hasConflictingKeyOpenPGP",
+ ]) {
+ this.removeNotification(value);
+ }
+ Enigmail.msg.showPartialDecryptionReminder = false;
+
+ let element = document.getElementById("openpgpKeyBox");
+ if (element) {
+ element.hidden = true;
+ }
+ element = document.getElementById("signatureKeyBox");
+ if (element) {
+ element.hidden = true;
+ element.removeAttribute("keyid");
+ }
+
+ this.setAttachmentReveal(null);
+
+ Enigmail.msg.decryptedMessage = null;
+ Enigmail.msg.securityInfo = null;
+
+ Enigmail.msg.allAttachmentsDone = false;
+ Enigmail.msg.messageDecryptDone = false;
+
+ let cryptoBox = document.getElementById("cryptoBox");
+ if (cryptoBox) {
+ cryptoBox.removeAttribute("decryptDone");
+ }
+
+ Enigmail.msg.toAndCCSet = null;
+ Enigmail.msg.authorEmail = "";
+
+ Enigmail.msg.keyCollectCandidates = new Map();
+
+ EnigmailKeyRing.emailAddressesWithSecretKey = null;
+
+ Enigmail.msg.attachedKeys = [];
+ Enigmail.msg.attachedSenderEmailKeysIndex = [];
+
+ Enigmail.msg.autoProcessPgpKeyAttachmentTransactionID++;
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount = 0;
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed = 0;
+ Enigmail.msg.unhideMissingSigKeyBoxIsTODO = false;
+ Enigmail.msg.missingSigKey = null;
+ Enigmail.msg.buggyMailType = null;
+ },
+
+ messageFrameUnload() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageFrameUnload\n");
+ Enigmail.msg.savedHeaders = null;
+ Enigmail.msg.messageCleanup();
+ },
+
+ getCurrentMsgUriSpec() {
+ return gMessageURI || "";
+ },
+
+ getCurrentMsgUrl() {
+ var uriSpec = this.getCurrentMsgUriSpec();
+ return EnigmailMsgRead.getUrlFromUriSpec(uriSpec);
+ },
+
+ updateOptionsDisplay() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: updateOptionsDisplay: \n");
+ var optList = ["autoDecrypt"];
+
+ for (let j = 0; j < optList.length; j++) {
+ let menuElement = document.getElementById("enigmail_" + optList[j]);
+ menuElement.setAttribute(
+ "checked",
+ Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")
+ ? "true"
+ : "false"
+ );
+
+ menuElement = document.getElementById("enigmail_" + optList[j] + "2");
+ if (menuElement) {
+ menuElement.setAttribute(
+ "checked",
+ Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")
+ ? "true"
+ : "false"
+ );
+ }
+ }
+
+ optList = ["decryptverify"];
+ for (let j = 0; j < optList.length; j++) {
+ let menuElement = document.getElementById("enigmail_" + optList[j]);
+ if (Enigmail.msg.decryptButton && Enigmail.msg.decryptButton.disabled) {
+ menuElement.setAttribute("disabled", "true");
+ } else {
+ menuElement.removeAttribute("disabled");
+ }
+
+ menuElement = document.getElementById("enigmail_" + optList[j] + "2");
+ if (menuElement) {
+ if (Enigmail.msg.decryptButton && Enigmail.msg.decryptButton.disabled) {
+ menuElement.setAttribute("disabled", "true");
+ } else {
+ menuElement.removeAttribute("disabled");
+ }
+ }
+ }
+ },
+
+ setMainMenuLabel() {
+ let o = ["menu_Enigmail", "appmenu-Enigmail"];
+
+ let m0 = document.getElementById(o[0]);
+ let m1 = document.getElementById(o[1]);
+
+ m1.setAttribute("enigmaillabel", m0.getAttribute("enigmaillabel"));
+
+ for (let menuId of o) {
+ let menu = document.getElementById(menuId);
+
+ if (menu) {
+ let lbl = menu.getAttribute("enigmaillabel");
+ menu.setAttribute("label", lbl);
+ }
+ }
+ },
+
+ displayMainMenu(menuPopup) {
+ let obj = menuPopup.firstChild;
+
+ while (obj) {
+ if (
+ obj.getAttribute("enigmailtype") == "enigmail" ||
+ obj.getAttribute("advanced") == "true"
+ ) {
+ obj.removeAttribute("hidden");
+ }
+
+ obj = obj.nextSibling;
+ }
+
+ EnigmailFuncs.collapseAdvanced(
+ menuPopup,
+ "hidden",
+ Enigmail.msg.updateOptionsDisplay()
+ );
+ },
+
+ /**
+ * Determine if Autocrypt is enabled for the currently selected message
+ */
+ /*
+ isAutocryptEnabled() {
+ try {
+ let email = EnigmailFuncs.stripEmail(
+ gFolderDisplay.selectedMessage.recipients
+ ).toLowerCase();
+ let identity = MailServices.accounts.allIdentities.find(id =>
+ id.email?.toLowerCase() == email
+ );
+
+ if (identity) {
+ let acct = EnigmailFuncs.getAccountForIdentity(identity);
+ return acct.incomingServer.getBoolValue("enableAutocrypt");
+ }
+ } catch (ex) {}
+
+ return false;
+ },
+ */
+
+ messageImport() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageImport:\n");
+
+ return this.messageParse(
+ true,
+ true,
+ "",
+ this.getCurrentMsgUriSpec(),
+ false
+ );
+ },
+
+ /***
+ * check that handler for multipart/signed is set to Enigmail.
+ * if handler is different, change it and reload message
+ *
+ * @return: - true if handler is OK
+ * - false if handler was changed and message is reloaded
+ */
+ checkPgpmimeHandler() {
+ if (
+ EnigmailVerify.currentCtHandler !== EnigmailConstants.MIME_HANDLER_PGPMIME
+ ) {
+ EnigmailVerify.registerPGPMimeHandler();
+ this.messageReload();
+ return false;
+ }
+
+ return true;
+ },
+
+ // callback function for automatic decryption
+ async messageAutoDecrypt() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageAutoDecrypt:\n");
+ await Enigmail.msg.messageDecrypt(null, true);
+ },
+
+ async notifyMessageDecryptDone() {
+ Enigmail.msg.messageDecryptDone = true;
+ await Enigmail.msg.processAfterAttachmentsAndDecrypt();
+
+ // Show the partial inline encryption reminder only if the decryption action
+ // came from a partially inline encrypted message.
+ if (Enigmail.msg.showPartialDecryptionReminder) {
+ Enigmail.msg.showPartialDecryptionReminder = false;
+
+ this.notificationBox.appendNotification(
+ "decryptInlinePGReminder",
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-reminder-partial-display"
+ ),
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ }
+ },
+
+ // analyse message header and decrypt/verify message
+ async messageDecrypt(event, isAuto) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageDecrypt: " + event + "\n"
+ );
+
+ event = !!event;
+
+ this.mimeParts = null;
+
+ if (!isAuto) {
+ EnigmailVerify.setManualUri(this.getCurrentMsgUriSpec());
+ }
+
+ let contentType = "text/plain";
+ if ("content-type" in currentHeaderData) {
+ contentType = currentHeaderData["content-type"].headerValue;
+ }
+
+ // don't parse message if we know it's a PGP/MIME message
+ if (
+ contentType.search(/^multipart\/encrypted(;|$)/i) === 0 &&
+ contentType.search(/application\/pgp-encrypted/i) > 0
+ ) {
+ this.movePEPsubject();
+ await this.messageDecryptCb(event, isAuto, null);
+ await this.notifyMessageDecryptDone();
+ return;
+ } else if (
+ contentType.search(/^multipart\/signed(;|$)/i) === 0 &&
+ contentType.search(/application\/pgp-signature/i) > 0
+ ) {
+ this.movePEPsubject();
+ await this.messageDecryptCb(event, isAuto, null);
+ await this.notifyMessageDecryptDone();
+ return;
+ }
+
+ let url = this.getCurrentMsgUrl();
+ if (!url) {
+ await Enigmail.msg.messageDecryptCb(event, isAuto, null);
+ await Enigmail.msg.notifyMessageDecryptDone();
+ return;
+ }
+ await new Promise(resolve => {
+ EnigmailMime.getMimeTreeFromUrl(
+ url.spec,
+ false,
+ async function (mimeMsg) {
+ await Enigmail.msg.messageDecryptCb(event, isAuto, mimeMsg);
+ await Enigmail.msg.notifyMessageDecryptDone();
+ resolve();
+ }
+ );
+ });
+ },
+
+ /***
+ * walk through the (sub-) mime tree and determine PGP/MIME encrypted and signed message parts
+ *
+ * @param mimePart: parent object to walk through
+ * @param resultObj: object containing two arrays. The resultObj must be pre-initialized by the caller
+ * - encrypted
+ * - signed
+ */
+ enumerateMimeParts(mimePart, resultObj) {
+ EnigmailLog.DEBUG(
+ 'enumerateMimeParts: partNum="' + mimePart.partNum + '"\n'
+ );
+ EnigmailLog.DEBUG(" " + mimePart.fullContentType + "\n");
+ EnigmailLog.DEBUG(
+ " " + mimePart.subParts.length + " subparts\n"
+ );
+
+ try {
+ var ct = mimePart.fullContentType;
+ if (typeof ct == "string") {
+ ct = ct.replace(/[\r\n]/g, " ");
+ if (ct.search(/multipart\/signed.*application\/pgp-signature/i) >= 0) {
+ resultObj.signed.push(mimePart.partNum);
+ } else if (ct.search(/application\/pgp-encrypted/i) >= 0) {
+ resultObj.encrypted.push(mimePart.partNum);
+ }
+ }
+ } catch (ex) {
+ // catch exception if no headers or no content-type defined.
+ }
+
+ var i;
+ for (i in mimePart.subParts) {
+ this.enumerateMimeParts(mimePart.subParts[i], resultObj);
+ }
+ },
+
+ async messageDecryptCb(event, isAuto, mimeMsg) {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageDecryptCb:\n");
+
+ let enigmailSvc;
+ let contentType = "";
+ try {
+ if (!mimeMsg) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageDecryptCb: mimeMsg is null\n"
+ );
+ try {
+ contentType = currentHeaderData["content-type"].headerValue;
+ } catch (ex) {
+ contentType = "text/plain";
+ }
+ mimeMsg = {
+ partNum: "1",
+ headers: {
+ has() {
+ return false;
+ },
+ contentType: {
+ type: contentType,
+ mediatype: "",
+ subtype: "",
+ },
+ },
+ fullContentType: contentType,
+ body: "",
+ parent: null,
+ subParts: [],
+ };
+ }
+
+ // Copy selected headers
+ Enigmail.msg.savedHeaders = {
+ autocrypt: [],
+ };
+
+ for (let h in currentHeaderData) {
+ if (h.search(/^autocrypt\d*$/) === 0) {
+ Enigmail.msg.savedHeaders.autocrypt.push(
+ currentHeaderData[h].headerValue
+ );
+ }
+ }
+
+ if (!mimeMsg.fullContentType) {
+ mimeMsg.fullContentType = "text/plain";
+ }
+
+ Enigmail.msg.savedHeaders["content-type"] = mimeMsg.fullContentType;
+ this.mimeParts = mimeMsg;
+
+ for (var index = 0; index < Enigmail.msg.headersList.length; index++) {
+ var headerName = Enigmail.msg.headersList[index];
+ var headerValue = "";
+
+ if (mimeMsg.headers.has(headerName)) {
+ let h = mimeMsg.headers.get(headerName);
+ if (Array.isArray(h)) {
+ headerValue = h.join("");
+ } else {
+ headerValue = h;
+ }
+ }
+ Enigmail.msg.savedHeaders[headerName] = headerValue;
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: header " +
+ headerName +
+ ": '" +
+ headerValue +
+ "'\n"
+ );
+ }
+
+ var msgSigned =
+ mimeMsg.fullContentType.search(/^multipart\/signed/i) === 0 &&
+ EnigmailMime.getProtocol(mimeMsg.fullContentType).search(
+ /^application\/pgp-signature/i
+ ) === 0;
+ var msgEncrypted =
+ mimeMsg.fullContentType.search(/^multipart\/encrypted/i) === 0 &&
+ EnigmailMime.getProtocol(mimeMsg.fullContentType).search(
+ /^application\/pgp-encrypted/i
+ ) === 0;
+ var resultObj = {
+ encrypted: [],
+ signed: [],
+ };
+
+ if (mimeMsg.subParts.length > 0) {
+ this.enumerateMimeParts(mimeMsg, resultObj);
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: embedded objects: " +
+ resultObj.encrypted.join(", ") +
+ " / " +
+ resultObj.signed.join(", ") +
+ "\n"
+ );
+
+ msgSigned = msgSigned || resultObj.signed.length > 0;
+ msgEncrypted = msgEncrypted || resultObj.encrypted.length > 0;
+
+ /*
+ if (
+ "autocrypt-setup-message" in Enigmail.msg.savedHeaders &&
+ Enigmail.msg.savedHeaders["autocrypt-setup-message"].toLowerCase() ===
+ "v1"
+ ) {
+ if (
+ currentAttachments[0].contentType.search(
+ /^application\/autocrypt-setup$/i
+ ) === 0
+ ) {
+ Enigmail.hdrView.displayAutoCryptSetupMsgHeader();
+ return;
+ }
+ }
+ */
+
+ // HACK for Zimbra OpenPGP Zimlet
+ // Zimbra illegally changes attachment content-type to application/pgp-encrypted which interfers with below
+ // see https://sourceforge.net/p/enigmail/bugs/600/
+
+ try {
+ if (
+ mimeMsg.subParts.length > 1 &&
+ mimeMsg.headers.has("x-mailer") &&
+ mimeMsg.headers.get("x-mailer")[0].includes("ZimbraWebClient") &&
+ mimeMsg.subParts[0].fullContentType.includes("text/plain") &&
+ mimeMsg.fullContentType.includes("multipart/mixed") &&
+ mimeMsg.subParts[1].fullContentType.includes(
+ "application/pgp-encrypted"
+ )
+ ) {
+ await this.messageParse(
+ event,
+ false,
+ Enigmail.msg.savedHeaders["content-transfer-encoding"],
+ this.getCurrentMsgUriSpec(),
+ isAuto
+ );
+ return;
+ }
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ // HACK for MS-EXCHANGE-Server Problem:
+ // check for possible bad mime structure due to buggy exchange server:
+ // - multipart/mixed Container with
+ // - application/pgp-encrypted Attachment with name "PGPMIME Versions Identification"
+ // - application/octet-stream Attachment with name "encrypted.asc" having the encrypted content in base64
+ // - see:
+ // - http://www.mozilla-enigmail.org/forum/viewtopic.php?f=4&t=425
+ // - http://sourceforge.net/p/enigmail/forum/support/thread/4add2b69/
+
+ // iPGMail produces a similar broken structure, see here:
+ // - https://sourceforge.net/p/enigmail/forum/support/thread/afc9c246/#5de7
+
+ // Don't attempt to detect again, if we have already decided
+ // it's a buggy exchange message (buggyMailType is already set).
+
+ if (
+ !Enigmail.msg.buggyMailType &&
+ mimeMsg.subParts.length == 3 &&
+ mimeMsg.fullContentType.search(/multipart\/mixed/i) >= 0 &&
+ mimeMsg.subParts[0].fullContentType.search(/multipart\/encrypted/i) <
+ 0 &&
+ mimeMsg.subParts[0].fullContentType.search(
+ /(text\/(plain|html)|multipart\/alternative)/i
+ ) >= 0 &&
+ mimeMsg.subParts[1].fullContentType.search(
+ /application\/pgp-encrypted/i
+ ) >= 0
+ ) {
+ if (
+ mimeMsg.subParts[1].fullContentType.search(
+ /multipart\/encrypted/i
+ ) < 0 &&
+ mimeMsg.subParts[1].fullContentType.search(
+ /PGP\/?MIME Versions? Identification/i
+ ) >= 0 &&
+ mimeMsg.subParts[2].fullContentType.search(
+ /application\/octet-stream/i
+ ) >= 0 &&
+ mimeMsg.subParts[2].fullContentType.search(/encrypted.asc/i) >= 0
+ ) {
+ this.buggyMailType = "exchange";
+ } else {
+ this.buggyMailType = "iPGMail";
+ }
+
+ // signal that the structure matches to save the content later on
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay: messageDecryptCb: enabling MS-Exchange hack\n"
+ );
+
+ await this.buggyMailHeader();
+ return;
+ }
+ }
+
+ var contentEncoding = "";
+ var msgUriSpec = this.getCurrentMsgUriSpec();
+
+ if (Enigmail.msg.savedHeaders) {
+ contentType = Enigmail.msg.savedHeaders["content-type"];
+ contentEncoding =
+ Enigmail.msg.savedHeaders["content-transfer-encoding"];
+ }
+
+ let smime =
+ contentType.search(
+ /multipart\/signed; protocol="application\/pkcs7-signature/i
+ ) >= 0;
+ if (!smime && (msgSigned || msgEncrypted)) {
+ // PGP/MIME messages
+ enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ if (!Enigmail.msg.checkPgpmimeHandler()) {
+ return;
+ }
+
+ if (isAuto && !Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ if (EnigmailVerify.getManualUri() != this.getCurrentMsgUriSpec()) {
+ // decryption set to manual
+ Enigmail.hdrView.updatePgpStatus(
+ EnigmailConstants.POSSIBLE_PGPMIME,
+ 0, // exitCode, statusFlags
+ 0,
+ "",
+ "", // keyId, userId
+ "", // sigDetails
+ await l10n.formatValue("possibly-pgp-mime"), // infoMsg
+ null, // blockSeparation
+ null, // encToDetails
+ null
+ ); // xtraStatus
+ }
+ } else if (!isAuto) {
+ Enigmail.msg.messageReload(false);
+ }
+ return;
+ }
+
+ // inline-PGP messages
+ if (!isAuto || Services.prefs.getBoolPref("temp.openpgp.autoDecrypt")) {
+ await this.messageParse(
+ event,
+ false,
+ contentEncoding,
+ msgUriSpec,
+ isAuto
+ );
+ }
+ } catch (ex) {
+ EnigmailLog.writeException(
+ "enigmailMessengerOverlay.js: messageDecryptCb",
+ ex
+ );
+ }
+ },
+
+ /**
+ * Display header about reparing buggy MS-Exchange messages.
+ */
+ async buggyMailHeader() {
+ let uri = this.getCurrentMsgUrl();
+ Enigmail.hdrView.headerPane.updateSecurityStatus(
+ "",
+ 0,
+ 0,
+ 0,
+ "",
+ "",
+ "",
+ "",
+ "",
+ uri,
+ "",
+ "1"
+ );
+
+ // Warn that we can't fix a message that was opened from a local file.
+ if (!gFolder) {
+ Enigmail.msg.notificationBox.appendNotification(
+ "brokenExchange",
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-broken-exchange-opened"
+ ),
+ priority: Enigmail.msg.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+ return;
+ }
+
+ let buttons = [
+ {
+ "l10n-id": "openpgp-broken-exchange-repair",
+ popup: null,
+ callback(notification, button) {
+ Enigmail.msg.fixBuggyExchangeMail();
+ return false; // Close notification.
+ },
+ },
+ ];
+
+ Enigmail.msg.notificationBox.appendNotification(
+ "brokenExchange",
+ {
+ label: await document.l10n.formatValue("openpgp-broken-exchange-info"),
+ priority: Enigmail.msg.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ buttons
+ );
+ },
+
+ getFirstPGPMessageType(msgText) {
+ let indexEncrypted = msgText.indexOf("-----BEGIN PGP MESSAGE-----");
+ let indexSigned = msgText.indexOf("-----BEGIN PGP SIGNED MESSAGE-----");
+ if (indexEncrypted >= 0) {
+ if (
+ indexSigned == -1 ||
+ (indexSigned >= 0 && indexEncrypted < indexSigned)
+ ) {
+ return "encrypted";
+ }
+ }
+
+ if (indexSigned >= 0) {
+ return "signed";
+ }
+
+ return "";
+ },
+
+ trimIfEncrypted(msgText) {
+ // If it's an encrypted message, we want to trim (at least) the
+ // separator line between the header and the content.
+ // However, trimming all lines should be safe.
+
+ if (Enigmail.msg.getFirstPGPMessageType(msgText) == "encrypted") {
+ // \xA0 is non-breaking-space
+ msgText = msgText.replace(/^[ \t\xA0]+/gm, "");
+ }
+ return msgText;
+ },
+
+ async messageParse(
+ interactive,
+ importOnly,
+ contentEncoding,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex = "0"
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParse: " + interactive + "\n"
+ );
+
+ var bodyElement = this.getBodyElement(pbMessageIndex);
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: bodyElement=" + bodyElement + "\n"
+ );
+
+ if (!bodyElement) {
+ return;
+ }
+
+ let topElement = bodyElement;
+ var findStr = /* interactive ? null : */ "-----BEGIN PGP";
+ var msgText = null;
+ var foundIndex = -1;
+
+ let bodyElementFound = false;
+ let hasHeadOrTailNode = false;
+
+ if (bodyElement.firstChild) {
+ let node = bodyElement.firstChild;
+ while (node) {
+ if (
+ node.firstChild &&
+ node.firstChild.nodeName.toUpperCase() == "LEGEND" &&
+ node.firstChild.className == "moz-mime-attachment-header-name"
+ ) {
+ // we reached the area where inline attachments are displayed
+ // --> don't try to decrypt displayed inline attachments
+ break;
+ }
+ if (node.nodeName === "DIV") {
+ if (bodyElementFound) {
+ hasHeadOrTailNode = true;
+ break;
+ }
+
+ foundIndex = node.textContent.indexOf(findStr);
+
+ if (foundIndex < 0) {
+ hasHeadOrTailNode = true;
+ node = node.nextSibling;
+ continue;
+ }
+
+ if (foundIndex >= 0) {
+ if (
+ node.textContent.indexOf(findStr + " LICENSE AUTHORIZATION") ==
+ foundIndex
+ ) {
+ foundIndex = -1;
+ node = node.nextSibling;
+ continue;
+ }
+ }
+
+ if (foundIndex === 0) {
+ bodyElement = node;
+ bodyElementFound = true;
+ } else if (
+ foundIndex > 0 &&
+ node.textContent.substr(foundIndex - 1, 1).search(/[\r\n]/) === 0
+ ) {
+ bodyElement = node;
+ bodyElementFound = true;
+ }
+ }
+ node = node.nextSibling;
+ }
+ }
+
+ if (foundIndex >= 0 && !this.hasInlineQuote(topElement)) {
+ let beginIndex = {};
+ let endIndex = {};
+ let indentStr = {};
+
+ if (
+ Enigmail.msg.savedHeaders["content-type"].search(/^text\/html/i) === 0
+ ) {
+ let p = Cc["@mozilla.org/parserutils;1"].createInstance(
+ Ci.nsIParserUtils
+ );
+ const de = Ci.nsIDocumentEncoder;
+ msgText = p.convertToPlainText(
+ topElement.innerHTML,
+ de.OutputRaw | de.OutputBodyOnly,
+ 0
+ );
+ } else {
+ msgText = bodyElement.textContent;
+ }
+
+ if (!isAuto) {
+ let blockType = EnigmailArmor.locateArmoredBlock(
+ msgText,
+ 0,
+ "",
+ beginIndex,
+ endIndex,
+ indentStr
+ );
+ if (!blockType) {
+ msgText = "";
+ } else {
+ msgText = msgText.substring(beginIndex.value, endIndex.value + 1);
+ }
+ }
+
+ msgText = this.trimIfEncrypted(msgText);
+ }
+
+ if (!msgText) {
+ // No PGP content
+ return;
+ }
+
+ let charset = currentCharacterSet ?? "";
+ if (charset != "UTF-8") {
+ // Encode ciphertext to charset from unicode
+ msgText = EnigmailData.convertFromUnicode(msgText, charset);
+ }
+
+ if (isAuto) {
+ let ht = hasHeadOrTailNode || this.hasHeadOrTailBesidesInlinePGP(msgText);
+ if (ht) {
+ let infoId;
+ let buttonId;
+ if (
+ ht & EnigmailConstants.UNCERTAIN_SIGNATURE ||
+ Enigmail.msg.getFirstPGPMessageType(msgText) == "signed"
+ ) {
+ infoId = "openpgp-partially-signed";
+ buttonId = "openpgp-partial-verify-button";
+ } else {
+ infoId = "openpgp-partially-encrypted";
+ buttonId = "openpgp-partial-decrypt-button";
+ }
+
+ let [description, buttonLabel] = await document.l10n.formatValues([
+ { id: infoId },
+ { id: buttonId },
+ ]);
+
+ let buttons = [
+ {
+ label: buttonLabel,
+ popup: null,
+ callback(aNotification, aButton) {
+ Enigmail.msg.processOpenPGPSubset();
+ return false; // Close notification.
+ },
+ },
+ ];
+
+ this.notificationBox.appendNotification(
+ "decryptInlinePG",
+ {
+ label: description,
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ buttons
+ );
+ return;
+ }
+ }
+
+ var mozPlainText = bodyElement.innerHTML.search(/class="moz-text-plain"/);
+
+ if (mozPlainText >= 0 && mozPlainText < 40) {
+ // workaround for too much expanded emoticons in plaintext msg
+ var r = new RegExp(
+ /( )(;-\)|:-\)|;\)|:\)|:-\(|:\(|:-\\|:-P|:-D|:-\[|:-\*|>:o|8-\)|:-\$|:-X|=-O|:-!|O:-\)|:'\()( )/g
+ );
+ if (msgText.search(r) >= 0) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParse: performing emoticons fixing\n"
+ );
+ msgText = msgText.replace(r, "$2");
+ }
+ }
+
+ // ignoring text following armored block
+
+ //EnigmailLog.DEBUG("enigmailMessengerOverlay.js: msgText='"+msgText+"'\n");
+
+ var mailNewsUrl = EnigmailMsgRead.getUrlFromUriSpec(msgUriSpec);
+
+ var urlSpec = mailNewsUrl ? mailNewsUrl.spec : "";
+
+ let retry = 1;
+
+ await Enigmail.msg.messageParseCallback(
+ msgText,
+ EnigmailDecryption.getMsgDate(window),
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ urlSpec,
+ "",
+ retry,
+ "", // head
+ "", // tail
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ );
+ },
+
+ hasInlineQuote(node) {
+ if (node.innerHTML.search(/<blockquote.*-----BEGIN PGP /i) < 0) {
+ return false;
+ }
+
+ return EnigmailMsgRead.searchQuotedPgp(node);
+ },
+
+ hasHeadOrTailBesidesInlinePGP(msgText) {
+ let startIndex = msgText.search(/-----BEGIN PGP (SIGNED )?MESSAGE-----/m);
+ let endIndex = msgText.indexOf("-----END PGP");
+ let hasHead = false;
+ let hasTail = false;
+ let crypto = 0;
+
+ if (startIndex > 0) {
+ let pgpMsg = msgText.match(/(-----BEGIN PGP (SIGNED )?MESSAGE-----)/m)[0];
+ if (pgpMsg.search(/SIGNED/) > 0) {
+ crypto = EnigmailConstants.UNCERTAIN_SIGNATURE;
+ } else {
+ crypto = EnigmailConstants.DECRYPTION_FAILED;
+ }
+ let startSection = msgText.substr(0, startIndex - 1);
+ hasHead = startSection.search(/\S/) >= 0;
+ }
+
+ if (endIndex > startIndex) {
+ let nextLine = msgText.substring(endIndex).search(/[\n\r]/);
+ if (nextLine > 0) {
+ hasTail = msgText.substring(endIndex + nextLine).search(/\S/) >= 0;
+ }
+ }
+
+ if (hasHead || hasTail) {
+ return EnigmailConstants.PARTIALLY_PGP | crypto;
+ }
+
+ return 0;
+ },
+
+ async processOpenPGPSubset() {
+ Enigmail.msg.showPartialDecryptionReminder = true;
+ await this.messageDecrypt(null, false);
+ },
+
+ getBodyElement() {
+ let msgFrame = document.getElementById("messagepane");
+ if (!msgFrame || !msgFrame.contentDocument) {
+ return null;
+ }
+ return msgFrame.contentDocument.getElementsByTagName("body")[0];
+ },
+
+ async messageParseCallback(
+ msgText,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ messageUrl,
+ signature,
+ retry,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParseCallback: " +
+ interactive +
+ ", " +
+ interactive +
+ ", importOnly=" +
+ importOnly +
+ ", charset=" +
+ charset +
+ ", msgUrl=" +
+ messageUrl +
+ ", retry=" +
+ retry +
+ ", signature='" +
+ signature +
+ "'\n"
+ );
+
+ if (!msgText) {
+ return;
+ }
+
+ var enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ var plainText;
+ var exitCode;
+ var newSignature = "";
+ var statusFlags = 0;
+ var extStatusFlags = 0;
+
+ var errorMsgObj = {
+ value: "",
+ };
+ var keyIdObj = {};
+ var userIdObj = {};
+ var sigDetailsObj = {};
+ var encToDetailsObj = {};
+
+ var blockSeparationObj = {
+ value: "",
+ };
+
+ if (importOnly) {
+ // Import public key
+ await this.importKeyFromMsgBody(msgText);
+ return;
+ }
+ let armorHeaders = EnigmailArmor.getArmorHeaders(msgText);
+ if ("charset" in armorHeaders) {
+ charset = armorHeaders.charset;
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParseCallback: OVERRIDING charset=" +
+ charset +
+ "\n"
+ );
+ }
+
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var signatureObj = {};
+ signatureObj.value = signature;
+
+ var uiFlags = interactive
+ ? EnigmailConstants.UI_INTERACTIVE |
+ // EnigmailConstants.UI_ALLOW_KEY_IMPORT |
+ EnigmailConstants.UI_UNVERIFIED_ENC_OK
+ : 0;
+
+ plainText = EnigmailDecryption.decryptMessage(
+ window,
+ uiFlags,
+ msgText,
+ msgDate,
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ );
+
+ //EnigmailLog.DEBUG("enigmailMessengerOverlay.js: messageParseCallback: plainText='"+plainText+"'\n");
+
+ exitCode = exitCodeObj.value;
+ newSignature = signatureObj.value;
+
+ if (plainText === "" && exitCode === 0) {
+ plainText = " ";
+ }
+
+ statusFlags = statusFlagsObj.value;
+ extStatusFlags = statusFlagsObj.ext;
+
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: messageParseCallback: newSignature='" +
+ newSignature +
+ "'\n"
+ );
+
+ var errorMsg = errorMsgObj.value;
+
+ if (importOnly) {
+ if (interactive && errorMsg) {
+ EnigmailDialog.alert(window, errorMsg);
+ }
+ return;
+ }
+
+ var displayedUriSpec = Enigmail.msg.getCurrentMsgUriSpec();
+ if (!msgUriSpec || displayedUriSpec == msgUriSpec) {
+ if (exitCode && !statusFlags) {
+ // Failure, but we don't know why it failed.
+ // Peek inside msgText, and check what kind of content it is,
+ // so we can show a minimal error.
+
+ let msgType = Enigmail.msg.getFirstPGPMessageType(msgText);
+ if (msgType == "encrypted") {
+ statusFlags = EnigmailConstants.DECRYPTION_FAILED;
+ } else if (msgType == "signed") {
+ statusFlags = EnigmailConstants.BAD_SIGNATURE;
+ }
+ }
+
+ Enigmail.hdrView.updatePgpStatus(
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyIdObj.value,
+ userIdObj.value,
+ sigDetailsObj.value,
+ errorMsg,
+ null, // blockSeparation
+ encToDetailsObj.value,
+ null
+ ); // xtraStatus
+ }
+
+ var noSecondTry =
+ EnigmailConstants.GOOD_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_KEY |
+ EnigmailConstants.REVOKED_KEY |
+ EnigmailConstants.NO_PUBKEY |
+ EnigmailConstants.NO_SECKEY |
+ EnigmailConstants.IMPORTED_KEY |
+ EnigmailConstants.MISSING_PASSPHRASE |
+ EnigmailConstants.BAD_PASSPHRASE |
+ EnigmailConstants.UNKNOWN_ALGO |
+ EnigmailConstants.DECRYPTION_OKAY |
+ EnigmailConstants.OVERFLOWED;
+
+ if (exitCode !== 0 && !(statusFlags & noSecondTry)) {
+ // Bad signature/armor
+ if (retry == 1) {
+ msgText = EnigmailData.convertFromUnicode(msgText, "UTF-8");
+ await Enigmail.msg.messageParseCallback(
+ msgText,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ messageUrl,
+ signature,
+ retry + 1,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ );
+ return;
+ } else if (retry == 2) {
+ // Try to verify signature by accessing raw message text directly
+ // (avoid recursion by setting retry parameter to false on callback)
+ newSignature = "";
+ await Enigmail.msg.msgDirectDecrypt(
+ interactive,
+ importOnly,
+ contentEncoding,
+ charset,
+ newSignature,
+ 0,
+ head,
+ tail,
+ msgUriSpec,
+ msgDate,
+ Enigmail.msg.messageParseCallback,
+ isAuto
+ );
+ return;
+ } else if (retry == 3) {
+ msgText = EnigmailData.convertFromUnicode(msgText, "UTF-8");
+ await Enigmail.msg.messageParseCallback(
+ msgText,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ messageUrl,
+ null,
+ retry + 1,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto,
+ pbMessageIndex
+ );
+ return;
+ }
+ }
+
+ if (!plainText) {
+ // Show the subset that we cannot process, together with status.
+ plainText = msgText;
+ }
+
+ if (retry >= 2) {
+ plainText = EnigmailData.convertFromUnicode(
+ EnigmailData.convertToUnicode(plainText, "UTF-8"),
+ charset
+ );
+ }
+
+ // TODO: what is blockSeparation ? How to emulate with RNP?
+ /*
+ if (blockSeparationObj.value.includes(" ")) {
+ var blocks = blockSeparationObj.value.split(/ /);
+ var blockInfo = blocks[0].split(/:/);
+ plainText =
+ EnigmailData.convertFromUnicode(
+ "*Parts of the message have NOT been signed nor encrypted*",
+ charset
+ ) +
+ "\n\n" +
+ plainText.substr(0, blockInfo[1]) +
+ "\n\n" +
+ "*Multiple message blocks found -- decryption/verification aborted*";
+ }
+ */
+
+ // Save decrypted message status, headers, and content
+ var headerList = {
+ subject: "",
+ from: "",
+ date: "",
+ to: "",
+ cc: "",
+ };
+
+ var index, headerName;
+
+ if (!gViewAllHeaders) {
+ for (index = 0; index < headerList.length; index++) {
+ headerList[index] = "";
+ }
+ } else {
+ for (index = 0; index < gExpandedHeaderList.length; index++) {
+ headerList[gExpandedHeaderList[index].name] = "";
+ }
+
+ for (headerName in currentHeaderData) {
+ headerList[headerName] = "";
+ }
+ }
+
+ for (headerName in headerList) {
+ if (currentHeaderData[headerName]) {
+ headerList[headerName] = currentHeaderData[headerName].headerValue;
+ }
+ }
+
+ // WORKAROUND
+ if (headerList.cc == headerList.to) {
+ headerList.cc = "";
+ }
+
+ var hasAttachments = currentAttachments && currentAttachments.length;
+ var attachmentsEncrypted = true;
+
+ for (index in currentAttachments) {
+ if (!Enigmail.msg.checkEncryptedAttach(currentAttachments[index])) {
+ if (
+ !EnigmailMsgRead.checkSignedAttachment(
+ currentAttachments,
+ index,
+ currentAttachments
+ )
+ ) {
+ attachmentsEncrypted = false;
+ }
+ }
+ }
+
+ Enigmail.msg.decryptedMessage = {
+ url: messageUrl,
+ uri: msgUriSpec,
+ headerList,
+ hasAttachments,
+ attachmentsEncrypted,
+ charset,
+ plainText,
+ };
+
+ // don't display decrypted message if message selection has changed
+ displayedUriSpec = Enigmail.msg.getCurrentMsgUriSpec();
+ if (msgUriSpec && displayedUriSpec && displayedUriSpec != msgUriSpec) {
+ return;
+ }
+
+ // Create and load one-time message URI
+ var messageContent = Enigmail.msg.getDecryptedMessage(
+ "message/rfc822",
+ false
+ );
+
+ var node;
+ var bodyElement = Enigmail.msg.getBodyElement(pbMessageIndex);
+
+ if (bodyElement.firstChild) {
+ node = bodyElement.firstChild;
+
+ let divFound = false;
+
+ while (node) {
+ if (node.nodeName == "DIV") {
+ if (divFound) {
+ node.innerHTML = "";
+ } else {
+ // for safety reasons, we replace the complete visible message with
+ // the decrypted or signed part (bug 983)
+ divFound = true;
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(
+ EnigmailData.convertToUnicode(messageContent, charset)
+ );
+ Enigmail.msg.movePEPsubject();
+ }
+ }
+ node = node.nextSibling;
+ }
+
+ if (divFound) {
+ return;
+ }
+
+ let preFound = false;
+
+ // if no <DIV> node is found, try with <PRE> (bug 24762)
+ node = bodyElement.firstChild;
+ while (node) {
+ if (node.nodeName == "PRE") {
+ if (preFound) {
+ node.innerHTML = "";
+ } else {
+ preFound = true;
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(
+ EnigmailData.convertToUnicode(messageContent, charset)
+ );
+ Enigmail.msg.movePEPsubject();
+ }
+ }
+ node = node.nextSibling;
+ }
+
+ if (preFound) {
+ return;
+ }
+ }
+
+ EnigmailLog.ERROR(
+ "enigmailMessengerOverlay.js: no node found to replace message display\n"
+ );
+ },
+
+ importAttachedSenderKey() {
+ for (let info of Enigmail.msg.attachedSenderEmailKeysIndex) {
+ EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ [info.keyInfo],
+ Enigmail.msg.attachedKeys[info.idx],
+ true,
+ ["0x" + info.keyInfo.fpr]
+ );
+ }
+ },
+
+ async searchSignatureKey() {
+ let keyId = document
+ .getElementById("signatureKeyBox")
+ .getAttribute("keyid");
+ if (!keyId) {
+ return false;
+ }
+ return KeyLookupHelper.lookupAndImportByKeyID(
+ "interactive-import",
+ window,
+ keyId,
+ true
+ );
+ },
+
+ notifySigKeyMissing(keyId) {
+ Enigmail.msg.missingSigKey = keyId;
+ if (
+ Enigmail.msg.allAttachmentsDone &&
+ Enigmail.msg.messageDecryptDone &&
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed ==
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount
+ ) {
+ Enigmail.msg.unhideMissingSigKeyBox();
+ } else {
+ Enigmail.msg.unhideMissingSigKeyBoxIsTODO = true;
+ }
+ },
+
+ unhideMissingSigKeyBox() {
+ let sigKeyIsAttached = false;
+ for (let info of Enigmail.msg.attachedSenderEmailKeysIndex) {
+ if (info.keyInfo.keyId == Enigmail.msg.missingSigKey) {
+ sigKeyIsAttached = true;
+ break;
+ }
+ }
+ if (!sigKeyIsAttached) {
+ let b = document.getElementById("signatureKeyBox");
+ b.removeAttribute("hidden");
+ b.setAttribute("keyid", Enigmail.msg.missingSigKey);
+ }
+ },
+
+ async importKeyFromMsgBody(msgData) {
+ let beginIndexObj = {};
+ let endIndexObj = {};
+ let indentStrObj = {};
+ let blockType = EnigmailArmor.locateArmoredBlock(
+ msgData,
+ 0,
+ "",
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ );
+ if (!blockType || blockType !== "PUBLIC KEY BLOCK") {
+ return;
+ }
+
+ let keyData = msgData.substring(beginIndexObj.value, endIndexObj.value);
+
+ let errorMsgObj = {};
+ let preview = await EnigmailKey.getKeyListFromKeyBlock(
+ keyData,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ if (preview && errorMsgObj.value === "") {
+ EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ preview,
+ keyData,
+ false
+ );
+ } else {
+ document.l10n.formatValue("preview-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n" + errorMsgObj.value);
+ });
+ }
+ },
+
+ /**
+ * Extract the subject from the 1st content line and move it to the subject line
+ */
+ movePEPsubject() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: movePEPsubject:\n");
+
+ let bodyElement = this.getBodyElement();
+ if (
+ bodyElement.textContent.search(/^\r?\n?Subject: [^\r\n]+\r?\n\r?\n/i) ===
+ 0 &&
+ "subject" in currentHeaderData &&
+ currentHeaderData.subject.headerValue === "pEp"
+ ) {
+ let m = EnigmailMime.extractSubjectFromBody(bodyElement.textContent);
+ if (m) {
+ let node = bodyElement.firstChild;
+ let found = false;
+
+ while (!found && node) {
+ if (node.nodeName == "DIV") {
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(m.messageBody);
+ found = true;
+ }
+ node = node.nextSibling;
+ }
+
+ // if no <DIV> node is found, try with <PRE> (bug 24762)
+ node = bodyElement.firstChild;
+ while (!found && node) {
+ if (node.nodeName == "PRE") {
+ node.innerHTML = EnigmailFuncs.formatPlaintextMsg(m.messageBody);
+ found = true;
+ }
+ node = node.nextSibling;
+ }
+
+ Enigmail.hdrView.setSubject(m.subject);
+ }
+ }
+ },
+
+ /**
+ * Fix broken PGP/MIME messages from MS-Exchange by replacing the broken original
+ * message with a fixed copy.
+ *
+ * no return
+ */
+ async fixBuggyExchangeMail() {
+ EnigmailLog.DEBUG("enigmailMessengerOverlay.js: fixBuggyExchangeMail:\n");
+
+ this.notificationBox.appendNotification(
+ "brokenExchangeProgress",
+ {
+ label: await document.l10n.formatValue("openpgp-broken-exchange-wait"),
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+
+ let msg = gMessage;
+ EnigmailFixExchangeMsg.fixExchangeMessage(msg, this.buggyMailType)
+ .then(msgKey => {
+ // Display the new message which now has the key msgKey.
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: fixBuggyExchangeMail: _success: msgKey=" +
+ msgKey +
+ "\n"
+ );
+ // TODO: scope is about:message, and this doesn't work
+ // parent.gDBView.selectMsgByKey(msgKey);
+ // ReloadMessage();
+ })
+ .catch(async function (ex) {
+ console.debug(ex);
+ EnigmailDialog.alert(
+ window,
+ await l10n.formatValue("fix-broken-exchange-msg-failed")
+ );
+ });
+
+ // Remove the brokenExchangeProgress notification at the end of the process.
+ this.removeNotification("brokenExchangeProgress");
+ },
+
+ /**
+ * Hide attachments containing OpenPGP keys
+ */
+ hidePgpKeys() {
+ let keys = [];
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ currentAttachments[i].contentType.search(/^application\/pgp-keys/i) ===
+ 0
+ ) {
+ keys.push(i);
+ }
+ }
+
+ if (keys.length > 0) {
+ let attachmentList = document.getElementById("attachmentList");
+
+ for (let i = keys.length; i > 0; i--) {
+ currentAttachments.splice(keys[i - 1], 1);
+ }
+
+ if (attachmentList) {
+ // delete all keys from attachment list
+ while (attachmentList.firstChild) {
+ attachmentList.firstChild.remove();
+ }
+
+ // build new attachment list
+
+ /* global gBuildAttachmentsForCurrentMsg: true */
+ let orig = gBuildAttachmentsForCurrentMsg;
+ gBuildAttachmentsForCurrentMsg = false;
+ displayAttachmentsForExpandedView();
+ gBuildAttachmentsForCurrentMsg = orig;
+ }
+ }
+ },
+
+ // check if the attachment could be encrypted
+ checkEncryptedAttach(attachment) {
+ return (
+ EnigmailMsgRead.getAttachmentName(attachment).match(
+ /\.(gpg|pgp|asc)$/i
+ ) ||
+ (attachment.contentType.match(/^application\/pgp(-.*)?$/i) &&
+ attachment.contentType.search(/^application\/pgp-signature/i) < 0)
+ );
+ },
+
+ getDecryptedMessage(contentType, includeHeaders) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: getDecryptedMessage: " +
+ contentType +
+ ", " +
+ includeHeaders +
+ "\n"
+ );
+
+ if (!Enigmail.msg.decryptedMessage) {
+ return "No decrypted message found!\n";
+ }
+
+ var enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return "";
+ }
+
+ var headerList = Enigmail.msg.decryptedMessage.headerList;
+ var statusLine = Enigmail.msg.securityInfo
+ ? Enigmail.msg.securityInfo.statusLine
+ : "";
+ var contentData = "";
+ var headerName;
+
+ if (contentType == "message/rfc822") {
+ // message/rfc822
+
+ if (includeHeaders) {
+ try {
+ var msg = gMessage;
+ if (msg) {
+ let msgHdr = {
+ From: msg.author,
+ Subject: msg.subject,
+ To: msg.recipients,
+ Cc: msg.ccList,
+ Date: new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ }).format(new Date(msg.dateInSeconds * 1000)),
+ };
+
+ if (
+ msg?.folder?.flags & Ci.nsMsgFolderFlags.Newsgroup &&
+ currentHeaderData.newsgroups
+ ) {
+ msgHdr.Newsgroups = currentHeaderData.newsgroups.headerValue;
+ }
+
+ for (let headerName in msgHdr) {
+ if (msgHdr[headerName] && msgHdr[headerName].length > 0) {
+ contentData += headerName + ": " + msgHdr[headerName] + "\r\n";
+ }
+ }
+ }
+ } catch (ex) {
+ // the above seems to fail every now and then
+ // so, here is the fallback
+ for (let headerName in headerList) {
+ let headerValue = headerList[headerName];
+ contentData += headerName + ": " + headerValue + "\r\n";
+ }
+ }
+
+ contentData += "Content-Type: text/plain";
+
+ if (Enigmail.msg.decryptedMessage.charset) {
+ contentData += "; charset=" + Enigmail.msg.decryptedMessage.charset;
+ }
+
+ contentData += "\r\n";
+ }
+
+ contentData += "\r\n";
+
+ if (
+ Enigmail.msg.decryptedMessage.hasAttachments &&
+ !Enigmail.msg.decryptedMessage.attachmentsEncrypted
+ ) {
+ contentData += EnigmailData.convertFromUnicode(
+ l10n.formatValueSync("enig-content-note") + "\r\n\r\n",
+ Enigmail.msg.decryptedMessage.charset
+ );
+ }
+
+ contentData += Enigmail.msg.decryptedMessage.plainText;
+ } else {
+ // text/html or text/plain
+
+ if (contentType == "text/html") {
+ contentData +=
+ '<meta http-equiv="Content-Type" content="text/html; charset=' +
+ Enigmail.msg.decryptedMessage.charset +
+ '">\r\n';
+ contentData += "<html><head></head><body>\r\n";
+ }
+
+ if (statusLine) {
+ if (contentType == "text/html") {
+ contentData +=
+ EnigmailMsgRead.escapeTextForHTML(statusLine, false) +
+ "<br>\r\n<hr>\r\n";
+ } else {
+ contentData += statusLine + "\r\n\r\n";
+ }
+ }
+
+ if (includeHeaders) {
+ for (headerName in headerList) {
+ let headerValue = headerList[headerName];
+
+ if (headerValue) {
+ if (contentType == "text/html") {
+ contentData +=
+ "<b>" +
+ EnigmailMsgRead.escapeTextForHTML(headerName, false) +
+ ":</b> " +
+ EnigmailMsgRead.escapeTextForHTML(headerValue, false) +
+ "<br>\r\n";
+ } else {
+ contentData += headerName + ": " + headerValue + "\r\n";
+ }
+ }
+ }
+ }
+
+ if (contentType == "text/html") {
+ contentData +=
+ "<pre>" +
+ EnigmailMsgRead.escapeTextForHTML(
+ Enigmail.msg.decryptedMessage.plainText,
+ false
+ ) +
+ "</pre>\r\n";
+
+ contentData += "</body></html>\r\n";
+ } else {
+ contentData += "\r\n" + Enigmail.msg.decryptedMessage.plainText;
+ }
+
+ if (AppConstants.platform != "win") {
+ contentData = contentData.replace(/\r\n/g, "\n");
+ }
+ }
+
+ return contentData;
+ },
+
+ async msgDirectDecrypt(
+ interactive,
+ importOnly,
+ contentEncoding,
+ charset,
+ signature,
+ bufferSize,
+ head,
+ tail,
+ msgUriSpec,
+ msgDate,
+ callbackFunction,
+ isAuto
+ ) {
+ EnigmailLog.WRITE(
+ "enigmailMessengerOverlay.js: msgDirectDecrypt: contentEncoding=" +
+ contentEncoding +
+ ", signature=" +
+ signature +
+ "\n"
+ );
+ let mailNewsUrl = this.getCurrentMsgUrl();
+ if (!mailNewsUrl) {
+ return;
+ }
+
+ let PromiseStreamListener = function () {
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this._data = null;
+ this._stream = null;
+ };
+
+ PromiseStreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ onStartRequest(request) {
+ this.data = "";
+ this.inStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ },
+
+ onStopRequest(request, statusCode) {
+ if (statusCode != Cr.NS_OK) {
+ this._reject(`Streaming failed: ${statusCode}`);
+ return;
+ }
+
+ let start = this.data.indexOf("-----BEGIN PGP");
+ let end = this.data.indexOf("-----END PGP");
+
+ if (start >= 0 && end > start) {
+ let tStr = this.data.substr(end);
+ let n = tStr.indexOf("\n");
+ let r = tStr.indexOf("\r");
+ let lEnd = -1;
+ if (n >= 0 && r >= 0) {
+ lEnd = Math.min(r, n);
+ } else if (r >= 0) {
+ lEnd = r;
+ } else if (n >= 0) {
+ lEnd = n;
+ }
+
+ if (lEnd >= 0) {
+ end += lEnd;
+ }
+
+ let data = Enigmail.msg.trimIfEncrypted(
+ this.data.substring(start, end + 1)
+ );
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: data: >" + data.substr(0, 100) + "<\n"
+ );
+
+ let currentMsgURL = Enigmail.msg.getCurrentMsgUrl();
+ let urlSpec = currentMsgURL ? currentMsgURL.spec : "";
+
+ let l = urlSpec.length;
+ if (urlSpec.substr(0, l) != mailNewsUrl.spec.substr(0, l)) {
+ EnigmailLog.ERROR(
+ "enigmailMessengerOverlay.js: Message URL mismatch " +
+ currentMsgURL +
+ " vs. " +
+ urlSpec +
+ "\n"
+ );
+ this._reject(`Msg url mismatch: ${currentMsgURL} vs ${urlSpec}`);
+ return;
+ }
+
+ Enigmail.msg
+ .messageParseCallback(
+ data,
+ msgDate,
+ contentEncoding,
+ charset,
+ interactive,
+ importOnly,
+ mailNewsUrl.spec,
+ signature,
+ 3,
+ head,
+ tail,
+ msgUriSpec,
+ isAuto
+ )
+ .then(() => this._resolve(this.data));
+ }
+ },
+
+ onDataAvailable(request, stream, off, count) {
+ this.inStream.init(stream);
+ this.data += this.inStream.read(count);
+ },
+
+ get promise() {
+ return this._promise;
+ },
+ };
+
+ let streamListener = new PromiseStreamListener();
+ let msgSvc = MailServices.messageServiceFromURI(msgUriSpec);
+ msgSvc.streamMessage(
+ msgUriSpec,
+ streamListener,
+ top.msgWindow,
+ null,
+ false,
+ null,
+ false
+ );
+ await streamListener;
+ },
+
+ revealAttachments(index) {
+ if (!index) {
+ index = 0;
+ }
+
+ if (index < currentAttachments.length) {
+ this.handleAttachment(
+ "revealName/" + index.toString(),
+ currentAttachments[index]
+ );
+ }
+ },
+
+ /**
+ * Set up some event handlers for the attachment items in #attachmentList.
+ */
+ handleAttachmentEvent() {
+ let attList = document.getElementById("attachmentList");
+
+ for (let att of attList.itemChildren) {
+ att.addEventListener("click", this.attachmentItemClick.bind(this), true);
+ }
+ },
+
+ // handle a selected attachment (decrypt & open or save)
+ handleAttachmentSel(actionType) {
+ let contextMenu = document.getElementById("attachmentItemContext");
+ let anAttachment = contextMenu.attachments[0];
+
+ switch (actionType) {
+ case "saveAttachment":
+ case "openAttachment":
+ case "importKey":
+ case "revealName":
+ this.handleAttachment(actionType, anAttachment);
+ break;
+ case "verifySig":
+ this.verifyDetachedSignature(anAttachment);
+ break;
+ }
+ },
+
+ /**
+ * save the original file plus the signature file to disk and then verify the signature
+ */
+ async verifyDetachedSignature(anAttachment) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: verifyDetachedSignature: url=" +
+ anAttachment.url +
+ "\n"
+ );
+
+ var enigmailSvc = Enigmail.getEnigmailSvc();
+ if (!enigmailSvc) {
+ return;
+ }
+
+ var origAtt, signatureAtt;
+ var isEncrypted = false;
+
+ if (
+ EnigmailMsgRead.getAttachmentName(anAttachment).search(/\.sig$/i) > 0 ||
+ anAttachment.contentType.search(/^application\/pgp-signature/i) === 0
+ ) {
+ // we have the .sig file; need to know the original file;
+
+ signatureAtt = anAttachment;
+ var origName = EnigmailMsgRead.getAttachmentName(anAttachment).replace(
+ /\.sig$/i,
+ ""
+ );
+
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ origName == EnigmailMsgRead.getAttachmentName(currentAttachments[i])
+ ) {
+ origAtt = currentAttachments[i];
+ break;
+ }
+ }
+
+ if (!origAtt) {
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ origName ==
+ EnigmailMsgRead.getAttachmentName(currentAttachments[i]).replace(
+ /\.pgp$/i,
+ ""
+ )
+ ) {
+ isEncrypted = true;
+ origAtt = currentAttachments[i];
+ break;
+ }
+ }
+ }
+ } else {
+ // we have a supposedly original file; need to know the .sig file;
+
+ origAtt = anAttachment;
+ var attachName = EnigmailMsgRead.getAttachmentName(anAttachment);
+ var sigName = attachName + ".sig";
+
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ sigName == EnigmailMsgRead.getAttachmentName(currentAttachments[i])
+ ) {
+ signatureAtt = currentAttachments[i];
+ break;
+ }
+ }
+
+ if (!signatureAtt && attachName.search(/\.pgp$/i) > 0) {
+ sigName = attachName.replace(/\.pgp$/i, ".sig");
+ for (let i = 0; i < currentAttachments.length; i++) {
+ if (
+ sigName == EnigmailMsgRead.getAttachmentName(currentAttachments[i])
+ ) {
+ isEncrypted = true;
+ signatureAtt = currentAttachments[i];
+ break;
+ }
+ }
+ }
+ }
+
+ if (!signatureAtt) {
+ EnigmailDialog.alert(
+ window,
+ l10n.formatValueSync("attachment-no-match-to-signature", {
+ attachment: EnigmailMsgRead.getAttachmentName(origAtt),
+ })
+ );
+ return;
+ }
+ if (!origAtt) {
+ EnigmailDialog.alert(
+ window,
+ l10n.formatValueSync("attachment-no-match-from-signature", {
+ attachment: EnigmailMsgRead.getAttachmentName(signatureAtt),
+ })
+ );
+ return;
+ }
+
+ // open
+ var outFile1 = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ outFile1.append(EnigmailMsgRead.getAttachmentName(origAtt));
+ outFile1.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ let response = await fetch(origAtt.url);
+ if (!response.ok) {
+ throw new Error(`Bad response for url=${origAtt.url}`);
+ }
+ await IOUtils.writeUTF8(outFile1.path, await response.text());
+
+ if (isEncrypted) {
+ // Try to decrypt message if we suspect the message is encrypted.
+ // If it fails we will just verify the encrypted data.
+ let readBinaryFile = async () => {
+ let data = await IOUtils.read(outFile1.path);
+ return MailStringUtils.uint8ArrayToByteString(data);
+ };
+ await EnigmailDecryption.decryptAttachment(
+ window,
+ outFile1,
+ EnigmailMsgRead.getAttachmentName(origAtt),
+ readBinaryFile,
+ {},
+ {},
+ {}
+ );
+ }
+
+ var outFile2 = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ outFile2.append(EnigmailMsgRead.getAttachmentName(signatureAtt));
+ outFile2.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ let response2 = await fetch(signatureAtt.url);
+ if (!response2.ok) {
+ throw new Error(`Bad response for url=${signatureAtt.url}`);
+ }
+ await IOUtils.writeUTF8(outFile2.path, await response2.text());
+
+ let cApi = EnigmailCryptoAPI();
+ let promise = cApi.verifyAttachment(outFile1.path, outFile2.path);
+ promise.then(async function (message) {
+ EnigmailDialog.info(
+ window,
+ l10n.formatValueSync("signature-verified-ok", {
+ attachment: EnigmailMsgRead.getAttachmentName(origAtt),
+ }) +
+ "\n\n" +
+ message
+ );
+ });
+ promise.catch(async function (err) {
+ EnigmailDialog.alert(
+ window,
+ l10n.formatValueSync("signature-verify-failed", {
+ attachment: EnigmailMsgRead.getAttachmentName(origAtt),
+ }) +
+ "\n\n" +
+ err
+ );
+ });
+
+ outFile1.remove(false);
+ outFile2.remove(false);
+ },
+
+ handleAttachment(actionType, attachment) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: handleAttachment: actionType=" +
+ actionType +
+ ", attachment(url)=" +
+ attachment.url +
+ "\n"
+ );
+
+ let bufferListener = EnigmailStreams.newStringStreamListener(async data => {
+ Enigmail.msg.decryptAttachmentCallback([
+ {
+ actionType,
+ attachment,
+ forceBrowser: false,
+ data,
+ },
+ ]);
+ });
+ let msgUri = Services.io.newURI(attachment.url);
+ let channel = EnigmailStreams.createChannel(msgUri);
+ channel.asyncOpen(bufferListener, msgUri);
+ },
+
+ setAttachmentName(attachment, newLabel, index) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: setAttachmentName (" + newLabel + "):\n"
+ );
+
+ var attList = document.getElementById("attachmentList");
+ if (attList) {
+ var attNode = attList.firstChild;
+ while (attNode) {
+ if (attNode.getAttribute("name") == attachment.name) {
+ attNode.setAttribute("name", newLabel);
+ }
+ attNode = attNode.nextSibling;
+ }
+ }
+
+ if (typeof attachment.displayName == "undefined") {
+ attachment.name = newLabel;
+ } else {
+ attachment.displayName = newLabel;
+ }
+
+ if (index && index.length > 0) {
+ this.revealAttachments(parseInt(index, 10) + 1);
+ }
+ },
+
+ async decryptAttachmentCallback(cbArray) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: decryptAttachmentCallback:\n"
+ );
+
+ var callbackArg = cbArray[0];
+
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var errorMsgObj = {};
+ var exitStatus = -1;
+
+ var outFile;
+ var origFilename;
+ var rawFileName = EnigmailMsgRead.getAttachmentName(
+ callbackArg.attachment
+ ).replace(/\.(asc|pgp|gpg)$/i, "");
+
+ // TODO: We don't have code yet to extract the original filename
+ // from an encrypted data block.
+ /*
+ if (callbackArg.actionType != "importKey") {
+ let cApi = EnigmailCryptoAPI();
+ let origFilename = await cApi.getFileName(window, callbackArg.data);
+ if (origFilename && origFilename.length > rawFileName.length) {
+ rawFileName = origFilename;
+ }
+ }
+ */
+
+ if (callbackArg.actionType == "saveAttachment") {
+ outFile = EnigmailDialog.filePicker(
+ window,
+ l10n.formatValueSync("save-attachment-header"),
+ Enigmail.msg.lastSaveDir,
+ true,
+ false,
+ "",
+ rawFileName,
+ null
+ );
+ if (!outFile) {
+ return;
+ }
+ } else if (callbackArg.actionType.substr(0, 10) == "revealName") {
+ if (origFilename && origFilename.length > 0) {
+ Enigmail.msg.setAttachmentName(
+ callbackArg.attachment,
+ origFilename + ".pgp",
+ callbackArg.actionType.substr(11, 10)
+ );
+ }
+ Enigmail.msg.setAttachmentReveal(null);
+ return;
+ } else {
+ // open
+ outFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ outFile.append(rawFileName);
+ outFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+
+ if (callbackArg.actionType == "importKey") {
+ var preview = await EnigmailKey.getKeyListFromKeyBlock(
+ callbackArg.data,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+
+ if (errorMsgObj.value !== "" || !preview || preview.length === 0) {
+ // try decrypting the attachment
+ exitStatus = await EnigmailDecryption.decryptAttachment(
+ window,
+ outFile,
+ EnigmailMsgRead.getAttachmentName(callbackArg.attachment),
+ callbackArg.data,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ );
+ if (exitStatus && exitCodeObj.value === 0) {
+ // success decrypting, let's try again
+ callbackArg.data = String.fromCharCode(
+ ...(await IOUtils.read(outFile.path))
+ );
+ preview = await EnigmailKey.getKeyListFromKeyBlock(
+ callbackArg.data,
+ errorMsgObj,
+ true,
+ true,
+ false
+ );
+ }
+ }
+
+ if (preview && errorMsgObj.value === "") {
+ EnigmailKeyRing.importKeyDataWithConfirmation(
+ window,
+ preview,
+ callbackArg.data,
+ false
+ );
+ } else {
+ document.l10n.formatValue("preview-failed").then(value => {
+ EnigmailDialog.alert(window, value + "\n" + errorMsgObj.value);
+ });
+ }
+ outFile.remove(true);
+ return;
+ }
+
+ exitStatus = await EnigmailDecryption.decryptAttachment(
+ window,
+ outFile,
+ EnigmailMsgRead.getAttachmentName(callbackArg.attachment),
+ callbackArg.data,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ );
+
+ if (!exitStatus || exitCodeObj.value !== 0) {
+ exitStatus = false;
+ if (
+ statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY &&
+ statusFlagsObj.value & EnigmailConstants.UNCERTAIN_SIGNATURE
+ ) {
+ if (callbackArg.actionType == "openAttachment") {
+ let [title, button] = await document.l10n.formatValues([
+ { id: "decrypt-ok-no-sig" },
+ { id: "msg-ovl-button-cont-anyway" },
+ ]);
+
+ exitStatus = EnigmailDialog.confirmDlg(window, title, button);
+ } else {
+ EnigmailDialog.info(
+ window,
+ await document.l10n.formatValue("decrypt-ok-no-sig")
+ );
+ }
+ } else {
+ let msg = await document.l10n.formatValue("failed-decrypt");
+ if (errorMsgObj.errorMsg) {
+ msg += "\n\n" + errorMsgObj.errorMsg;
+ }
+ EnigmailDialog.info(window, msg);
+ exitStatus = false;
+ }
+ }
+ if (exitStatus) {
+ if (statusFlagsObj.value & EnigmailConstants.IMPORTED_KEY) {
+ if (exitCodeObj.keyList) {
+ let importKeyList = exitCodeObj.keyList.map(function (a) {
+ return a.id;
+ });
+ EnigmailDialog.keyImportDlg(window, importKeyList);
+ }
+ } else if (statusFlagsObj.value & EnigmailConstants.DISPLAY_MESSAGE) {
+ HandleSelectedAttachments("open");
+ } else if (
+ statusFlagsObj.value & EnigmailConstants.DISPLAY_MESSAGE ||
+ callbackArg.actionType == "openAttachment"
+ ) {
+ var ioServ = Services.io;
+ var outFileUri = ioServ.newFileURI(outFile);
+ var fileExt = outFile.leafName.replace(/(.*\.)(\w+)$/, "$2");
+ if (fileExt && !callbackArg.forceBrowser) {
+ var extAppLauncher = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsPIExternalAppLauncher);
+ extAppLauncher.deleteTemporaryFileOnExit(outFile);
+
+ try {
+ var mimeService = Cc["@mozilla.org/mime;1"].getService(
+ Ci.nsIMIMEService
+ );
+ var fileMimeType = mimeService.getTypeFromFile(outFile);
+ var fileMimeInfo = mimeService.getFromTypeAndExtension(
+ fileMimeType,
+ fileExt
+ );
+
+ fileMimeInfo.launchWithFile(outFile);
+ } catch (ex) {
+ // if the attachment file type is unknown, an exception is thrown,
+ // so let it be handled by a browser window
+ Enigmail.msg.loadExternalURL(outFileUri.asciiSpec);
+ }
+ } else {
+ // open the attachment using an external application
+ Enigmail.msg.loadExternalURL(outFileUri.asciiSpec);
+ }
+ }
+ }
+ },
+
+ loadExternalURL(url) {
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadURI(Services.io.newURI(url));
+ },
+
+ // retrieves the most recent navigator window (opens one if need be)
+ loadURLInNavigatorWindow(url, aOpenFlag) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: loadURLInNavigatorWindow: " +
+ url +
+ ", " +
+ aOpenFlag +
+ "\n"
+ );
+
+ var navWindow;
+
+ // if this is a browser window, just use it
+ if ("document" in top) {
+ var possibleNavigator = top.document.getElementById("main-window");
+ if (
+ possibleNavigator &&
+ possibleNavigator.getAttribute("windowtype") == "navigator:browser"
+ ) {
+ navWindow = top;
+ }
+ }
+
+ // if not, get the most recently used browser window
+ if (!navWindow) {
+ var wm = Services.wm;
+ navWindow = wm.getMostRecentWindow("navigator:browser");
+ }
+
+ if (navWindow) {
+ if ("fixupAndLoadURIString" in navWindow) {
+ navWindow.fixupAndLoadURIString(url);
+ } else {
+ navWindow._content.location.href = url;
+ }
+ } else if (aOpenFlag) {
+ // if no browser window available and it's ok to open a new one, do so
+ navWindow = window.open(url, "Enigmail");
+ }
+
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: loadURLInNavigatorWindow: navWindow=" +
+ navWindow +
+ "\n"
+ );
+
+ return navWindow;
+ },
+
+ /**
+ * Open an encrypted attachment item.
+ */
+ attachmentItemClick(event) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: attachmentItemClick: event=" + event + "\n"
+ );
+
+ let attachment = event.currentTarget.attachment;
+ if (this.checkEncryptedAttach(attachment)) {
+ if (event.button === 0 && event.detail == 2) {
+ // double click
+ this.handleAttachment("openAttachment", attachment);
+ event.stopPropagation();
+ }
+ }
+ },
+
+ // decrypted and copy/move all selected messages in a target folder
+
+ async decryptToFolder(destFolder, move) {
+ let msgHdrs = gDBView.getSelectedMsgHdrs();
+ if (!msgHdrs || msgHdrs.length === 0) {
+ return;
+ }
+
+ let total = msgHdrs.length;
+ let failures = 0;
+ for (let msgHdr of msgHdrs) {
+ await EnigmailPersistentCrypto.cryptMessage(
+ msgHdr,
+ destFolder.URI,
+ move,
+ false
+ ).catch(err => {
+ failures++;
+ });
+ }
+
+ if (failures) {
+ let info = await document.l10n.formatValue(
+ "decrypt-and-copy-failures-multiple",
+ {
+ failures,
+ total,
+ }
+ );
+ Services.prompt.alert(null, document.title, info);
+ }
+ },
+
+ async searchKeysOnInternet(event) {
+ return KeyLookupHelper.lookupAndImportByEmail(
+ "interactive-import",
+ window,
+ event.currentTarget.parentNode.headerField?.emailAddress,
+ true
+ );
+ },
+
+ onUnloadEnigmail() {
+ window.removeEventListener("unload", Enigmail.msg.messengerClose);
+ window.removeEventListener(
+ "unload-enigmail",
+ Enigmail.msg.onUnloadEnigmail
+ );
+ window.removeEventListener("load-enigmail", Enigmail.msg.messengerStartup);
+
+ this.messageCleanup();
+
+ if (this.messagePane) {
+ this.messagePane.removeEventListener(
+ "unload",
+ Enigmail.msg.messageFrameUnload,
+ true
+ );
+ }
+
+ for (let c of this.changedAttributes) {
+ let elem = document.getElementById(c.id);
+ if (elem) {
+ elem.setAttribute(c.attrib, c.value);
+ }
+ }
+
+ this.messengerClose();
+
+ if (Enigmail.columnHandler) {
+ Enigmail.columnHandler.onUnloadEnigmail();
+ }
+ if (Enigmail.hdrView) {
+ Enigmail.hdrView.onUnloadEnigmail();
+ }
+
+ // eslint-disable-next-line no-global-assign
+ Enigmail = undefined;
+ },
+
+ /**
+ * Process key data from a message.
+ *
+ * @param {string} keyData - The key data.
+ * @param {boolean} isBinaryAutocrypt - false if ASCII armored data.
+ * @param {string} [description] - Key source description, if any.
+ */
+ async commonProcessAttachedKey(keyData, isBinaryAutocrypt, description) {
+ if (!keyData) {
+ return;
+ }
+
+ // Processing is slow for some types of keys.
+ // We want to avoid automatic key import/updates for users who
+ // have OpenPGP disabled (no account has an OpenPGP key configured).
+ if (
+ !MailServices.accounts.allIdentities.find(id =>
+ id.getUnicharAttribute("openpgp_key_id")
+ )
+ ) {
+ return;
+ }
+
+ let errorMsgObj = {};
+ let preview = await EnigmailKey.getKeyListFromKeyBlock(
+ keyData,
+ errorMsgObj,
+ true,
+ true,
+ false,
+ true
+ );
+
+ // If we cannot analyze the keyblock, or if it's empty, or if we
+ // got an error message, then the key is bad and shouldn't be used.
+ if (!preview || !preview.length || errorMsgObj.value) {
+ return;
+ }
+
+ this.fetchParticipants();
+
+ for (let newKey of preview) {
+ let oldKey = EnigmailKeyRing.getKeyById(newKey.fpr);
+ if (!oldKey) {
+ // If the key is unknown, an expired key cannot help us
+ // for anything new, so don't use it.
+ if (newKey.keyTrust == "e") {
+ continue;
+ }
+
+ // Potentially merge the revocation into CollectedKeysDB, it if
+ // already has that key.
+ if (newKey.keyTrust == "r") {
+ let db = await CollectedKeysDB.getInstance();
+ let existing = await db.findKeyForFingerprint(newKey.fpr);
+ if (existing) {
+ let key = await db.mergeExisting(newKey, newKey.pubKey, {
+ uri: `mid:${gMessage.messageId}`,
+ type: isBinaryAutocrypt ? "autocrypt" : "attachment",
+ description,
+ });
+ await db.storeKey(key);
+ Services.obs.notifyObservers(null, "openpgp-key-change");
+ }
+ continue;
+ }
+
+ // It doesn't make sense to import a public key,
+ // if we have a secret key for that email address.
+ // Because, if we are the owner of that email address, why would
+ // we need a public key referring to our own email address,
+ // sent to us by someone else?
+
+ let keyInOurName = false;
+ for (let userId of newKey.userIds) {
+ if (userId.type !== "uid") {
+ continue;
+ }
+ if (EnigmailTrust.isInvalid(userId.keyTrust)) {
+ continue;
+ }
+ if (
+ await EnigmailKeyRing.hasSecretKeyForEmail(
+ EnigmailFuncs.getEmailFromUserID(userId.userId).toLowerCase()
+ )
+ ) {
+ keyInOurName = true;
+ break;
+ }
+ }
+ if (keyInOurName) {
+ continue;
+ }
+
+ // Only advertise the key for import if it contains a user ID
+ // that points to the email author email address.
+ let relatedParticipantEmailAddress = null;
+ if (this.hasUserIdForEmail(newKey.userIds, this.authorEmail)) {
+ relatedParticipantEmailAddress = this.authorEmail;
+ }
+
+ if (relatedParticipantEmailAddress) {
+ // If it's a non expired, non revoked new key, in the email
+ // author's name (email address match), then offer it for
+ // manual (immediate) import.
+ let nextIndex = Enigmail.msg.attachedKeys.length;
+ let info = {
+ fpr: "0x" + newKey.fpr,
+ idx: nextIndex,
+ keyInfo: newKey,
+ binary: isBinaryAutocrypt,
+ };
+ Enigmail.msg.attachedSenderEmailKeysIndex.push(info);
+ Enigmail.msg.attachedKeys.push(newKey.pubKey);
+ }
+
+ // We want to collect keys for potential later use, however,
+ // we also want to avoid that an attacker can send us a large
+ // number of keys to poison our cache, so we only collect keys
+ // that are related to the author or one of the recipients.
+ // Also, we don't want a public key, if we already have a
+ // secret key for that email address.
+
+ if (!relatedParticipantEmailAddress) {
+ // Not related to the author
+ for (let toOrCc of this.toAndCCSet) {
+ if (this.hasUserIdForEmail(newKey.userIds, toOrCc)) {
+ // Might be ok to import, so remember to which email
+ // the key is related and leave the loop.
+ relatedParticipantEmailAddress = toOrCc;
+ break;
+ }
+ }
+ }
+
+ if (relatedParticipantEmailAddress) {
+ // It seems OK to import, however, don't import yet.
+ // Wait until after we have processed all attachments to
+ // the current message. Because we don't want to import
+ // multiple keys for the same email address, that wouldn't
+ // make sense. Remember the import candidate, and postpone
+ // until we are done looking at all attachments.
+
+ if (this.keyCollectCandidates.has(relatedParticipantEmailAddress)) {
+ // The email contains more than one public key for this
+ // email address.
+ this.keyCollectCandidates.set(relatedParticipantEmailAddress, {
+ skip: true,
+ });
+ } else {
+ let candidate = {};
+ candidate.skip = false;
+ candidate.newKeyObj = newKey;
+ candidate.pubKey = newKey.pubKey;
+ candidate.source = {
+ uri: `mid:${gMessage.messageId}`,
+ type: isBinaryAutocrypt ? "autocrypt" : "attachment",
+ description,
+ };
+ this.keyCollectCandidates.set(
+ relatedParticipantEmailAddress,
+ candidate
+ );
+ }
+ }
+
+ // done with processing for new keys (!oldKey)
+ continue;
+ }
+
+ // The key is known (we have an oldKey), then it makes sense to
+ // import, even if it's expired/revoked, to learn about the
+ // changed validity.
+
+ // Also, we auto import/merge such keys, even if the sender
+ // doesn't match any key user ID. Why is this useful?
+ // If I am Alice, and the email is from Bob, the email could have
+ // Charlie's revoked or extended key attached. It's useful for
+ // me to learn that.
+
+ // User IDs are another reason. The key might contain a new
+ // additional user ID, or a revoked user ID.
+ // That's relevant for Autocrypt headers, which only have one user
+ // ID. If we had imported the key with just one user ID in the
+ // past, and now we're being sent the same key for a different
+ // user ID, we must not skip it, even if it the validity is the
+ // same.
+ // Let's update on all possible changes of the user ID list,
+ // additions, removals, differences.
+
+ let shouldUpdate = false;
+
+ // new validity?
+ if (
+ oldKey.expiryTime < newKey.expiryTime ||
+ (oldKey.keyTrust != "r" && newKey.keyTrust == "r")
+ ) {
+ shouldUpdate = true;
+ } else if (
+ oldKey.userIds.length != newKey.userIds.length ||
+ !oldKey.userIds.every((el, ix) => el === newKey.userIds[ix])
+ ) {
+ shouldUpdate = true;
+ }
+
+ if (!shouldUpdate) {
+ continue;
+ }
+
+ if (
+ !(await EnigmailKeyRing.importKeyDataSilent(
+ window,
+ newKey.pubKey,
+ isBinaryAutocrypt,
+ "0x" + newKey.fpr
+ ))
+ ) {
+ console.debug(
+ "EnigmailKeyRing.importKeyDataSilent failed 0x" + newKey.fpr
+ );
+ }
+ }
+ },
+
+ /**
+ * Show the import key notification.
+ */
+ async unhideImportKeyBox() {
+ Enigmail.hdrView.notifyHasKeyAttached();
+ document.getElementById("openpgpKeyBox").removeAttribute("hidden");
+
+ // Check if the proposed key to import was previously accepted.
+ let hasAreadyAcceptedOther =
+ await PgpSqliteDb2.hasAnyPositivelyAcceptedKeyForEmail(
+ Enigmail.msg.authorEmail
+ );
+ if (hasAreadyAcceptedOther) {
+ Enigmail.msg.notificationBox.appendNotification(
+ "hasConflictingKeyOpenPGP",
+ {
+ label: await document.l10n.formatValue("openpgp-be-careful-new-key", {
+ email: Enigmail.msg.authorEmail,
+ }),
+ priority: Enigmail.msg.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ }
+ },
+
+ /*
+ * This function is called from several places. Any call may trigger
+ * the final processing for this message, it depends on the amount
+ * of attachments present, and whether we decrypt immediately, or
+ * after a delay (for inline encryption).
+ */
+ async processAfterAttachmentsAndDecrypt() {
+ // Return early if message processing isn't ready yet.
+ if (!Enigmail.msg.allAttachmentsDone || !Enigmail.msg.messageDecryptDone) {
+ return;
+ }
+
+ // Return early if we haven't yet processed all attachments.
+ if (
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed <
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount
+ ) {
+ return;
+ }
+
+ if (Enigmail.msg.unhideMissingSigKeyBoxIsTODO) {
+ Enigmail.msg.unhideMissingSigKeyBox();
+ }
+
+ // We have already processed all attached pgp-keys, we're ready
+ // to make final decisions on how to notify the user about
+ // available or missing keys.
+ // If we already found a good key for the sender's email
+ // in attachments, then don't look at the autocrypt header.
+ if (Enigmail.msg.attachedSenderEmailKeysIndex.length) {
+ this.unhideImportKeyBox();
+ } else if (
+ Enigmail.msg.savedHeaders &&
+ "autocrypt" in Enigmail.msg.savedHeaders &&
+ Enigmail.msg.savedHeaders.autocrypt.length > 0 &&
+ "from" in currentHeaderData
+ ) {
+ let fromAddr = EnigmailFuncs.stripEmail(
+ currentHeaderData.from.headerValue
+ ).toLowerCase();
+ // There might be multiple headers, we only want the one
+ // matching the sender's address.
+ for (let ac of Enigmail.msg.savedHeaders.autocrypt) {
+ let acAddr = MimeParser.getParameter(ac, "addr");
+ if (fromAddr == acAddr) {
+ let senderAutocryptKey;
+ try {
+ senderAutocryptKey = atob(
+ MimeParser.getParameter(ac.replace(/ /g, ""), "keydata")
+ );
+ } catch {}
+ if (senderAutocryptKey) {
+ // Make sure to let the message load before doing potentially *very*
+ // time consuming auto processing (seconds!?).
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+ await this.commonProcessAttachedKey(senderAutocryptKey, true);
+
+ if (Enigmail.msg.attachedSenderEmailKeysIndex.length) {
+ this.unhideImportKeyBox();
+ }
+ }
+ }
+ }
+ }
+
+ for (let gossipKey of EnigmailSingletons.lastDecryptedMessage.gossip) {
+ await this.commonProcessAttachedKey(gossipKey, true);
+ }
+
+ if (this.keyCollectCandidates && this.keyCollectCandidates.size) {
+ let db = await CollectedKeysDB.getInstance();
+
+ for (let candidate of this.keyCollectCandidates.values()) {
+ if (candidate.skip) {
+ continue;
+ }
+ // If key is known in the db: merge + update.
+ let key = await db.mergeExisting(
+ candidate.newKeyObj,
+ candidate.pubKey,
+ candidate.source
+ );
+
+ await db.storeKey(key);
+ Services.obs.notifyObservers(null, "openpgp-key-change");
+ }
+ }
+
+ // Should we notify the user about available encrypted nested parts,
+ // which have not been automatically decrypted?
+ if (
+ EnigmailSingletons.isRecentUriWithNestedEncryptedPart(
+ Enigmail.msg.getCurrentMsgUriSpec()
+ )
+ ) {
+ let buttons = [
+ {
+ "l10n-id": "openpgp-show-encrypted-parts",
+ popup: null,
+ callback(notification, button) {
+ top.viewEncryptedPart(Enigmail.msg.getCurrentMsgUriSpec());
+ return true; // keep notification
+ },
+ },
+ ];
+
+ Enigmail.msg.notificationBox.appendNotification(
+ "hasNestedEncryptedParts",
+ {
+ label: await document.l10n.formatValue(
+ "openpgp-has-nested-encrypted-parts"
+ ),
+ priority: Enigmail.msg.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ buttons
+ );
+ }
+
+ document.dispatchEvent(
+ new CustomEvent("openpgpprocessed", {
+ detail: { messageDecryptDone: true },
+ })
+ );
+ },
+
+ async notifyEndAllAttachments() {
+ Enigmail.msg.allAttachmentsDone = true;
+
+ if (!Enigmail.msg.autoProcessPgpKeyAttachmentCount) {
+ await Enigmail.msg.processAfterAttachmentsAndDecrypt();
+ }
+ },
+
+ toAndCCSet: null,
+ authorEmail: "",
+
+ // Used to remember the list of keys that we might want to add to
+ // our cache of seen keys. Will be used after we are done looking
+ // at all attachments.
+ keyCollectCandidates: new Map(),
+
+ attachedKeys: [],
+ attachedSenderEmailKeysIndex: [], // each: {idx (to-attachedKeys), keyInfo, binary}
+
+ fetchParticipants() {
+ if (this.toAndCCSet) {
+ return;
+ }
+
+ // toAndCCSet non-null indicates that we already fetched.
+ this.toAndCCSet = new Set();
+
+ // This message may have already disappeared.
+ if (!gMessage) {
+ return;
+ }
+
+ let addresses = MailServices.headerParser.parseEncodedHeader(
+ gMessage.author
+ );
+ if (addresses.length) {
+ this.authorEmail = addresses[0].email.toLowerCase();
+ }
+
+ addresses = MailServices.headerParser.parseEncodedHeader(
+ gMessage.recipients + "," + gMessage.ccList
+ );
+ for (let addr of addresses) {
+ this.toAndCCSet.add(addr.email.toLowerCase());
+ }
+ },
+
+ hasUserIdForEmail(userIds, authorEmail) {
+ authorEmail = authorEmail.toLowerCase();
+
+ for (let id of userIds) {
+ if (id.type !== "uid") {
+ continue;
+ }
+
+ if (
+ EnigmailFuncs.getEmailFromUserID(id.userId).toLowerCase() == authorEmail
+ ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ autoProcessPgpKeyAttachmentTransactionID: 0,
+ autoProcessPgpKeyAttachmentCount: 0,
+ autoProcessPgpKeyAttachmentProcessed: 0,
+ unhideMissingSigKeyBoxIsTODO: false,
+ unhideMissingSigKey: null,
+
+ autoProcessPgpKeyAttachment(attachment) {
+ if (
+ attachment.contentType != "application/pgp-keys" &&
+ !attachment.name.endsWith(".asc")
+ ) {
+ return;
+ }
+
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount++;
+
+ let bufferListener = EnigmailStreams.newStringStreamListener(async data => {
+ // Make sure to let the message load before doing potentially *very*
+ // time consuming auto processing (seconds!?).
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+ await this.commonProcessAttachedKey(data, false, attachment.name);
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed++;
+ if (
+ Enigmail.msg.autoProcessPgpKeyAttachmentProcessed ==
+ Enigmail.msg.autoProcessPgpKeyAttachmentCount
+ ) {
+ await Enigmail.msg.processAfterAttachmentsAndDecrypt();
+ }
+ });
+ let msgUri = Services.io.newURI(attachment.url);
+ let channel = EnigmailStreams.createChannel(msgUri);
+ channel.asyncOpen(bufferListener, msgUri);
+ },
+
+ /**
+ * Populate the message security popup panel with OpenPGP data.
+ */
+ async loadOpenPgpMessageSecurityInfo() {
+ let sigInfoWithDateLabel = null;
+ let sigInfoLabel = null;
+ let sigInfo = null;
+ let sigClass = null;
+ let wantToShowDate = false;
+
+ // All scenarios that set wantToShowDate to true should set both
+ // sigInfoWithDateLabel and sigInfoLabel, to ensure we have a
+ // fallback label, if the date is unavailable.
+ switch (Enigmail.hdrView.msgSignatureState) {
+ case EnigmailConstants.MSG_SIG_NONE:
+ sigInfoLabel = "openpgp-no-sig";
+ sigClass = "none";
+ sigInfo = "openpgp-no-sig-info";
+ break;
+
+ case EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_UNAVAILABLE:
+ sigInfoLabel = "openpgp-uncertain-sig";
+ sigClass = "unknown";
+ sigInfo = "openpgp-sig-uncertain-no-key";
+ break;
+
+ case EnigmailConstants.MSG_SIG_UNCERTAIN_UID_MISMATCH:
+ sigInfoLabel = "openpgp-uncertain-sig";
+ sigInfoWithDateLabel = "openpgp-uncertain-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "mismatch";
+ sigInfo = "openpgp-sig-uncertain-uid-mismatch";
+ break;
+
+ case EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_NOT_ACCEPTED:
+ sigInfoLabel = "openpgp-uncertain-sig";
+ sigInfoWithDateLabel = "openpgp-uncertain-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "unknown";
+ sigInfo = "openpgp-sig-uncertain-not-accepted";
+ break;
+
+ case EnigmailConstants.MSG_SIG_INVALID_KEY_REJECTED:
+ sigInfoLabel = "openpgp-invalid-sig";
+ sigInfoWithDateLabel = "openpgp-invalid-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "mismatch";
+ sigInfo = "openpgp-sig-invalid-rejected";
+ break;
+
+ case EnigmailConstants.MSG_SIG_INVALID:
+ sigInfoLabel = "openpgp-invalid-sig";
+ sigInfoWithDateLabel = "openpgp-invalid-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "mismatch";
+ sigInfo = "openpgp-sig-invalid-technical-problem";
+ break;
+
+ case EnigmailConstants.MSG_SIG_VALID_KEY_UNVERIFIED:
+ sigInfoLabel = "openpgp-good-sig";
+ sigInfoWithDateLabel = "openpgp-good-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "unverified";
+ sigInfo = "openpgp-sig-valid-unverified";
+ break;
+
+ case EnigmailConstants.MSG_SIG_VALID_KEY_VERIFIED:
+ sigInfoLabel = "openpgp-good-sig";
+ sigInfoWithDateLabel = "openpgp-good-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "verified";
+ sigInfo = "openpgp-sig-valid-verified";
+ break;
+
+ case EnigmailConstants.MSG_SIG_VALID_SELF:
+ sigInfoLabel = "openpgp-good-sig";
+ sigInfoWithDateLabel = "openpgp-good-sig-with-date";
+ wantToShowDate = true;
+ sigClass = "ok";
+ sigInfo = "openpgp-sig-valid-own-key";
+ break;
+
+ default:
+ console.error(
+ "Unexpected msgSignatureState: " + Enigmail.hdrView.msgSignatureState
+ );
+ }
+
+ let signatureLabel = document.getElementById("signatureLabel");
+ if (wantToShowDate && Enigmail.hdrView.msgSignatureDate) {
+ let date = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ }).format(Enigmail.hdrView.msgSignatureDate);
+ document.l10n.setAttributes(signatureLabel, sigInfoWithDateLabel, {
+ date,
+ });
+ } else {
+ document.l10n.setAttributes(signatureLabel, sigInfoLabel);
+ }
+
+ // Remove the second class to properly update the signature icon.
+ signatureLabel.classList.remove(signatureLabel.classList.item(1));
+ signatureLabel.classList.add(sigClass);
+
+ let signatureExplanation = document.getElementById("signatureExplanation");
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ signatureExplanation.textContent = await document.l10n.formatValue(sigInfo);
+
+ let encInfoLabel = null;
+ let encInfo = null;
+ let encClass = null;
+
+ switch (Enigmail.hdrView.msgEncryptionState) {
+ case EnigmailConstants.MSG_ENC_NONE:
+ encInfoLabel = "openpgp-enc-none";
+ encInfo = "openpgp-enc-none-label";
+ encClass = "none";
+ break;
+
+ case EnigmailConstants.MSG_ENC_NO_SECRET_KEY:
+ encInfoLabel = "openpgp-enc-invalid-label";
+ encInfo = "openpgp-enc-invalid";
+ encClass = "notok";
+ break;
+
+ case EnigmailConstants.MSG_ENC_FAILURE:
+ encInfoLabel = "openpgp-enc-invalid-label";
+ encInfo = "openpgp-enc-clueless";
+ encClass = "notok";
+ break;
+
+ case EnigmailConstants.MSG_ENC_OK:
+ encInfoLabel = "openpgp-enc-valid-label";
+ encInfo = "openpgp-enc-valid";
+ encClass = "ok";
+ break;
+
+ default:
+ console.error(
+ "Unexpected msgEncryptionState: " +
+ Enigmail.hdrView.msgEncryptionState
+ );
+ }
+
+ document.getElementById("techLabel").textContent = "- OpenPGP";
+
+ let encryptionLabel = document.getElementById("encryptionLabel");
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ encryptionLabel.textContent = await document.l10n.formatValue(encInfoLabel);
+
+ // Remove the second class to properly update the encryption icon.
+ encryptionLabel.classList.remove(encryptionLabel.classList.item(1));
+ encryptionLabel.classList.add(encClass);
+
+ document.getElementById("encryptionExplanation").textContent =
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ await document.l10n.formatValue(encInfo);
+
+ if (Enigmail.hdrView.msgSignatureKeyId) {
+ let sigKeyInfo = EnigmailKeyRing.getKeyById(
+ Enigmail.hdrView.msgSignatureKeyId
+ );
+
+ document.getElementById("signatureKey").collapsed = false;
+
+ if (
+ sigKeyInfo &&
+ sigKeyInfo.keyId != Enigmail.hdrView.msgSignatureKeyId
+ ) {
+ document.l10n.setAttributes(
+ document.getElementById("signatureKeyId"),
+ "openpgp-sig-key-id-with-subkey-id",
+ {
+ key: `0x${sigKeyInfo.keyId}`,
+ subkey: `0x${Enigmail.hdrView.msgSignatureKeyId}`,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("signatureKeyId"),
+ "openpgp-sig-key-id",
+ {
+ key: `0x${Enigmail.hdrView.msgSignatureKeyId}`,
+ }
+ );
+ }
+
+ if (sigKeyInfo) {
+ document.getElementById("viewSignatureKey").collapsed = false;
+ gSigKeyId = Enigmail.hdrView.msgSignatureKeyId;
+ }
+ }
+
+ let myIdToSkipInList;
+ if (
+ Enigmail.hdrView.msgEncryptionKeyId &&
+ Enigmail.hdrView.msgEncryptionKeyId.keyId
+ ) {
+ myIdToSkipInList = Enigmail.hdrView.msgEncryptionKeyId.keyId;
+
+ // If we were given a separate primaryKeyId, it means keyId is a subkey.
+ let havePrimaryId = !!Enigmail.hdrView.msgEncryptionKeyId.primaryKeyId;
+ document.getElementById("encryptionKey").collapsed = false;
+
+ if (havePrimaryId) {
+ document.l10n.setAttributes(
+ document.getElementById("encryptionKeyId"),
+ "openpgp-enc-key-with-subkey-id",
+ {
+ key: `0x${Enigmail.hdrView.msgEncryptionKeyId.primaryKeyId}`,
+ subkey: `0x${Enigmail.hdrView.msgEncryptionKeyId.keyId}`,
+ }
+ );
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("encryptionKeyId"),
+ "openpgp-enc-key-id",
+ {
+ key: `0x${Enigmail.hdrView.msgEncryptionKeyId.keyId}`,
+ }
+ );
+ }
+
+ if (
+ EnigmailKeyRing.getKeyById(Enigmail.hdrView.msgEncryptionKeyId.keyId)
+ ) {
+ document.getElementById("viewEncryptionKey").collapsed = false;
+ gEncKeyId = Enigmail.hdrView.msgEncryptionKeyId.keyId;
+ }
+ }
+
+ let otherLabel = document.getElementById("otherLabel");
+ if (myIdToSkipInList) {
+ document.l10n.setAttributes(otherLabel, "openpgp-other-enc-all-key-ids");
+ } else {
+ document.l10n.setAttributes(
+ otherLabel,
+ "openpgp-other-enc-additional-key-ids"
+ );
+ }
+
+ if (!Enigmail.hdrView.msgEncryptionAllKeyIds) {
+ return;
+ }
+
+ let keyList = document.getElementById("otherEncryptionKeysList");
+ // Remove all the previously populated keys.
+ while (keyList.lastChild) {
+ keyList.removeChild(keyList.lastChild);
+ }
+
+ let showExtraKeysList = false;
+ for (let key of Enigmail.hdrView.msgEncryptionAllKeyIds) {
+ if (key.keyId == myIdToSkipInList) {
+ continue;
+ }
+
+ let container = document.createXULElement("vbox");
+ container.classList.add("other-key-row");
+
+ let havePrimaryId2 = !!key.primaryKeyId;
+ let keyInfo = EnigmailKeyRing.getKeyById(
+ havePrimaryId2 ? key.primaryKeyId : key.keyId
+ );
+
+ // Use textContent for label XUl elements to enable text wrapping.
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.setAttribute("context", "simpleCopyPopup");
+ if (keyInfo) {
+ name.textContent = keyInfo.userId;
+ } else {
+ document.l10n.setAttributes(name, "openpgp-other-enc-all-key-ids");
+ }
+
+ let id = document.createXULElement("label");
+ id.setAttribute("context", "simpleCopyPopup");
+ id.classList.add("openpgp-key-id");
+ id.textContent = havePrimaryId2
+ ? ` 0x${key.primaryKeyId} (0x${key.keyId})`
+ : ` 0x${key.keyId}`;
+
+ container.appendChild(name);
+ container.appendChild(id);
+
+ keyList.appendChild(container);
+ showExtraKeysList = true;
+ }
+
+ // Show extra keys if present in the message.
+ document.getElementById("otherEncryptionKeys").collapsed =
+ !showExtraKeysList;
+ },
+};
+
+window.addEventListener(
+ "load-enigmail",
+ Enigmail.msg.messengerStartup.bind(Enigmail.msg)
+);
+window.addEventListener(
+ "unload",
+ Enigmail.msg.messengerClose.bind(Enigmail.msg)
+);
+window.addEventListener(
+ "unload-enigmail",
+ Enigmail.msg.onUnloadEnigmail.bind(Enigmail.msg)
+);
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js
new file mode 100644
index 0000000000..1ff4c2c27e
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js
@@ -0,0 +1,181 @@
+/*
+ * 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function onLoad() {
+ document.documentElement.style.minHeight = "120px";
+ var dlg = document.getElementById("enigmailMsgBox");
+ dlg.getButton("cancel").setAttribute("hidden", "true");
+ dlg.getButton("extra1").setAttribute("hidden", "true");
+ dlg.getButton("extra2").setAttribute("hidden", "true");
+
+ document.getElementById("filler").style.maxWidth =
+ screen.availWidth - 50 + "px";
+
+ let args = window.arguments[0];
+ let msgtext = args.msgtext;
+ let button1 = args.button1;
+ let button2 = args.button2;
+ let button3 = args.button3;
+ let buttonCancel = args.cancelButton;
+ let checkboxLabel = args.checkboxLabel;
+
+ if (args.iconType) {
+ let icn = document.getElementById("infoImage");
+ icn.removeAttribute("collapsed");
+ let iconClass = "";
+
+ switch (args.iconType) {
+ case 2:
+ iconClass = "question-icon";
+ break;
+ case 3:
+ iconClass = "alert-icon";
+ break;
+ case 4:
+ iconClass = "error-icon";
+ break;
+ default:
+ iconClass = "message-icon";
+ }
+ icn.setAttribute("class", "spaced " + iconClass);
+ }
+
+ if (args.dialogTitle) {
+ if (AppConstants.platform == "macosx") {
+ let t = document.getElementById("macosDialogTitle");
+ t.setAttribute("value", args.dialogTitle);
+ t.removeAttribute("collapsed");
+ }
+
+ dlg.setAttribute("title", args.dialogTitle);
+ } else {
+ document.l10n.setAttributes(dlg, "enig-alert-title");
+ }
+
+ if (button1) {
+ setButton("accept", button1);
+ }
+ if (button2) {
+ setButton("extra1", button2);
+ }
+ if (button3) {
+ setButton("extra2", button3);
+ }
+ if (buttonCancel) {
+ setButton("cancel", buttonCancel);
+ }
+
+ if (checkboxLabel) {
+ let checkboxElem = document.getElementById("theCheckBox");
+ checkboxElem.setAttribute("label", checkboxLabel);
+ document.getElementById("checkboxContainer").removeAttribute("hidden");
+ }
+
+ dlg.getButton("accept").focus();
+ let textbox = document.getElementById("msgtext");
+ textbox.appendChild(textbox.ownerDocument.createTextNode(msgtext));
+
+ window.addEventListener("keypress", onKeyPress);
+ setTimeout(resizeDlg, 0);
+}
+
+function resizeDlg() {
+ let availHeight = screen.availHeight;
+ if (window.outerHeight > availHeight - 100) {
+ let box = document.getElementById("msgContainer");
+ let dlg = document.getElementById("enigmailMsgBox");
+ let btnHeight = dlg.getButton("accept").parentNode.clientHeight + 20;
+ let boxHeight = box.clientHeight;
+ let dlgHeight = dlg.clientHeight;
+
+ box.setAttribute("style", "overflow: auto;");
+ box.setAttribute(
+ "height",
+ boxHeight - btnHeight - (dlgHeight - availHeight)
+ );
+ window.resizeTo(window.outerWidth, availHeight);
+ }
+}
+
+function setButton(buttonId, label) {
+ var labelType = buttonId;
+
+ var dlg = document.getElementById("enigmailMsgBox");
+ var elem = dlg.getButton(labelType);
+
+ var i = label.indexOf(":");
+ if (i === 0) {
+ elem = dlg.getButton(label.substr(1));
+ elem.setAttribute("hidden", "false");
+ elem.setAttribute("oncommand", "dlgClose('" + buttonId + "')");
+ return;
+ }
+ if (i > 0) {
+ labelType = label.substr(0, i);
+ label = label.substr(i + 1);
+ elem = dlg.getButton(labelType);
+ }
+ i = label.indexOf("&");
+ if (i >= 0) {
+ var c = label.substr(i + 1, 1);
+ if (c != "&") {
+ elem.setAttribute("accesskey", c);
+ }
+ label = label.substr(0, i) + label.substr(i + 1);
+ }
+ elem.setAttribute("label", label);
+ elem.setAttribute("oncommand", "dlgClose('" + buttonId + "')");
+ elem.removeAttribute("hidden");
+}
+
+function dlgClose(buttonId) {
+ let buttonNumber = 99;
+
+ switch (buttonId) {
+ case "accept":
+ buttonNumber = 0;
+ break;
+ case "extra1":
+ buttonNumber = 1;
+ break;
+ case "extra2":
+ buttonNumber = 2;
+ break;
+ case "cancel":
+ buttonNumber = -1;
+ }
+
+ window.arguments[1].value = buttonNumber;
+ window.arguments[1].checked =
+ document.getElementById("theCheckBox").getAttribute("checked") == "true";
+ window.close();
+}
+
+function checkboxCb() {
+ // do nothing
+}
+
+async function copyToClipbrd() {
+ let s = window.getSelection().toString();
+ return navigator.clipboard.writeText(s);
+}
+
+function onKeyPress(event) {
+ if (event.key == "c" && event.getModifierState("Accel")) {
+ copyToClipbrd();
+ event.stopPropagation();
+ }
+}
+
+document.addEventListener("dialogaccept", function (event) {
+ dlgClose("accept");
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml
new file mode 100644
index 0000000000..3fec668c8c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml
@@ -0,0 +1,71 @@
+<?xml version="1.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/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/enigmail.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+
+<!DOCTYPE window [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD; ]>
+
+<window
+ title=""
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons="accept,help,cancel,extra1,extra2"
+ onload="onLoad();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <dialog id="enigmailMsgBox" buttonpack="center">
+ <script
+ type="application/x-javascript"
+ src="chrome://openpgp/content/ui/enigmailMsgBox.js"
+ />
+ <linkset>
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+
+ <popupset>
+ <menupopup id="ctxmenu">
+ <menuitem
+ data-l10n-id="openpgp-copy-cmd-label"
+ oncommand="copyToClipbrd()"
+ />
+ </menupopup>
+ </popupset>
+
+ <hbox id="filler" style="min-width: 0%">
+ <spacer style="width: 29em" />
+ </hbox>
+
+ <html:div class="grid-two-column-fr">
+ <html:div class="flex-items-center">
+ <image id="infoImage" class="spaced" collapsed="true" />
+ </html:div>
+ <html:div class="flex-items-center">
+ <vbox id="infoContainer" pack="center">
+ <label
+ id="macosDialogTitle"
+ collapsed="true"
+ class="enigmailDialogTitle"
+ />
+ <vbox id="msgContainer" style="max-width: 45em">
+ <description
+ id="msgtext"
+ context="ctxmenu"
+ noinitialfocus="true"
+ class="enigmailDialogBody"
+ />
+ </vbox>
+ </vbox>
+ </html:div>
+ <html:div id="checkboxContainer" class="grid-item-col2" hidden="hidden">
+ <checkbox id="theCheckBox" oncommand="checkboxCb()" />
+ </html:div>
+ </html:div>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js b/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js
new file mode 100644
index 0000000000..db973f0ee9
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js
@@ -0,0 +1,3034 @@
+/*
+ * 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";
+
+/* import-globals-from ../../../../components/compose/content/MsgComposeCommands.js */
+/* import-globals-from ../../../../components/compose/content/addressingWidgetOverlay.js */
+/* global MsgAccountManager */
+/* global gCurrentIdentity */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var EnigmailCore = ChromeUtils.import(
+ "chrome://openpgp/content/modules/core.jsm"
+).EnigmailCore;
+var EnigmailFuncs = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+).EnigmailFuncs;
+var { EnigmailLog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/log.jsm"
+);
+var EnigmailArmor = ChromeUtils.import(
+ "chrome://openpgp/content/modules/armor.jsm"
+).EnigmailArmor;
+var EnigmailData = ChromeUtils.import(
+ "chrome://openpgp/content/modules/data.jsm"
+).EnigmailData;
+var EnigmailDialog = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+).EnigmailDialog;
+var EnigmailWindows = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+).EnigmailWindows;
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var EnigmailURIs = ChromeUtils.import(
+ "chrome://openpgp/content/modules/uris.jsm"
+).EnigmailURIs;
+var EnigmailConstants = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+).EnigmailConstants;
+var EnigmailDecryption = ChromeUtils.import(
+ "chrome://openpgp/content/modules/decryption.jsm"
+).EnigmailDecryption;
+var EnigmailEncryption = ChromeUtils.import(
+ "chrome://openpgp/content/modules/encryption.jsm"
+).EnigmailEncryption;
+var EnigmailWkdLookup = ChromeUtils.import(
+ "chrome://openpgp/content/modules/wkdLookup.jsm"
+).EnigmailWkdLookup;
+var EnigmailMime = ChromeUtils.import(
+ "chrome://openpgp/content/modules/mime.jsm"
+).EnigmailMime;
+var EnigmailMsgRead = ChromeUtils.import(
+ "chrome://openpgp/content/modules/msgRead.jsm"
+).EnigmailMsgRead;
+var EnigmailMimeEncrypt = ChromeUtils.import(
+ "chrome://openpgp/content/modules/mimeEncrypt.jsm"
+).EnigmailMimeEncrypt;
+const { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+const { OpenPGPAlias } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/OpenPGPAlias.jsm"
+);
+var { jsmime } = ChromeUtils.import("resource:///modules/jsmime.jsm");
+
+var l10nOpenPGP = new Localization(["messenger/openpgp/openpgp.ftl"]);
+
+// Account encryption policy values:
+// const kEncryptionPolicy_Never = 0;
+// 'IfPossible' was used by ns4.
+// const kEncryptionPolicy_IfPossible = 1;
+var kEncryptionPolicy_Always = 2;
+
+var Enigmail = {};
+
+const IOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
+const LOCAL_FILE_CONTRACTID = "@mozilla.org/file/local;1";
+
+Enigmail.msg = {
+ editor: null,
+ dirty: 0,
+ // dirty means: composer contents were modified by this code, right?
+ processed: null, // contains information for undo of inline signed/encrypt
+ timeoutId: null, // TODO: once set, it's never reset
+ sendPgpMime: true,
+ //sendMode: null, // the current default for sending a message (0, SIGN, ENCRYPT, or SIGN|ENCRYPT)
+ //sendModeDirty: false, // send mode or final send options changed?
+
+ // processed strings to signal final encrypt/sign/pgpmime state:
+ statusEncryptedStr: "???",
+ statusSignedStr: "???",
+ //statusPGPMimeStr: "???",
+ //statusSMimeStr: "???",
+ //statusInlinePGPStr: "???",
+ statusAttachOwnKey: "???",
+
+ sendProcess: false,
+ composeBodyReady: false,
+ modifiedAttach: null,
+ lastFocusedWindow: null,
+ draftSubjectEncrypted: false,
+ attachOwnKeyObj: {
+ attachedObj: null,
+ attachedKey: null,
+ },
+
+ keyLookupDone: [],
+
+ addrOnChangeTimeout: 250,
+ /* timeout when entering something into the address field */
+
+ async composeStartup() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.composeStartup\n"
+ );
+
+ if (!gMsgCompose || !gMsgCompose.compFields) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: no gMsgCompose, leaving\n"
+ );
+ return;
+ }
+
+ gMsgCompose.RegisterStateListener(Enigmail.composeStateListener);
+ Enigmail.msg.composeBodyReady = false;
+
+ // Listen to message sending event
+ addEventListener(
+ "compose-send-message",
+ Enigmail.msg.sendMessageListener.bind(Enigmail.msg),
+ true
+ );
+
+ await OpenPGPAlias.load().catch(console.error);
+
+ Enigmail.msg.composeOpen();
+ //Enigmail.msg.processFinalState();
+ },
+
+ // TODO: call this from global compose when options change
+ enigmailComposeProcessFinalState() {
+ //Enigmail.msg.processFinalState();
+ },
+
+ /*
+ handleClick: function(event, modifyType) {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.handleClick\n");
+ switch (event.button) {
+ case 2:
+ // do not process the event any further
+ // needed on Windows to prevent displaying the context menu
+ event.preventDefault();
+ this.doPgpButton();
+ break;
+ case 0:
+ this.doPgpButton(modifyType);
+ break;
+ }
+ },
+ */
+
+ /* return whether the account specific setting key is enabled or disabled
+ */
+ /*
+ getAccDefault: function(key) {
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.getAccDefault: identity="+this.identity.key+"("+this.identity.email+") key="+key+"\n");
+ let res = null;
+ let mimePreferOpenPGP = this.identity.getIntAttribute("mimePreferOpenPGP");
+ let isSmimeEnabled = Enigmail.msg.isSmimeEnabled();
+ let wasEnigmailEnabledForIdentity = Enigmail.msg.wasEnigmailEnabledForIdentity();
+ let preferSmimeByDefault = false;
+
+ if (isSmimeEnabled && wasEnigmailEnabledForIdentity) {
+ }
+
+ if (wasEnigmailEnabledForIdentity) {
+ switch (key) {
+ case 'sign':
+ if (preferSmimeByDefault) {
+ res = (this.identity.signMail);
+ }
+ else {
+ res = (this.identity.getIntAttribute("defaultSigningPolicy") > 0);
+ }
+ break;
+ case 'encrypt':
+ if (preferSmimeByDefault) {
+ res = (this.identity.encryptionPolicy > 0);
+ }
+ else {
+ res = (this.identity.getIntAttribute("defaultEncryptionPolicy") > 0);
+ }
+ break;
+ case 'sign-pgp':
+ res = (this.identity.getIntAttribute("defaultSigningPolicy") > 0);
+ break;
+ case 'pgpMimeMode':
+ res = this.identity.getBoolAttribute(key);
+ break;
+ case 'attachPgpKey':
+ res = this.identity.attachPgpKey;
+ break;
+ }
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.getAccDefault: "+key+"="+res+"\n");
+ return res;
+ }
+ else if (Enigmail.msg.isSmimeEnabled()) {
+ switch (key) {
+ case 'sign':
+ res = this.identity.signMail;
+ break;
+ case 'encrypt':
+ res = (this.identity.encryptionPolicy > 0);
+ break;
+ default:
+ res = false;
+ }
+ return res;
+ }
+ else {
+ // every detail is disabled if OpenPGP in general is disabled:
+ switch (key) {
+ case 'sign':
+ case 'encrypt':
+ case 'pgpMimeMode':
+ case 'attachPgpKey':
+ case 'sign-pgp':
+ return false;
+ }
+ }
+
+ // should not be reached
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.getAccDefault: internal error: invalid key '" + key + "'\n");
+ return null;
+ },
+ */
+
+ /**
+ * Determine if any of Enigmail (OpenPGP) or S/MIME encryption is enabled for the account
+ */
+ /*
+ isAnyEncryptionEnabled: function() {
+ let id = getCurrentIdentity();
+
+ return ((id.getUnicharAttribute("encryption_cert_name") !== "") ||
+ Enigmail.msg.wasEnigmailEnabledForIdentity());
+ },
+ */
+
+ isSmimeEnabled() {
+ return (
+ gCurrentIdentity.getUnicharAttribute("signing_cert_name") !== "" ||
+ gCurrentIdentity.getUnicharAttribute("encryption_cert_name") !== ""
+ );
+ },
+
+ /**
+ * Determine if any of Enigmail (OpenPGP) or S/MIME signing is enabled for the account
+ */
+ /*
+ getSigningEnabled: function() {
+ let id = getCurrentIdentity();
+
+ return ((id.getUnicharAttribute("signing_cert_name") !== "") ||
+ Enigmail.msg.wasEnigmailEnabledForIdentity());
+ },
+ */
+
+ /*
+ getSmimeSigningEnabled: function() {
+ let id = getCurrentIdentity();
+
+ if (!id.getUnicharAttribute("signing_cert_name")) return false;
+
+ return id.signMail;
+ },
+ */
+
+ /*
+ // set the current default for sending a message
+ // depending on the identity
+ processAccountSpecificDefaultOptions: function() {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.processAccountSpecificDefaultOptions\n");
+
+ const SIGN = EnigmailConstants.SEND_SIGNED;
+ const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;
+
+ this.sendMode = 0;
+
+ if (this.getSmimeSigningEnabled()) {
+ this.sendMode |= SIGN;
+ }
+
+ if (!Enigmail.msg.wasEnigmailEnabledForIdentity()) {
+ return;
+ }
+
+ if (this.getAccDefault("encrypt")) {
+ this.sendMode |= ENCRYPT;
+ }
+ if (this.getAccDefault("sign")) {
+ this.sendMode |= SIGN;
+ }
+
+ //this.sendPgpMime = this.getAccDefault("pgpMimeMode");
+ //console.debug("processAccountSpecificDefaultOptions sendPgpMime: " + this.sendPgpMime);
+ gAttachMyPublicPGPKey = this.getAccDefault("attachPgpKey");
+ this.setOwnKeyStatus();
+ this.attachOwnKeyObj.attachedObj = null;
+ this.attachOwnKeyObj.attachedKey = null;
+
+ //this.finalSignDependsOnEncrypt = (this.getAccDefault("signIfEnc") || this.getAccDefault("signIfNotEnc"));
+ },
+ */
+
+ getOriginalMsgUri() {
+ let draftId = gMsgCompose.compFields.draftId;
+ let msgUri = null;
+
+ if (draftId) {
+ // original message is draft
+ msgUri = draftId.replace(/\?.*$/, "");
+ } else if (gMsgCompose.originalMsgURI) {
+ // original message is a "true" mail
+ msgUri = gMsgCompose.originalMsgURI;
+ }
+
+ return msgUri;
+ },
+
+ getMsgHdr(msgUri) {
+ try {
+ if (!msgUri) {
+ msgUri = this.getOriginalMsgUri();
+ }
+ if (msgUri) {
+ return gMessenger.msgHdrFromURI(msgUri);
+ }
+ } catch (ex) {
+ // See also bug 1635648
+ console.debug("exception in getMsgHdr: " + ex);
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: exception in getMsgHdr: " + ex + "\n"
+ );
+ }
+ return null;
+ },
+
+ getMsgProperties(draft, msgUri, msgHdr, mimeMsg, obtainedDraftFlagsObj) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: Enigmail.msg.getMsgProperties:\n"
+ );
+ obtainedDraftFlagsObj.value = false;
+
+ let self = this;
+ let properties = 0;
+ try {
+ if (msgHdr) {
+ properties = msgHdr.getUint32Property("enigmail");
+
+ if (draft) {
+ if (self.getSavedDraftOptions(mimeMsg)) {
+ obtainedDraftFlagsObj.value = true;
+ }
+ updateEncryptionDependencies();
+ }
+ }
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: Enigmail.msg.getMsgProperties: got exception '" +
+ ex.toString() +
+ "'\n"
+ );
+ }
+
+ if (EnigmailURIs.isEncryptedUri(msgUri)) {
+ properties |= EnigmailConstants.DECRYPTION_OKAY;
+ }
+
+ return properties;
+ },
+
+ getSavedDraftOptions(mimeMsg) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.getSavedDraftOptions\n"
+ );
+ if (!mimeMsg || !mimeMsg.headers.has("x-enigmail-draft-status")) {
+ return false;
+ }
+
+ let stat = mimeMsg.headers.get("x-enigmail-draft-status").join("");
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.getSavedDraftOptions: draftStatus: " +
+ stat +
+ "\n"
+ );
+
+ if (stat.substr(0, 1) == "N") {
+ switch (Number(stat.substr(1, 1))) {
+ case 2:
+ // treat as "user decision to enable encryption, disable auto"
+ gUserTouchedSendEncrypted = true;
+ gSendEncrypted = true;
+ updateEncryptionDependencies();
+ break;
+ case 0:
+ // treat as "user decision to disable encryption, disable auto"
+ gUserTouchedSendEncrypted = true;
+ gSendEncrypted = false;
+ updateEncryptionDependencies();
+ break;
+ case 1:
+ default:
+ // treat as "no user decision, automatic mode"
+ break;
+ }
+
+ switch (Number(stat.substr(2, 1))) {
+ case 2:
+ gSendSigned = true;
+ gUserTouchedSendSigned = true;
+ break;
+ case 0:
+ gUserTouchedSendSigned = true;
+ gSendSigned = false;
+ break;
+ case 1:
+ default:
+ // treat as "no user decision, automatic mode, based on encryption or other prefs"
+ break;
+ }
+
+ switch (Number(stat.substr(3, 1))) {
+ case 1:
+ break;
+ case EnigmailConstants.ENIG_FORCE_SMIME:
+ // 3
+ gSelectedTechnologyIsPGP = false;
+ break;
+ case 2: // pgp/mime
+ case 0: // inline
+ default:
+ gSelectedTechnologyIsPGP = true;
+ break;
+ }
+
+ switch (Number(stat.substr(4, 1))) {
+ case 1:
+ gUserTouchedAttachMyPubKey = true;
+ gAttachMyPublicPGPKey = true;
+ break;
+ case 2:
+ gUserTouchedAttachMyPubKey = false;
+ break;
+ case 0:
+ default:
+ gUserTouchedAttachMyPubKey = true;
+ gAttachMyPublicPGPKey = false;
+ break;
+ }
+
+ switch (Number(stat.substr(4, 1))) {
+ case 1:
+ gUserTouchedAttachMyPubKey = true;
+ gAttachMyPublicPGPKey = true;
+ break;
+ case 2:
+ gUserTouchedAttachMyPubKey = false;
+ break;
+ case 0:
+ default:
+ gUserTouchedAttachMyPubKey = true;
+ gAttachMyPublicPGPKey = false;
+ break;
+ }
+
+ switch (Number(stat.substr(5, 1))) {
+ case 1:
+ gUserTouchedEncryptSubject = true;
+ gEncryptSubject = true;
+ break;
+ case 2:
+ gUserTouchedEncryptSubject = false;
+ break;
+ case 0:
+ default:
+ gUserTouchedEncryptSubject = true;
+ gEncryptSubject = false;
+ break;
+ }
+ }
+ //Enigmail.msg.setOwnKeyStatus();
+ return true;
+ },
+
+ composeOpen() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.composeOpen\n"
+ );
+
+ let msgUri = null;
+ let msgHdr = null;
+
+ msgUri = this.getOriginalMsgUri();
+ if (msgUri) {
+ msgHdr = this.getMsgHdr(msgUri);
+ if (msgHdr) {
+ try {
+ let msgUrl = EnigmailMsgRead.getUrlFromUriSpec(msgUri);
+ EnigmailMime.getMimeTreeFromUrl(msgUrl.spec, false, mimeMsg => {
+ Enigmail.msg.continueComposeOpenWithMimeTree(
+ msgUri,
+ msgHdr,
+ mimeMsg
+ );
+ });
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMessengerOverlay.js: composeOpen: exception in getMimeTreeFromUrl: " +
+ ex +
+ "\n"
+ );
+ this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
+ }
+ } else {
+ this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
+ }
+ } else {
+ this.continueComposeOpenWithMimeTree(msgUri, msgHdr, null);
+ }
+ },
+
+ continueComposeOpenWithMimeTree(msgUri, msgHdr, mimeMsg) {
+ let selectedElement = document.activeElement;
+
+ let msgIsDraft =
+ gMsgCompose.type === Ci.nsIMsgCompType.Draft ||
+ gMsgCompose.type === Ci.nsIMsgCompType.Template;
+
+ if (!gSendEncrypted || msgIsDraft) {
+ let useEncryptionUnlessWeHaveDraftInfo = false;
+ let usePGPUnlessWeKnowOtherwise = false;
+ let useSMIMEUnlessWeKnowOtherwise = false;
+
+ if (msgIsDraft) {
+ let globalSaysItsEncrypted =
+ gEncryptedURIService &&
+ gMsgCompose.originalMsgURI &&
+ gEncryptedURIService.isEncrypted(gMsgCompose.originalMsgURI);
+
+ if (globalSaysItsEncrypted) {
+ useEncryptionUnlessWeHaveDraftInfo = true;
+ useSMIMEUnlessWeKnowOtherwise = true;
+ }
+ }
+
+ let obtainedDraftFlagsObj = { value: false };
+ if (msgUri) {
+ let msgFlags = this.getMsgProperties(
+ msgIsDraft,
+ msgUri,
+ msgHdr,
+ mimeMsg,
+ obtainedDraftFlagsObj
+ );
+ if (msgFlags & EnigmailConstants.DECRYPTION_OKAY) {
+ usePGPUnlessWeKnowOtherwise = true;
+ useSMIMEUnlessWeKnowOtherwise = false;
+ }
+ if (msgIsDraft && obtainedDraftFlagsObj.value) {
+ useEncryptionUnlessWeHaveDraftInfo = false;
+ usePGPUnlessWeKnowOtherwise = false;
+ useSMIMEUnlessWeKnowOtherwise = false;
+ }
+ if (!msgIsDraft) {
+ if (msgFlags & EnigmailConstants.DECRYPTION_OKAY) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.composeOpen: has encrypted originalMsgUri\n"
+ );
+ EnigmailLog.DEBUG(
+ "originalMsgURI=" + gMsgCompose.originalMsgURI + "\n"
+ );
+ gSendEncrypted = true;
+ updateEncryptionDependencies();
+ gSelectedTechnologyIsPGP = true;
+ useEncryptionUnlessWeHaveDraftInfo = false;
+ usePGPUnlessWeKnowOtherwise = false;
+ useSMIMEUnlessWeKnowOtherwise = false;
+ }
+ }
+ this.removeAttachedKey();
+ }
+
+ if (useEncryptionUnlessWeHaveDraftInfo) {
+ gSendEncrypted = true;
+ updateEncryptionDependencies();
+ }
+ if (gSendEncrypted && !obtainedDraftFlagsObj.value) {
+ gSendSigned = true;
+ }
+ if (usePGPUnlessWeKnowOtherwise) {
+ gSelectedTechnologyIsPGP = true;
+ } else if (useSMIMEUnlessWeKnowOtherwise) {
+ gSelectedTechnologyIsPGP = false;
+ }
+ }
+
+ // check for attached signature files and remove them
+ var bucketList = document.getElementById("attachmentBucket");
+ if (bucketList.hasChildNodes()) {
+ var node = bucketList.firstChild;
+ while (node) {
+ if (node.attachment.contentType == "application/pgp-signature") {
+ if (!this.findRelatedAttachment(bucketList, node)) {
+ // Let's release the attachment object held by the node else it won't go away until the window is destroyed
+ node.attachment = null;
+ node = bucketList.removeChild(node);
+ }
+ }
+ node = node.nextSibling;
+ }
+ }
+
+ // If we removed all the children and the bucket wasn't meant
+ // to stay open, close it.
+ if (!Services.prefs.getBoolPref("mail.compose.show_attachment_pane")) {
+ UpdateAttachmentBucket(bucketList.hasChildNodes());
+ }
+
+ this.warnUserIfSenderKeyExpired();
+
+ //this.processFinalState();
+ if (selectedElement) {
+ selectedElement.focus();
+ }
+ },
+
+ // check if an signature is related to another attachment
+ findRelatedAttachment(bucketList, node) {
+ // check if filename ends with .sig
+ if (node.attachment.name.search(/\.sig$/i) < 0) {
+ return null;
+ }
+
+ var relatedNode = bucketList.firstChild;
+ var findFile = node.attachment.name.toLowerCase();
+ var baseAttachment = null;
+ while (relatedNode) {
+ if (relatedNode.attachment.name.toLowerCase() + ".sig" == findFile) {
+ baseAttachment = relatedNode.attachment;
+ }
+ relatedNode = relatedNode.nextSibling;
+ }
+ return baseAttachment;
+ },
+
+ async attachOwnKey(id) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.attachOwnKey: " + id + "\n"
+ );
+
+ if (
+ this.attachOwnKeyObj.attachedKey &&
+ this.attachOwnKeyObj.attachedKey != id
+ ) {
+ // remove attached key if user ID changed
+ this.removeAttachedKey();
+ }
+ let revokedIDs = EnigmailKeyRing.findRevokedPersonalKeysByEmail(
+ gCurrentIdentity.email
+ );
+
+ if (!this.attachOwnKeyObj.attachedKey) {
+ let hex = "0x" + id;
+ var attachedObj = await this.extractAndAttachKey(
+ hex,
+ revokedIDs,
+ gCurrentIdentity.email,
+ true,
+ true // one key plus revocations
+ );
+ if (attachedObj) {
+ this.attachOwnKeyObj.attachedObj = attachedObj;
+ this.attachOwnKeyObj.attachedKey = hex;
+ }
+ }
+ },
+
+ async extractAndAttachKey(
+ primaryId,
+ revokedIds,
+ emailForFilename,
+ warnOnError
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.extractAndAttachKey: \n"
+ );
+ var enigmailSvc = EnigmailCore.getService(window);
+ if (!enigmailSvc) {
+ return null;
+ }
+
+ var tmpFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tmpFile.append("key.asc");
+ tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ // save file
+ var exitCodeObj = {};
+ var errorMsgObj = {};
+
+ await EnigmailKeyRing.extractPublicKeys(
+ [], // full
+ [primaryId], // reduced
+ revokedIds, // minimal
+ tmpFile,
+ exitCodeObj,
+ errorMsgObj
+ );
+ if (exitCodeObj.value !== 0) {
+ if (warnOnError) {
+ EnigmailDialog.alert(window, errorMsgObj.value);
+ }
+ return null;
+ }
+
+ // create attachment
+ var tmpFileURI = Services.io.newFileURI(tmpFile);
+ var keyAttachment = Cc[
+ "@mozilla.org/messengercompose/attachment;1"
+ ].createInstance(Ci.nsIMsgAttachment);
+ keyAttachment.url = tmpFileURI.spec;
+ keyAttachment.name = primaryId.substr(-16, 16);
+ if (keyAttachment.name.search(/^0x/) < 0) {
+ keyAttachment.name = "0x" + keyAttachment.name;
+ }
+ let withRevSuffix = "";
+ if (revokedIds && revokedIds.length) {
+ withRevSuffix = "_and_old_rev";
+ }
+ keyAttachment.name =
+ "OpenPGP_" + keyAttachment.name + withRevSuffix + ".asc";
+ keyAttachment.temporary = true;
+ keyAttachment.contentType = "application/pgp-keys";
+ keyAttachment.size = tmpFile.fileSize;
+
+ if (
+ !gAttachmentBucket.itemChildren.find(
+ item => item.attachment.name == keyAttachment.name
+ )
+ ) {
+ await this.addAttachment(keyAttachment);
+ }
+
+ gContentChanged = true;
+ return keyAttachment;
+ },
+
+ addAttachment(attachment) {
+ return AddAttachments([attachment]);
+ },
+
+ removeAttachedKey() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.removeAttachedKey: \n"
+ );
+
+ let bucketList = document.getElementById("attachmentBucket");
+ let node = bucketList.firstElementChild;
+
+ if (bucketList.itemCount && this.attachOwnKeyObj.attachedObj) {
+ // Undo attaching own key.
+ while (node) {
+ if (node.attachment.url == this.attachOwnKeyObj.attachedObj.url) {
+ node = bucketList.removeChild(node);
+ // Let's release the attachment object held by the node else it won't
+ // go away until the window is destroyed.
+ node.attachment = null;
+ this.attachOwnKeyObj.attachedObj = null;
+ this.attachOwnKeyObj.attachedKey = null;
+ node = null; // exit loop.
+ } else {
+ node = node.nextSibling;
+ }
+ }
+
+ // Update the visibility of the attachment pane.
+ UpdateAttachmentBucket(bucketList.itemCount);
+ }
+ },
+
+ getSecurityParams(compFields = null) {
+ if (!compFields) {
+ if (!gMsgCompose) {
+ return null;
+ }
+
+ compFields = gMsgCompose.compFields;
+ }
+
+ return compFields.composeSecure;
+ },
+
+ setSecurityParams(newSecurityParams) {
+ if (!gMsgCompose || !gMsgCompose.compFields) {
+ return;
+ }
+ gMsgCompose.compFields.composeSecure = newSecurityParams;
+ },
+
+ // Used on send failure, to reset the pre-send modifications
+ resetUpdatedFields() {
+ this.removeAttachedKey();
+
+ // reset subject
+ let p = Enigmail.msg.getSecurityParams();
+ if (p && EnigmailMimeEncrypt.isEnigmailCompField(p)) {
+ let si = p.wrappedJSObject;
+ if (si.originalSubject) {
+ gMsgCompose.compFields.subject = si.originalSubject;
+ }
+ }
+ },
+
+ replaceEditorText(text) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.replaceEditorText:\n"
+ );
+
+ this.editorSelectAll();
+ // Overwrite text in clipboard for security
+ // (Otherwise plaintext will be available in the clipbaord)
+
+ if (this.editor.textLength > 0) {
+ this.editorInsertText("Enigmail");
+ } else {
+ this.editorInsertText(" ");
+ }
+
+ this.editorSelectAll();
+ this.editorInsertText(text);
+ },
+
+ /**
+ * Determine if Enigmail is enabled for the account
+ */
+
+ isEnigmailEnabledForIdentity() {
+ return !!gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
+ },
+
+ /**
+ * Determine if Autocrypt is enabled for the account
+ */
+ isAutocryptEnabled() {
+ return false;
+ /*
+ if (Enigmail.msg.wasEnigmailEnabledForIdentity()) {
+ let srv = this.getCurrentIncomingServer();
+ return (srv ? srv.getBoolValue("enableAutocrypt") : false);
+ }
+
+ return false;
+ */
+ },
+
+ /*
+ doPgpButton: function(what) {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.doPgpButton: what=" + what + "\n");
+
+ if (Enigmail.msg.wasEnigmailEnabledForIdentity()) {
+ EnigmailCore.getService(window); // try to access Enigmail to launch the wizard if needed
+ }
+
+ // ignore settings for this account?
+ try {
+ if (!this.isAnyEncryptionEnabled() && !this.getSigningEnabled()) {
+ return;
+ }
+ }
+ catch (ex) {}
+
+ switch (what) {
+ case 'sign':
+ case 'encrypt':
+ this.setSendMode(what);
+ break;
+
+ case 'trustKeys':
+ this.tempTrustAllKeys();
+ break;
+
+ case 'nothing':
+ break;
+
+ case 'displaySecuritySettings':
+ this.displaySecuritySettings();
+ break;
+ default:
+ this.displaySecuritySettings();
+ }
+
+ },
+ */
+
+ // changes the DEFAULT sendMode
+ // - also called internally for saved emails
+ /*
+ setSendMode: function(sendMode) {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.setSendMode: sendMode=" + sendMode + "\n");
+ const SIGN = EnigmailConstants.SEND_SIGNED;
+ const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;
+
+ var origSendMode = this.sendMode;
+ switch (sendMode) {
+ case 'sign':
+ this.sendMode |= SIGN;
+ break;
+ case 'encrypt':
+ this.sendMode |= ENCRYPT;
+ break;
+ default:
+ EnigmailDialog.alert(window, "Enigmail.msg.setSendMode - unexpected value: " + sendMode);
+ break;
+ }
+ // sendMode changed ?
+ // - sign and send are internal initializations
+ if (!this.sendModeDirty && (this.sendMode != origSendMode) && sendMode != 'sign' && sendMode != 'encrypt') {
+ this.sendModeDirty = true;
+ }
+ this.processFinalState();
+ },
+ */
+
+ /**
+ key function to process the final encrypt/sign/pgpmime state from all settings
+ *
+ @param sendFlags: contains the sendFlags if the message is really processed. Optional, can be null
+ - uses as INPUT:
+ - this.sendMode
+ - this.encryptForced, this.encryptSigned
+ - uses as OUTPUT:
+ - this.statusEncrypt, this.statusSign
+
+ no return value
+ */
+ processFinalState(sendFlags) {},
+
+ /* check if encryption is possible (have keys for everyone or not)
+ */
+ async determineSendFlags() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.focusChange: Enigmail.msg.determineSendFlags\n"
+ );
+
+ let detailsObj = {};
+ var compFields = gMsgCompose.compFields;
+
+ if (!Enigmail.msg.composeBodyReady) {
+ compFields = Cc[
+ "@mozilla.org/messengercompose/composefields;1"
+ ].createInstance(Ci.nsIMsgCompFields);
+ }
+ Recipients2CompFields(compFields);
+
+ // disabled, see bug 1625135
+ // gMsgCompose.expandMailingLists();
+
+ if (Enigmail.msg.isEnigmailEnabledForIdentity()) {
+ var toAddrList = [];
+ var arrLen = {};
+ var recList;
+ if (compFields.to) {
+ recList = compFields.splitRecipients(compFields.to, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+ if (compFields.cc) {
+ recList = compFields.splitRecipients(compFields.cc, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+ if (compFields.bcc) {
+ recList = compFields.splitRecipients(compFields.bcc, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+
+ let addresses = [];
+ try {
+ addresses = EnigmailFuncs.stripEmail(toAddrList.join(", ")).split(",");
+ } catch (ex) {}
+
+ // Resolve all the email addresses if possible.
+ await EnigmailKeyRing.getValidKeysForAllRecipients(addresses, detailsObj);
+ //this.autoPgpEncryption = (validKeyList !== null);
+ }
+
+ // process and signal new resulting state
+ //this.processFinalState();
+
+ return detailsObj;
+ },
+
+ /*
+ displaySecuritySettings: function() {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.displaySecuritySettings\n");
+
+ var inputObj = {
+ gSendEncrypted: gSendEncrypted,
+ gSendSigned: gSendSigned,
+ success: false,
+ resetDefaults: false
+ };
+ window.openDialog("chrome://openpgp/content/ui/enigmailEncryptionDlg.xhtml", "", "dialog,modal,centerscreen", inputObj);
+
+ if (!inputObj.success) return; // Cancel pressed
+
+ if (inputObj.resetDefaults) {
+ // reset everything to defaults
+ this.encryptForced = 1;
+ this.signForced = 1;
+ }
+ else {
+ if (this.signForced != inputObj.sign) {
+ this.dirty = 2;
+ this.signForced = inputObj.sign;
+ }
+
+ this.dirty = 2;
+
+ this.encryptForced = inputObj.encrypt;
+ }
+
+ //this.processFinalState();
+ },
+ */
+
+ addRecipients(toAddrList, recList) {
+ for (var i = 0; i < recList.length; i++) {
+ try {
+ toAddrList.push(
+ EnigmailFuncs.stripEmail(recList[i].replace(/[",]/g, ""))
+ );
+ } catch (ex) {}
+ }
+ },
+
+ setDraftStatus(doEncrypt) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.setDraftStatus - enabling draft mode\n"
+ );
+
+ // Draft Status:
+ // N (for new style) plus 5 digits:
+ // 1: encryption
+ // 2: signing
+ // 3: PGP/MIME
+ // 4: attach own key
+ // 5: subject encrypted
+
+ var draftStatus = "N";
+
+ // Encryption:
+ // 2 -> required/enabled
+ // 0 -> disabled
+
+ if (!gUserTouchedSendEncrypted && !gIsRelatedToEncryptedOriginal) {
+ // After opening draft, it's allowed to use automatic decision.
+ draftStatus += "1";
+ } else {
+ // After opening draft, use the same state that is set now.
+ draftStatus += gSendEncrypted ? "2" : "0";
+ }
+
+ if (!gUserTouchedSendSigned) {
+ // After opening draft, it's allowed to use automatic decision.
+ draftStatus += "1";
+ } else {
+ // After opening draft, use the same state that is set now.
+ // Signing:
+ // 2 -> enabled
+ // 0 -> disabled
+ draftStatus += gSendSigned ? "2" : "0";
+ }
+
+ // MIME/technology
+ // ENIG_FORCE_SMIME == 3 -> S/MIME
+ // ENIG_FORCE_ALWAYS == 2 -> PGP/MIME
+ // 0 -> PGP inline
+ if (gSelectedTechnologyIsPGP) {
+ // inline signing currently not implemented
+ draftStatus += "2";
+ } else {
+ draftStatus += "3";
+ }
+
+ if (!gUserTouchedAttachMyPubKey) {
+ draftStatus += "2";
+ } else {
+ draftStatus += gAttachMyPublicPGPKey ? "1" : "0";
+ }
+
+ if (!gUserTouchedEncryptSubject) {
+ draftStatus += "2";
+ } else {
+ draftStatus += gSendEncrypted && gEncryptSubject ? "1" : "0";
+ }
+
+ this.setAdditionalHeader("X-Enigmail-Draft-Status", draftStatus);
+ },
+
+ getSenderUserId() {
+ let keyId = gCurrentIdentity?.getUnicharAttribute("openpgp_key_id");
+ return keyId ? "0x" + keyId : null;
+ },
+
+ /**
+ * Determine if S/MIME or OpenPGP should be used
+ *
+ * @param sendFlags: Number - input send flags.
+ *
+ * @return: Boolean:
+ * 1: use OpenPGP
+ * 0: use S/MIME
+ */
+ /*
+ preferPgpOverSmime: function(sendFlags) {
+
+ let si = Enigmail.msg.getSecurityParams(null);
+ let isSmime = !EnigmailMimeEncrypt.isEnigmailCompField(si);
+
+ if (isSmime &&
+ (sendFlags & (EnigmailConstants.SEND_SIGNED | EnigmailConstants.SEND_ENCRYPTED))) {
+
+ if (si.requireEncryptMessage || si.signMessage) {
+
+ if (sendFlags & EnigmailConstants.SAVE_MESSAGE) {
+ // use S/MIME if it's enabled for saving drafts
+ return 0;
+ }
+ else {
+ return this.mimePreferOpenPGP;
+ }
+ }
+ }
+
+ return 1;
+ },
+ */
+
+ /* Manage the wrapping of inline signed mails
+ *
+ * @wrapresultObj: Result:
+ * @wrapresultObj.cancelled, true if send operation is to be cancelled, else false
+ * @wrapresultObj.usePpgMime, true if message send option was changed to PGP/MIME, else false
+ */
+
+ async wrapInLine(wrapresultObj) {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: WrapInLine\n");
+ wrapresultObj.cancelled = false;
+ wrapresultObj.usePpgMime = false;
+ try {
+ const dce = Ci.nsIDocumentEncoder;
+ var editor = gMsgCompose.editor.QueryInterface(Ci.nsIEditorMailSupport);
+ var encoderFlags = dce.OutputFormatted | dce.OutputLFLineBreak;
+
+ var wrapWidth = Services.prefs.getIntPref("mailnews.wraplength");
+ if (wrapWidth > 0 && wrapWidth < 68 && editor.wrapWidth > 0) {
+ if (
+ EnigmailDialog.confirmDlg(
+ window,
+ await l10nOpenPGP.formatValue("minimal-line-wrapping", {
+ width: wrapWidth,
+ })
+ )
+ ) {
+ wrapWidth = 68;
+ Services.prefs.setIntPref("mailnews.wraplength", wrapWidth);
+ }
+ }
+
+ if (wrapWidth && editor.wrapWidth > 0) {
+ // First use standard editor wrap mechanism:
+ editor.wrapWidth = wrapWidth - 2;
+ editor.rewrap(true);
+ editor.wrapWidth = wrapWidth;
+
+ // Now get plaintext from editor
+ var wrapText = this.editorGetContentAs("text/plain", encoderFlags);
+
+ // split the lines into an array
+ wrapText = wrapText.split(/\r\n|\r|\n/g);
+
+ var i = 0;
+ var excess = 0;
+ // inspect all lines of mail text to detect if we still have excessive lines which the "standard" editor wrapper leaves
+ for (i = 0; i < wrapText.length; i++) {
+ if (wrapText[i].length > wrapWidth) {
+ excess = 1;
+ }
+ }
+
+ if (excess) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Excess lines detected\n"
+ );
+ var resultObj = {};
+ window.openDialog(
+ "chrome://openpgp/content/ui/enigmailWrapSelection.xhtml",
+ "",
+ "dialog,modal,centerscreen",
+ resultObj
+ );
+ try {
+ if (resultObj.cancelled) {
+ // cancel pressed -> do not send, return instead.
+ wrapresultObj.cancelled = true;
+ return;
+ }
+ } catch (ex) {
+ // cancel pressed -> do not send, return instead.
+ wrapresultObj.cancelled = true;
+ return;
+ }
+
+ var limitedLine = "";
+ var restOfLine = "";
+
+ var WrapSelect = resultObj.Select;
+ switch (WrapSelect) {
+ case "0": // Selection: Force rewrap
+ for (i = 0; i < wrapText.length; i++) {
+ if (wrapText[i].length > wrapWidth) {
+ // If the current line is too long, limit it hard to wrapWidth and insert the rest as the next line into wrapText array
+ limitedLine = wrapText[i].slice(0, wrapWidth);
+ restOfLine = wrapText[i].slice(wrapWidth);
+
+ // We should add quotes at the beginning of "restOfLine", if limitedLine is a quoted line
+ // However, this would be purely academic, because limitedLine will always be "standard"-wrapped
+ // by the editor-rewrapper at the space between quote sign (>) and the quoted text.
+
+ wrapText.splice(i, 1, limitedLine, restOfLine);
+ }
+ }
+ break;
+ case "1": // Selection: Send as is
+ break;
+ case "2": // Selection: Use MIME
+ wrapresultObj.usePpgMime = true;
+ break;
+ case "3": // Selection: Edit manually -> do not send, return instead.
+ wrapresultObj.cancelled = true;
+ return;
+ } //switch
+ }
+ // Now join all lines together again and feed it back into the compose editor.
+ var newtext = wrapText.join("\n");
+ this.replaceEditorText(newtext);
+ }
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Exception while wrapping=" + ex + "\n"
+ );
+ }
+ },
+
+ // Save draft message. We do not want most of the other processing for encrypted mails here...
+ async saveDraftMessage(senderKeyIsGnuPG) {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: saveDraftMessage()\n");
+
+ // If we have an encryption key configured, then encrypt saved
+ // drafts by default, as a precaution. This is independent from the
+ // final decision of sending the message encrypted or not.
+ // However, we allow the user to disable encrypted drafts.
+ let doEncrypt =
+ Enigmail.msg.isEnigmailEnabledForIdentity() &&
+ gCurrentIdentity.autoEncryptDrafts;
+
+ this.setDraftStatus(doEncrypt);
+
+ if (!doEncrypt) {
+ try {
+ let p = Enigmail.msg.getSecurityParams();
+ if (EnigmailMimeEncrypt.isEnigmailCompField(p)) {
+ p.wrappedJSObject.sendFlags = 0;
+ }
+ } catch (ex) {
+ console.debug(ex);
+ }
+
+ return true;
+ }
+
+ let sendFlags =
+ EnigmailConstants.SEND_PGP_MIME |
+ EnigmailConstants.SEND_ENCRYPTED |
+ EnigmailConstants.SEND_ENCRYPT_TO_SELF |
+ EnigmailConstants.SAVE_MESSAGE;
+
+ if (gEncryptSubject) {
+ sendFlags |= EnigmailConstants.ENCRYPT_SUBJECT;
+ }
+ if (senderKeyIsGnuPG) {
+ sendFlags |= EnigmailConstants.SEND_SENDER_KEY_EXTERNAL;
+ }
+
+ let fromAddr = this.getSenderUserId();
+
+ let enigmailSvc = EnigmailCore.getService(window);
+ if (!enigmailSvc) {
+ return true;
+ }
+
+ let senderKeyUsable = await EnigmailEncryption.determineOwnKeyUsability(
+ sendFlags,
+ fromAddr,
+ senderKeyIsGnuPG
+ );
+ if (senderKeyUsable.errorMsg) {
+ let fullAlert = await document.l10n.formatValue(
+ "msg-compose-cannot-save-draft"
+ );
+ fullAlert += " - " + senderKeyUsable.errorMsg;
+ EnigmailDialog.alert(window, fullAlert);
+ return false;
+ }
+
+ //if (this.preferPgpOverSmime(sendFlags) === 0) return true; // use S/MIME
+
+ let secInfo;
+
+ let param = Enigmail.msg.getSecurityParams();
+
+ if (EnigmailMimeEncrypt.isEnigmailCompField(param)) {
+ secInfo = param.wrappedJSObject;
+ } else {
+ try {
+ secInfo = EnigmailMimeEncrypt.createMimeEncrypt(param);
+ if (secInfo) {
+ Enigmail.msg.setSecurityParams(secInfo);
+ }
+ } catch (ex) {
+ EnigmailLog.writeException(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.saveDraftMessage",
+ ex
+ );
+ return false;
+ }
+ }
+
+ secInfo.sendFlags = sendFlags;
+ secInfo.UIFlags = 0;
+ secInfo.senderEmailAddr = fromAddr;
+ secInfo.recipients = "";
+ secInfo.bccRecipients = "";
+ secInfo.originalSubject = gMsgCompose.compFields.subject;
+ this.dirty = 1;
+
+ if (sendFlags & EnigmailConstants.ENCRYPT_SUBJECT) {
+ gMsgCompose.compFields.subject = "";
+ }
+
+ return true;
+ },
+
+ createEnigmailSecurityFields(oldSecurityInfo) {
+ let newSecurityInfo = EnigmailMimeEncrypt.createMimeEncrypt(
+ Enigmail.msg.getSecurityParams()
+ );
+
+ if (!newSecurityInfo) {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+
+ Enigmail.msg.setSecurityParams(newSecurityInfo);
+ },
+
+ /*
+ sendSmimeEncrypted: function(msgSendType, sendFlags, isOffline) {
+ let recList;
+ let toAddrList = [];
+ let arrLen = {};
+ const DeliverMode = Ci.nsIMsgCompDeliverMode;
+
+ switch (msgSendType) {
+ case DeliverMode.SaveAsDraft:
+ case DeliverMode.SaveAsTemplate:
+ case DeliverMode.AutoSaveAsDraft:
+ break;
+ default:
+ if (gAttachMyPublicPGPKey) {
+ await this.attachOwnKey();
+ Attachments2CompFields(gMsgCompose.compFields); // update list of attachments
+ }
+ }
+
+ gSMFields.signMessage = (sendFlags & EnigmailConstants.SEND_SIGNED ? true : false);
+ gSMFields.requireEncryptMessage = (sendFlags & EnigmailConstants.SEND_ENCRYPTED ? true : false);
+
+ Enigmail.msg.setSecurityParams(gSMFields);
+
+ let conf = this.isSendConfirmationRequired(sendFlags);
+
+ if (conf === null) return false;
+ if (conf) {
+ // confirm before send requested
+ let msgCompFields = gMsgCompose.compFields;
+ let splitRecipients = msgCompFields.splitRecipients;
+
+ if (msgCompFields.to.length > 0) {
+ recList = splitRecipients(msgCompFields.to, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+
+ if (msgCompFields.cc.length > 0) {
+ recList = splitRecipients(msgCompFields.cc, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+
+ switch (msgSendType) {
+ case DeliverMode.SaveAsDraft:
+ case DeliverMode.SaveAsTemplate:
+ case DeliverMode.AutoSaveAsDraft:
+ break;
+ default:
+ if (!this.confirmBeforeSend(toAddrList.join(", "), "", sendFlags, isOffline)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ },
+ */
+
+ getEncryptionFlags() {
+ let f = 0;
+
+ if (gSendEncrypted) {
+ f |= EnigmailConstants.SEND_ENCRYPTED;
+ } else {
+ f &= ~EnigmailConstants.SEND_ENCRYPTED;
+ }
+
+ if (gSendSigned) {
+ f |= EnigmailConstants.SEND_SIGNED;
+ } else {
+ f &= ~EnigmailConstants.SEND_SIGNED;
+ }
+
+ if (gSendEncrypted && gSendSigned) {
+ if (Services.prefs.getBoolPref("mail.openpgp.separate_mime_layers")) {
+ f |= EnigmailConstants.SEND_TWO_MIME_LAYERS;
+ }
+ }
+
+ if (gSendEncrypted && gEncryptSubject) {
+ f |= EnigmailConstants.ENCRYPT_SUBJECT;
+ }
+
+ return f;
+ },
+
+ resetDirty() {
+ let newSecurityInfo = null;
+
+ if (this.dirty) {
+ // make sure the sendFlags are reset before the message is processed
+ // (it may have been set by a previously cancelled send operation!)
+
+ let si = Enigmail.msg.getSecurityParams();
+
+ if (EnigmailMimeEncrypt.isEnigmailCompField(si)) {
+ si.sendFlags = 0;
+ si.originalSubject = gMsgCompose.compFields.subject;
+ } else {
+ try {
+ newSecurityInfo = EnigmailMimeEncrypt.createMimeEncrypt(si);
+ if (newSecurityInfo) {
+ newSecurityInfo.sendFlags = 0;
+ newSecurityInfo.originalSubject = gMsgCompose.compFields.subject;
+
+ Enigmail.msg.setSecurityParams(newSecurityInfo);
+ }
+ } catch (ex) {
+ EnigmailLog.writeException(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.resetDirty",
+ ex
+ );
+ }
+ }
+ }
+
+ return newSecurityInfo;
+ },
+
+ async determineMsgRecipients(sendFlags) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.determineMsgRecipients: currentId=" +
+ gCurrentIdentity +
+ ", " +
+ gCurrentIdentity.email +
+ "\n"
+ );
+
+ let fromAddr = gCurrentIdentity.email;
+ let toAddrList = [];
+ let recList;
+ let bccAddrList = [];
+ let arrLen = {};
+ let splitRecipients;
+
+ if (!Enigmail.msg.isEnigmailEnabledForIdentity()) {
+ return true;
+ }
+
+ let optSendFlags = 0;
+ let msgCompFields = gMsgCompose.compFields;
+ let newsgroups = msgCompFields.newsgroups;
+
+ if (Services.prefs.getBoolPref("temp.openpgp.encryptToSelf")) {
+ optSendFlags |= EnigmailConstants.SEND_ENCRYPT_TO_SELF;
+ }
+
+ sendFlags |= optSendFlags;
+
+ var userIdValue = this.getSenderUserId();
+ if (userIdValue) {
+ fromAddr = userIdValue;
+ }
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.determineMsgRecipients:gMsgCompose=" +
+ gMsgCompose +
+ "\n"
+ );
+
+ splitRecipients = msgCompFields.splitRecipients;
+
+ if (msgCompFields.to.length > 0) {
+ recList = splitRecipients(msgCompFields.to, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+
+ if (msgCompFields.cc.length > 0) {
+ recList = splitRecipients(msgCompFields.cc, true, arrLen);
+ this.addRecipients(toAddrList, recList);
+ }
+
+ // We allow sending to BCC recipients, we assume the user interface
+ // has warned the user that there is no privacy of BCC recipients.
+ if (msgCompFields.bcc.length > 0) {
+ recList = splitRecipients(msgCompFields.bcc, true, arrLen);
+ this.addRecipients(bccAddrList, recList);
+ }
+
+ if (newsgroups) {
+ toAddrList.push(newsgroups);
+
+ if (sendFlags & EnigmailConstants.SEND_ENCRYPTED) {
+ if (!Services.prefs.getBoolPref("temp.openpgp.encryptToNews")) {
+ document.l10n.formatValue("sending-news").then(value => {
+ EnigmailDialog.alert(window, value);
+ });
+ return false;
+ } else if (
+ !EnigmailDialog.confirmBoolPref(
+ window,
+ await l10nOpenPGP.formatValue("send-to-news-warning"),
+ "temp.openpgp.warnOnSendingNewsgroups",
+ await l10nOpenPGP.formatValue("msg-compose-button-send")
+ )
+ ) {
+ return false;
+ }
+ }
+ }
+
+ return {
+ sendFlags,
+ optSendFlags,
+ fromAddr,
+ toAddrList,
+ bccAddrList,
+ };
+ },
+
+ prepareSending(sendFlags, toAddrStr, gpgKeys, isOffline) {
+ // perform confirmation dialog if necessary/requested
+ if (
+ sendFlags & EnigmailConstants.SEND_WITH_CHECK &&
+ !this.messageSendCheck()
+ ) {
+ // Abort send
+ if (!this.processed) {
+ this.removeAttachedKey();
+ }
+
+ return false;
+ }
+
+ return true;
+ },
+
+ prepareSecurityInfo(
+ sendFlags,
+ uiFlags,
+ rcpt,
+ newSecurityInfo,
+ autocryptGossipHeaders
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSecurityInfo(): Using PGP/MIME, flags=" +
+ sendFlags +
+ "\n"
+ );
+
+ let oldSecurityInfo = Enigmail.msg.getSecurityParams();
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSecurityInfo: oldSecurityInfo = " +
+ oldSecurityInfo +
+ "\n"
+ );
+
+ if (!newSecurityInfo) {
+ this.createEnigmailSecurityFields(Enigmail.msg.getSecurityParams());
+ newSecurityInfo = Enigmail.msg.getSecurityParams().wrappedJSObject;
+ }
+
+ newSecurityInfo.originalSubject = gMsgCompose.compFields.subject;
+ newSecurityInfo.originalReferences = gMsgCompose.compFields.references;
+
+ if (sendFlags & EnigmailConstants.SEND_ENCRYPTED) {
+ if (sendFlags & EnigmailConstants.ENCRYPT_SUBJECT) {
+ gMsgCompose.compFields.subject = "";
+ }
+
+ if (Services.prefs.getBoolPref("temp.openpgp.protectReferencesHdr")) {
+ gMsgCompose.compFields.references = "";
+ }
+ }
+
+ newSecurityInfo.sendFlags = sendFlags;
+ newSecurityInfo.UIFlags = uiFlags;
+ newSecurityInfo.senderEmailAddr = rcpt.fromAddr;
+ newSecurityInfo.bccRecipients = rcpt.bccAddrStr;
+ newSecurityInfo.autocryptGossipHeaders = autocryptGossipHeaders;
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSecurityInfo: securityInfo = " +
+ newSecurityInfo +
+ "\n"
+ );
+ return newSecurityInfo;
+ },
+
+ async prepareSendMsg(msgSendType) {
+ // msgSendType: value from nsIMsgCompDeliverMode
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg: msgSendType=" +
+ msgSendType +
+ ", gSendSigned=" +
+ gSendSigned +
+ ", gSendEncrypted=" +
+ gSendEncrypted +
+ "\n"
+ );
+
+ const SIGN = EnigmailConstants.SEND_SIGNED;
+ const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;
+ const DeliverMode = Ci.nsIMsgCompDeliverMode;
+
+ var ioService = Services.io;
+ // EnigSend: Handle both plain and encrypted messages below
+ var isOffline = ioService && ioService.offline;
+
+ let senderKeyIsGnuPG =
+ Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg") &&
+ gCurrentIdentity.getBoolAttribute("is_gnupg_key_id");
+
+ let sendFlags = this.getEncryptionFlags();
+
+ switch (msgSendType) {
+ case DeliverMode.SaveAsDraft:
+ case DeliverMode.SaveAsTemplate:
+ case DeliverMode.AutoSaveAsDraft:
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg: detected save draft\n"
+ );
+
+ // saving drafts is simpler and works differently than the rest of Enigmail.
+ // All rules except account-settings are ignored.
+ return this.saveDraftMessage(senderKeyIsGnuPG);
+ }
+
+ this.unsetAdditionalHeader("x-enigmail-draft-status");
+
+ let msgCompFields = gMsgCompose.compFields;
+ let newsgroups = msgCompFields.newsgroups; // Check if sending to any newsgroups
+
+ if (
+ msgCompFields.to === "" &&
+ msgCompFields.cc === "" &&
+ msgCompFields.bcc === "" &&
+ newsgroups === ""
+ ) {
+ // don't attempt to send message if no recipient specified
+ var bundle = document.getElementById("bundle_composeMsgs");
+ EnigmailDialog.alert(window, bundle.getString("12511"));
+ return false;
+ }
+
+ let senderKeyId = gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
+
+ if ((gSendEncrypted || gSendSigned) && !senderKeyId) {
+ let msgId = gSendEncrypted
+ ? "cannot-send-enc-because-no-own-key"
+ : "cannot-send-sig-because-no-own-key";
+ let fullAlert = await document.l10n.formatValue(msgId, {
+ key: gCurrentIdentity.email,
+ });
+ EnigmailDialog.alert(window, fullAlert);
+ return false;
+ }
+
+ if (senderKeyIsGnuPG) {
+ sendFlags |= EnigmailConstants.SEND_SENDER_KEY_EXTERNAL;
+ }
+
+ if ((gSendEncrypted || gSendSigned) && senderKeyId) {
+ let senderKeyUsable = await EnigmailEncryption.determineOwnKeyUsability(
+ sendFlags,
+ senderKeyId,
+ senderKeyIsGnuPG
+ );
+ if (senderKeyUsable.errorMsg) {
+ let fullAlert = await document.l10n.formatValue(
+ "cannot-use-own-key-because",
+ {
+ problem: senderKeyUsable.errorMsg,
+ }
+ );
+ EnigmailDialog.alert(window, fullAlert);
+ return false;
+ }
+ }
+
+ let cannotEncryptMissingInfo = false;
+ if (gSendEncrypted) {
+ let canEncryptDetails = await this.determineSendFlags();
+ if (canEncryptDetails.errArray.length != 0) {
+ cannotEncryptMissingInfo = true;
+ }
+ }
+
+ if (gWindowLocked) {
+ EnigmailDialog.alert(
+ window,
+ await document.l10n.formatValue("window-locked")
+ );
+ return false;
+ }
+
+ let newSecurityInfo = this.resetDirty();
+ this.dirty = 1;
+
+ try {
+ this.modifiedAttach = null;
+
+ // fill fromAddr, toAddrList, bcc etc
+ let rcpt = await this.determineMsgRecipients(sendFlags);
+ if (typeof rcpt === "boolean") {
+ return rcpt;
+ }
+ sendFlags = rcpt.sendFlags;
+
+ if (cannotEncryptMissingInfo) {
+ showMessageComposeSecurityStatus(true);
+ return false;
+ }
+
+ if (this.sendPgpMime) {
+ // Use PGP/MIME
+ sendFlags |= EnigmailConstants.SEND_PGP_MIME;
+ }
+
+ let toAddrStr = rcpt.toAddrList.join(", ");
+ let bccAddrStr = rcpt.bccAddrList.join(", ");
+
+ if (gAttachMyPublicPGPKey) {
+ await this.attachOwnKey(senderKeyId);
+ }
+
+ let autocryptGossipHeaders = await this.getAutocryptGossip();
+
+ /*
+ if (this.preferPgpOverSmime(sendFlags) === 0) {
+ // use S/MIME
+ Attachments2CompFields(gMsgCompose.compFields); // update list of attachments
+ sendFlags = 0;
+ return true;
+ }
+ */
+
+ var usingPGPMime =
+ sendFlags & EnigmailConstants.SEND_PGP_MIME &&
+ sendFlags & (ENCRYPT | SIGN);
+
+ // ----------------------- Rewrapping code, taken from function "encryptInline"
+
+ if (sendFlags & ENCRYPT && !usingPGPMime) {
+ throw new Error("Sending encrypted inline not supported!");
+ }
+ if (sendFlags & SIGN && !usingPGPMime && gMsgCompose.composeHTML) {
+ throw new Error(
+ "Sending signed inline only supported for plain text composition!"
+ );
+ }
+
+ // Check wrapping, if sign only and inline and plaintext
+ if (
+ sendFlags & SIGN &&
+ !(sendFlags & ENCRYPT) &&
+ !usingPGPMime &&
+ !gMsgCompose.composeHTML
+ ) {
+ var wrapresultObj = {};
+
+ await this.wrapInLine(wrapresultObj);
+
+ if (wrapresultObj.usePpgMime) {
+ sendFlags |= EnigmailConstants.SEND_PGP_MIME;
+ usingPGPMime = EnigmailConstants.SEND_PGP_MIME;
+ }
+ if (wrapresultObj.cancelled) {
+ return false;
+ }
+ }
+
+ var uiFlags = EnigmailConstants.UI_INTERACTIVE;
+
+ if (usingPGPMime) {
+ uiFlags |= EnigmailConstants.UI_PGP_MIME;
+ }
+
+ if (sendFlags & (ENCRYPT | SIGN) && usingPGPMime) {
+ // Use PGP/MIME
+ newSecurityInfo = this.prepareSecurityInfo(
+ sendFlags,
+ uiFlags,
+ rcpt,
+ newSecurityInfo,
+ autocryptGossipHeaders
+ );
+ newSecurityInfo.recipients = toAddrStr;
+ newSecurityInfo.bccRecipients = bccAddrStr;
+ } else if (!this.processed && sendFlags & (ENCRYPT | SIGN)) {
+ // use inline PGP
+
+ let sendInfo = {
+ sendFlags,
+ fromAddr: rcpt.fromAddr,
+ toAddr: toAddrStr,
+ bccAddr: bccAddrStr,
+ uiFlags,
+ bucketList: document.getElementById("attachmentBucket"),
+ };
+
+ if (!(await this.signInline(sendInfo))) {
+ return false;
+ }
+ }
+
+ // update the list of attachments
+ Attachments2CompFields(msgCompFields);
+
+ if (
+ !this.prepareSending(
+ sendFlags,
+ rcpt.toAddrList.join(", "),
+ toAddrStr + ", " + bccAddrStr,
+ isOffline
+ )
+ ) {
+ return false;
+ }
+
+ if (msgCompFields.characterSet != "ISO-2022-JP") {
+ if (
+ (usingPGPMime && sendFlags & (ENCRYPT | SIGN)) ||
+ (!usingPGPMime && sendFlags & ENCRYPT)
+ ) {
+ try {
+ // make sure plaintext is not changed to 7bit
+ if (typeof msgCompFields.forceMsgEncoding == "boolean") {
+ msgCompFields.forceMsgEncoding = true;
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg: enabled forceMsgEncoding\n"
+ );
+ }
+ } catch (ex) {
+ console.debug(ex);
+ }
+ }
+ }
+ } catch (ex) {
+ EnigmailLog.writeException(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.prepareSendMsg",
+ ex
+ );
+ return false;
+ }
+
+ // The encryption process for PGP/MIME messages follows "here". It's
+ // called automatically from nsMsgCompose->sendMsg().
+ // registration for this is done in core.jsm: startup()
+
+ return true;
+ },
+
+ async signInline(sendInfo) {
+ // sign message using inline-PGP
+
+ if (sendInfo.sendFlags & ENCRYPT) {
+ throw new Error("Encryption not supported in inline messages!");
+ }
+ if (gMsgCompose.composeHTML) {
+ throw new Error(
+ "Signing inline only supported for plain text composition!"
+ );
+ }
+
+ const dce = Ci.nsIDocumentEncoder;
+ const SIGN = EnigmailConstants.SEND_SIGNED;
+ const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED;
+
+ var enigmailSvc = EnigmailCore.getService(window);
+ if (!enigmailSvc) {
+ return false;
+ }
+
+ if (Services.prefs.getBoolPref("mail.strictly_mime")) {
+ if (
+ EnigmailDialog.confirmIntPref(
+ window,
+ await l10nOpenPGP.formatValue("quoted-printable-warn"),
+ "temp.openpgp.quotedPrintableWarn"
+ )
+ ) {
+ Services.prefs.setBoolPref("mail.strictly_mime", false);
+ }
+ }
+
+ var sendFlowed = Services.prefs.getBoolPref(
+ "mailnews.send_plaintext_flowed"
+ );
+ var encoderFlags = dce.OutputFormatted | dce.OutputLFLineBreak;
+
+ // plaintext: Wrapping code has been moved to superordinate function prepareSendMsg to enable interactive format switch
+
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var errorMsgObj = {};
+ var exitCode;
+
+ // Get plain text
+ // (Do we need to set the nsIDocumentEncoder.* flags?)
+ var origText = this.editorGetContentAs("text/plain", encoderFlags);
+ if (!origText) {
+ origText = "";
+ }
+
+ if (origText.length > 0) {
+ // Sign/encrypt body text
+
+ var escText = origText; // Copy plain text for possible escaping
+
+ if (sendFlowed) {
+ // Prevent space stuffing a la RFC 2646 (format=flowed).
+
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: escText["+encoderFlags+"] = '"+escText+"'\n");
+
+ escText = escText.replace(/^From /gm, "~From ");
+ escText = escText.replace(/^>/gm, "|");
+ escText = escText.replace(/^[ \t]+$/gm, "");
+ escText = escText.replace(/^ /gm, "~ ");
+
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: escText = '"+escText+"'\n");
+ // Replace plain text and get it again
+ this.replaceEditorText(escText);
+
+ escText = this.editorGetContentAs("text/plain", encoderFlags);
+ }
+
+ // Replace plain text and get it again (to avoid linewrapping problems)
+ this.replaceEditorText(escText);
+
+ escText = this.editorGetContentAs("text/plain", encoderFlags);
+
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: escText["+encoderFlags+"] = '"+escText+"'\n");
+
+ var charset = this.editorGetCharset();
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.signInline: charset=" +
+ charset +
+ "\n"
+ );
+
+ // Encode plaintext to charset from unicode
+ var plainText = EnigmailData.convertFromUnicode(escText, charset);
+
+ // this will sign, not encrypt
+ var cipherText = EnigmailEncryption.encryptMessage(
+ window,
+ sendInfo.uiFlags,
+ plainText,
+ sendInfo.fromAddr,
+ sendInfo.toAddr,
+ sendInfo.bccAddr,
+ sendInfo.sendFlags,
+ exitCodeObj,
+ statusFlagsObj,
+ errorMsgObj
+ );
+
+ exitCode = exitCodeObj.value;
+
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: cipherText = '"+cipherText+"'\n");
+ if (cipherText && exitCode === 0) {
+ // Encryption/signing succeeded; overwrite plaintext
+
+ cipherText = cipherText.replace(/\r\n/g, "\n");
+
+ // Decode ciphertext from charset to unicode and overwrite
+ this.replaceEditorText(
+ EnigmailData.convertToUnicode(cipherText, charset)
+ );
+
+ // Save original text (for undo)
+ this.processed = {
+ origText,
+ charset,
+ };
+ } else {
+ // Restore original text
+ this.replaceEditorText(origText);
+
+ if (sendInfo.sendFlags & SIGN) {
+ // Encryption/signing failed
+
+ this.sendAborted(window, errorMsgObj);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ },
+
+ async sendAborted(window, errorMsgObj) {
+ if (errorMsgObj && errorMsgObj.value) {
+ var txt = errorMsgObj.value;
+ var txtLines = txt.split(/\r?\n/);
+ var errorMsg = "";
+ for (var i = 0; i < txtLines.length; ++i) {
+ var line = txtLines[i];
+ var tokens = line.split(/ /);
+ // process most important business reasons for invalid recipient (and sender) errors:
+ if (
+ tokens.length == 3 &&
+ (tokens[0] == "INV_RECP" || tokens[0] == "INV_SGNR")
+ ) {
+ var reason = tokens[1];
+ var key = tokens[2];
+ if (reason == "10") {
+ errorMsg +=
+ (await document.l10n.formatValue("key-not-trusted", { key })) +
+ "\n";
+ } else if (reason == "1") {
+ errorMsg +=
+ (await document.l10n.formatValue("key-not-found", { key })) +
+ "\n";
+ } else if (reason == "4") {
+ errorMsg +=
+ (await document.l10n.formatValue("key-revoked", { key })) + "\n";
+ } else if (reason == "5") {
+ errorMsg +=
+ (await document.l10n.formatValue("key-expired", { key })) + "\n";
+ }
+ }
+ }
+ if (errorMsg !== "") {
+ txt = errorMsg + "\n" + txt;
+ }
+ EnigmailDialog.info(
+ window,
+ (await document.l10n.formatValue("send-aborted")) + "\n" + txt
+ );
+ } else {
+ let [title, message] = await document.l10n.formatValues([
+ { id: "send-aborted" },
+ { id: "msg-compose-internal-error" },
+ ]);
+ EnigmailDialog.info(window, title + "\n" + message);
+ }
+ },
+
+ messageSendCheck() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.messageSendCheck\n"
+ );
+
+ try {
+ var warn = Services.prefs.getBoolPref("mail.warn_on_send_accel_key");
+
+ if (warn) {
+ var checkValue = {
+ value: false,
+ };
+ var bundle = document.getElementById("bundle_composeMsgs");
+ var buttonPressed = EnigmailDialog.getPromptSvc().confirmEx(
+ window,
+ bundle.getString("sendMessageCheckWindowTitle"),
+ bundle.getString("sendMessageCheckLabel"),
+ EnigmailDialog.getPromptSvc().BUTTON_TITLE_IS_STRING *
+ EnigmailDialog.getPromptSvc().BUTTON_POS_0 +
+ EnigmailDialog.getPromptSvc().BUTTON_TITLE_CANCEL *
+ EnigmailDialog.getPromptSvc().BUTTON_POS_1,
+ bundle.getString("sendMessageCheckSendButtonLabel"),
+ null,
+ null,
+ bundle.getString("CheckMsg"),
+ checkValue
+ );
+ if (buttonPressed !== 0) {
+ return false;
+ }
+ if (checkValue.value) {
+ Services.prefs.setBoolPref("mail.warn_on_send_accel_key", false);
+ }
+ }
+ } catch (ex) {}
+
+ return true;
+ },
+
+ /**
+ * set non-standard message Header
+ * (depending on TB version)
+ *
+ * hdr: String: header type (e.g. X-Enigmail-Version)
+ * val: String: header data (e.g. 1.2.3.4)
+ */
+ setAdditionalHeader(hdr, val) {
+ if ("otherRandomHeaders" in gMsgCompose.compFields) {
+ // TB <= 36
+ gMsgCompose.compFields.otherRandomHeaders += hdr + ": " + val + "\r\n";
+ } else {
+ gMsgCompose.compFields.setHeader(hdr, val);
+ }
+ },
+
+ unsetAdditionalHeader(hdr) {
+ gMsgCompose.compFields.deleteHeader(hdr);
+ },
+
+ // called just before sending
+ modifyCompFields() {
+ try {
+ if (
+ !Enigmail.msg.isEnigmailEnabledForIdentity() ||
+ !gCurrentIdentity.sendAutocryptHeaders
+ ) {
+ return;
+ }
+ if ((gSendSigned || gSendEncrypted) && !gSelectedTechnologyIsPGP) {
+ // If we're sending an S/MIME message, we don't want to send
+ // the OpenPGP autocrypt header.
+ return;
+ }
+ this.setAutocryptHeader();
+ } catch (ex) {
+ EnigmailLog.writeException(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.modifyCompFields",
+ ex
+ );
+ }
+ },
+
+ getCurrentIncomingServer() {
+ let currentAccountKey = getCurrentAccountKey();
+ let account = MailServices.accounts.getAccount(currentAccountKey);
+
+ return account.incomingServer; /* returns nsIMsgIncomingServer */
+ },
+
+ /**
+ * Obtain all Autocrypt-Gossip header lines that should be included in
+ * the outgoing message, excluding the sender's (from) email address.
+ * If there is just one recipient (ignoring the from address),
+ * no headers will be returned.
+ *
+ * @returns {string} - All header lines including line endings,
+ * could be the empty string.
+ */
+ async getAutocryptGossip() {
+ let fromMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.from);
+ let replyToMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.replyTo);
+
+ let optionalReplyToGossip = "";
+ if (replyToMail != fromMail) {
+ optionalReplyToGossip = ", " + gMsgCompose.compFields.replyTo;
+ }
+
+ // Assumes that extractHeaderAddressMailboxes will separate all
+ // entries with the sequence comma-space.
+ let allEmails = MailServices.headerParser
+ .extractHeaderAddressMailboxes(
+ gMsgCompose.compFields.to +
+ ", " +
+ gMsgCompose.compFields.cc +
+ optionalReplyToGossip
+ )
+ .split(/, /);
+
+ // Use a Set to ensure we have each address only once.
+ let uniqueEmails = new Set();
+ for (let e of allEmails) {
+ uniqueEmails.add(e);
+ }
+
+ // Potentially to/cc might contain the sender email address.
+ // Remove it, if it's there.
+ uniqueEmails.delete(fromMail);
+
+ // When sending to yourself, only, allEmails.length is 0.
+ // When sending to exactly one other person (with or without
+ // "from" in to/cc), then allEmails.length is 1. In that scenario,
+ // that recipient obviously already has their own key, and doesn't
+ // need the gossip. The sender's key will be included in the
+ // separate autocrypt (non-gossip) header.
+
+ if (uniqueEmails.size < 2) {
+ return "";
+ }
+
+ let gossip = "";
+ for (const email of uniqueEmails) {
+ let k = await EnigmailKeyRing.getRecipientAutocryptKeyForEmail(email);
+ if (!k) {
+ continue;
+ }
+ let keyData =
+ " " + k.replace(/(.{72})/g, "$1\r\n ").replace(/\r\n $/, "");
+ gossip +=
+ "Autocrypt-Gossip: addr=" + email + "; keydata=\r\n" + keyData + "\r\n";
+ }
+
+ return gossip;
+ },
+
+ setAutocryptHeader() {
+ let senderKeyId = gCurrentIdentity.getUnicharAttribute("openpgp_key_id");
+ if (!senderKeyId) {
+ return;
+ }
+
+ let fromMail = gCurrentIdentity.email;
+ try {
+ fromMail = EnigmailFuncs.stripEmail(gMsgCompose.compFields.from);
+ } catch (ex) {}
+
+ let keyData = EnigmailKeyRing.getAutocryptKey("0x" + senderKeyId, fromMail);
+
+ if (keyData) {
+ keyData =
+ " " + keyData.replace(/(.{72})/g, "$1\r\n ").replace(/\r\n $/, "");
+ this.setAdditionalHeader(
+ "Autocrypt",
+ "addr=" + fromMail + "; keydata=\r\n" + keyData
+ );
+ }
+ },
+
+ /**
+ * Handle the 'compose-send-message' event from TB
+ */
+ sendMessageListener(event) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.sendMessageListener\n"
+ );
+
+ let msgcomposeWindow = document.getElementById("msgcomposeWindow");
+ let sendMsgType = Number(msgcomposeWindow.getAttribute("msgtype"));
+
+ if (
+ !(
+ this.sendProcess &&
+ sendMsgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft
+ )
+ ) {
+ this.modifyCompFields();
+ if (!gSelectedTechnologyIsPGP) {
+ return;
+ }
+
+ this.sendProcess = true;
+ //let bc = document.getElementById("enigmail-bc-sendprocess");
+
+ try {
+ const cApi = EnigmailCryptoAPI();
+ let encryptResult = cApi.sync(this.prepareSendMsg(sendMsgType));
+ if (!encryptResult) {
+ this.resetUpdatedFields();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ } catch (ex) {
+ console.error("GenericSendMessage FAILED: " + ex);
+ this.resetUpdatedFields();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ } else {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.sendMessageListener: sending in progress - autosave aborted\n"
+ );
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ this.sendProcess = false;
+ },
+
+ async decryptQuote(interactive) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: " +
+ interactive +
+ "\n"
+ );
+
+ if (gWindowLocked || this.processed) {
+ return;
+ }
+
+ var enigmailSvc = EnigmailCore.getService(window);
+ if (!enigmailSvc) {
+ return;
+ }
+
+ const dce = Ci.nsIDocumentEncoder;
+ var encoderFlags = dce.OutputFormatted | dce.OutputLFLineBreak;
+
+ var docText = this.editorGetContentAs("text/plain", encoderFlags);
+
+ var blockBegin = docText.indexOf("-----BEGIN PGP ");
+ if (blockBegin < 0) {
+ return;
+ }
+
+ // Determine indentation string
+ var indentBegin = docText.substr(0, blockBegin).lastIndexOf("\n");
+ var indentStr = docText.substring(indentBegin + 1, blockBegin);
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: indentStr='" +
+ indentStr +
+ "'\n"
+ );
+
+ var beginIndexObj = {};
+ var endIndexObj = {};
+ var indentStrObj = {};
+ var blockType = EnigmailArmor.locateArmoredBlock(
+ docText,
+ 0,
+ indentStr,
+ beginIndexObj,
+ endIndexObj,
+ indentStrObj
+ );
+ if (blockType != "MESSAGE" && blockType != "SIGNED MESSAGE") {
+ return;
+ }
+
+ var beginIndex = beginIndexObj.value;
+ var endIndex = endIndexObj.value;
+
+ var head = docText.substr(0, beginIndex);
+ var tail = docText.substr(endIndex + 1);
+
+ var pgpBlock = docText.substr(beginIndex, endIndex - beginIndex + 1);
+ var indentRegexp;
+
+ if (indentStr) {
+ if (indentStr == "> ") {
+ // replace ">> " with "> > " to allow correct quoting
+ pgpBlock = pgpBlock.replace(/^>>/gm, "> >");
+ }
+
+ // Escape regex chars.
+ let escapedIndent1 = indentStr.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+
+ // Delete indentation
+ indentRegexp = new RegExp("^" + escapedIndent1, "gm");
+
+ pgpBlock = pgpBlock.replace(indentRegexp, "");
+ //tail = tail.replace(indentRegexp, "");
+
+ if (indentStr.match(/[ \t]*$/)) {
+ indentStr = indentStr.replace(/[ \t]*$/gm, "");
+ // Escape regex chars.
+ let escapedIndent2 = indentStr.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
+ indentRegexp = new RegExp("^" + escapedIndent2 + "$", "gm");
+
+ pgpBlock = pgpBlock.replace(indentRegexp, "");
+ }
+
+ // Handle blank indented lines
+ pgpBlock = pgpBlock.replace(/^[ \t]*>[ \t]*$/gm, "");
+ //tail = tail.replace(/^[ \t]*>[ \t]*$/g, "");
+
+ // Trim leading space in tail
+ tail = tail.replace(/^\s*\n/m, "\n");
+ }
+
+ if (tail.search(/\S/) < 0) {
+ // No non-space characters in tail; delete it
+ tail = "";
+ }
+
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: pgpBlock='"+pgpBlock+"'\n");
+
+ var charset = this.editorGetCharset();
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: charset=" +
+ charset +
+ "\n"
+ );
+
+ // Encode ciphertext from unicode to charset
+ var cipherText = EnigmailData.convertFromUnicode(pgpBlock, charset);
+
+ // Decrypt message
+ var signatureObj = {};
+ signatureObj.value = "";
+ var exitCodeObj = {};
+ var statusFlagsObj = {};
+ var userIdObj = {};
+ var keyIdObj = {};
+ var sigDetailsObj = {};
+ var errorMsgObj = {};
+ var blockSeparationObj = {};
+ var encToDetailsObj = {};
+
+ var uiFlags = EnigmailConstants.UI_UNVERIFIED_ENC_OK;
+
+ var plainText = "";
+
+ plainText = EnigmailDecryption.decryptMessage(
+ window,
+ uiFlags,
+ cipherText,
+ null, // date
+ signatureObj,
+ exitCodeObj,
+ statusFlagsObj,
+ keyIdObj,
+ userIdObj,
+ sigDetailsObj,
+ errorMsgObj,
+ blockSeparationObj,
+ encToDetailsObj
+ );
+ // Decode plaintext from charset to unicode
+ plainText = EnigmailData.convertToUnicode(plainText, charset).replace(
+ /\r\n/g,
+ "\n"
+ );
+
+ //if (Services.prefs.getBoolPref("temp.openpgp.keepSettingsForReply")) {
+ if (statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY) {
+ //this.setSendMode('encrypt');
+
+ // TODO : Check, when is this code reached?
+ // automatic enabling encryption currently depends on
+ // adjustSignEncryptAfterIdentityChanged to be always reached
+ gIsRelatedToEncryptedOriginal = true;
+ gSendEncrypted = true;
+ updateEncryptionDependencies();
+ }
+ //}
+
+ var exitCode = exitCodeObj.value;
+
+ if (exitCode !== 0) {
+ // Error processing
+ var errorMsg = errorMsgObj.value;
+
+ var statusLines = errorMsg ? errorMsg.split(/\r?\n/) : [];
+
+ var displayMsg;
+ if (statusLines && statusLines.length) {
+ // Display only first ten lines of error message
+ while (statusLines.length > 10) {
+ statusLines.pop();
+ }
+
+ displayMsg = statusLines.join("\n");
+
+ if (interactive) {
+ EnigmailDialog.info(window, displayMsg);
+ }
+ }
+ }
+
+ if (blockType == "MESSAGE" && exitCode === 0 && plainText.length === 0) {
+ plainText = " ";
+ }
+
+ if (!plainText) {
+ if (blockType != "SIGNED MESSAGE") {
+ return;
+ }
+
+ // Extract text portion of clearsign block
+ plainText = EnigmailArmor.extractSignaturePart(
+ pgpBlock,
+ EnigmailConstants.SIGNATURE_TEXT
+ );
+ }
+
+ const nsIMsgCompType = Ci.nsIMsgCompType;
+ var doubleDashSeparator = Services.prefs.getBoolPref(
+ "temp.openpgp.doubleDashSeparator"
+ );
+ if (
+ gMsgCompose.type != nsIMsgCompType.Template &&
+ gMsgCompose.type != nsIMsgCompType.Draft &&
+ doubleDashSeparator
+ ) {
+ var signOffset = plainText.search(/[\r\n]-- +[\r\n]/);
+
+ if (signOffset < 0 && blockType == "SIGNED MESSAGE") {
+ signOffset = plainText.search(/[\r\n]--[\r\n]/);
+ }
+
+ if (signOffset > 0) {
+ // Strip signature portion of quoted message
+ plainText = plainText.substr(0, signOffset + 1);
+ }
+ }
+
+ this.editorSelectAll();
+
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: plainText='"+plainText+"'\n");
+
+ if (head) {
+ this.editorInsertText(head);
+ }
+
+ var quoteElement;
+
+ if (indentStr) {
+ quoteElement = this.editorInsertAsQuotation(plainText);
+ } else {
+ this.editorInsertText(plainText);
+ }
+
+ if (tail) {
+ this.editorInsertText(tail);
+ }
+
+ if (statusFlagsObj.value & EnigmailConstants.DECRYPTION_OKAY) {
+ this.checkInlinePgpReply(head, tail);
+ }
+
+ if (interactive) {
+ return;
+ }
+
+ // Position cursor
+ var replyOnTop = gCurrentIdentity.replyOnTop;
+
+ if (!indentStr || !quoteElement) {
+ replyOnTop = 1;
+ }
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.decryptQuote: replyOnTop=" +
+ replyOnTop +
+ ", quoteElement=" +
+ quoteElement +
+ "\n"
+ );
+
+ if (this.editor.selectionController) {
+ var selection = this.editor.selectionController;
+ selection.completeMove(false, false); // go to start;
+
+ switch (replyOnTop) {
+ case 0:
+ // Position after quote
+ this.editor.endOfDocument();
+ if (tail) {
+ for (let cPos = 0; cPos < tail.length; cPos++) {
+ selection.characterMove(false, false); // move backwards
+ }
+ }
+ break;
+
+ case 2:
+ // Select quote
+
+ if (head) {
+ for (let cPos = 0; cPos < head.length; cPos++) {
+ selection.characterMove(true, false);
+ }
+ }
+ selection.completeMove(true, true);
+ if (tail) {
+ for (let cPos = 0; cPos < tail.length; cPos++) {
+ selection.characterMove(false, true); // move backwards
+ }
+ }
+ break;
+
+ default:
+ // Position at beginning of document
+
+ if (this.editor) {
+ this.editor.beginningOfDocument();
+ }
+ }
+
+ this.editor.selectionController.scrollSelectionIntoView(
+ Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
+ true
+ );
+ }
+
+ //this.processFinalState();
+ },
+
+ checkInlinePgpReply(head, tail) {
+ const CT = Ci.nsIMsgCompType;
+ let hLines = head.search(/[^\s>]/) < 0 ? 0 : 1;
+
+ if (hLines > 0) {
+ switch (gMsgCompose.type) {
+ case CT.Reply:
+ case CT.ReplyAll:
+ case CT.ReplyToSender:
+ case CT.ReplyToGroup:
+ case CT.ReplyToSenderAndGroup:
+ case CT.ReplyToList: {
+ // if head contains at only a few line of text, we assume it's the
+ // header above the quote (e.g. XYZ wrote:) and the user's signature
+
+ let h = head.split(/\r?\n/);
+ hLines = -1;
+
+ for (let i = 0; i < h.length; i++) {
+ if (h[i].search(/[^\s>]/) >= 0) {
+ hLines++;
+ }
+ }
+ }
+ }
+ }
+
+ if (
+ hLines > 0 &&
+ (!gCurrentIdentity.sigOnReply || gCurrentIdentity.sigBottom)
+ ) {
+ // display warning if no signature on top of message
+ this.displayPartialEncryptedWarning();
+ } else if (hLines > 10) {
+ this.displayPartialEncryptedWarning();
+ } else if (
+ tail.search(/[^\s>]/) >= 0 &&
+ !(gCurrentIdentity.sigOnReply && gCurrentIdentity.sigBottom)
+ ) {
+ // display warning if no signature below message
+ this.displayPartialEncryptedWarning();
+ }
+ },
+
+ editorInsertText(plainText) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertText\n"
+ );
+ if (this.editor) {
+ var mailEditor;
+ try {
+ mailEditor = this.editor.QueryInterface(Ci.nsIEditorMailSupport);
+ mailEditor.insertTextWithQuotations(plainText);
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertText: no mail editor\n"
+ );
+ this.editor.insertText(plainText);
+ }
+ }
+ },
+
+ editorInsertAsQuotation(plainText) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertAsQuotation\n"
+ );
+ if (this.editor) {
+ var mailEditor;
+ try {
+ mailEditor = this.editor.QueryInterface(Ci.nsIEditorMailSupport);
+ } catch (ex) {}
+
+ if (!mailEditor) {
+ return 0;
+ }
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.editorInsertAsQuotation: mailEditor=" +
+ mailEditor +
+ "\n"
+ );
+
+ mailEditor.insertAsCitedQuotation(plainText, "", false);
+
+ return 1;
+ }
+ return 0;
+ },
+
+ isSenderKeyExpired() {
+ const senderKeyId = this.getSenderUserId();
+
+ if (senderKeyId) {
+ const key = EnigmailKeyRing.getKeyById(senderKeyId);
+ return key?.expiryTime && Math.round(Date.now() / 1000) > key.expiryTime;
+ }
+
+ return false;
+ },
+
+ removeNotificationIfPresent(name) {
+ const notif = gComposeNotification.getNotificationWithValue(name);
+ if (notif) {
+ gComposeNotification.removeNotification(notif);
+ }
+ },
+
+ warnUserThatSenderKeyExpired() {
+ const label = {
+ "l10n-id": "openpgp-selection-status-error",
+ "l10n-args": { key: this.getSenderUserId() },
+ };
+
+ const buttons = [
+ {
+ "l10n-id": "settings-context-open-account-settings-item2",
+ callback() {
+ MsgAccountManager(
+ "am-e2e.xhtml",
+ MailServices.accounts.getServersForIdentity(gCurrentIdentity)[0]
+ );
+ Services.wm.getMostRecentWindow("mail:3pane")?.focus();
+ return true;
+ },
+ },
+ ];
+
+ gComposeNotification.appendNotification(
+ "openpgpSenderKeyExpired",
+ {
+ label,
+ priority: gComposeNotification.PRIORITY_WARNING_MEDIUM,
+ },
+ buttons
+ );
+ },
+
+ warnUserIfSenderKeyExpired() {
+ if (!this.isSenderKeyExpired()) {
+ this.removeNotificationIfPresent("openpgpSenderKeyExpired");
+ return;
+ }
+
+ this.warnUserThatSenderKeyExpired();
+ },
+
+ /**
+ * Display a notification to the user at the bottom of the window
+ *
+ * @param priority: Number - Priority of the message [1 = high (error) ... 3 = low (info)]
+ * @param msgText: String - Text to be displayed in notification bar
+ * @param messageId: String - Unique message type identification
+ * @param detailsText: String - optional text to be displayed by clicking on "Details" button.
+ * if null or "", then the Detail button will no be displayed.
+ */
+ async notifyUser(priority, msgText, messageId, detailsText) {
+ let prio;
+
+ switch (priority) {
+ case 1:
+ prio = gComposeNotification.PRIORITY_CRITICAL_MEDIUM;
+ break;
+ case 3:
+ prio = gComposeNotification.PRIORITY_INFO_MEDIUM;
+ break;
+ default:
+ prio = gComposeNotification.PRIORITY_WARNING_MEDIUM;
+ }
+
+ let buttonArr = [];
+
+ if (detailsText && detailsText.length > 0) {
+ let [accessKey, label] = await document.l10n.formatValues([
+ { id: "msg-compose-details-button-access-key" },
+ { id: "msg-compose-details-button-label" },
+ ]);
+
+ buttonArr.push({
+ accessKey,
+ label,
+ callback(aNotificationBar, aButton) {
+ EnigmailDialog.info(window, detailsText);
+ },
+ });
+ }
+ gComposeNotification.appendNotification(
+ messageId,
+ {
+ label: msgText,
+ priority: prio,
+ },
+ buttonArr
+ );
+ },
+
+ /**
+ * Display a warning message if we are replying to or forwarding
+ * a partially decrypted inline-PGP email
+ */
+ async displayPartialEncryptedWarning() {
+ let [msgLong, msgShort] = await document.l10n.formatValues([
+ { id: "msg-compose-partially-encrypted-inlinePGP" },
+ { id: "msg-compose-partially-encrypted-short" },
+ ]);
+
+ this.notifyUser(1, msgShort, "notifyPartialDecrypt", msgLong);
+ },
+
+ editorSelectAll() {
+ if (this.editor) {
+ this.editor.selectAll();
+ }
+ },
+
+ editorGetCharset() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.editorGetCharset\n"
+ );
+ return this.editor.documentCharacterSet;
+ },
+
+ editorGetContentAs(mimeType, flags) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: Enigmail.msg.editorGetContentAs\n"
+ );
+ if (this.editor) {
+ return this.editor.outputToString(mimeType, flags);
+ }
+
+ return null;
+ },
+
+ async focusChange() {
+ // call original TB function
+ CommandUpdate_MsgCompose();
+
+ var focusedWindow = top.document.commandDispatcher.focusedWindow;
+
+ // we're just setting focus to where it was before
+ if (focusedWindow == Enigmail.msg.lastFocusedWindow) {
+ // skip
+ return;
+ }
+
+ Enigmail.msg.lastFocusedWindow = focusedWindow;
+ },
+
+ /**
+ * Merge multiple Re: Re: into one Re: in message subject
+ */
+ fixMessageSubject() {
+ let subjElem = document.getElementById("msgSubject");
+ if (subjElem) {
+ let r = subjElem.value.replace(/^(Re: )+/, "Re: ");
+ if (r !== subjElem.value) {
+ subjElem.value = r;
+ if (typeof subjElem.oninput === "function") {
+ subjElem.oninput();
+ }
+ }
+ }
+ },
+};
+
+Enigmail.composeStateListener = {
+ NotifyComposeFieldsReady() {
+ // Note: NotifyComposeFieldsReady is only called when a new window is created (i.e. not in case a window object is reused).
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: ECSL.NotifyComposeFieldsReady\n"
+ );
+
+ try {
+ Enigmail.msg.editor = gMsgCompose.editor.QueryInterface(Ci.nsIEditor);
+ } catch (ex) {}
+
+ if (!Enigmail.msg.editor) {
+ return;
+ }
+
+ Enigmail.msg.fixMessageSubject();
+
+ function enigDocStateListener() {}
+
+ enigDocStateListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIDocumentStateListener"]),
+
+ NotifyDocumentWillBeDestroyed() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: EDSL.enigDocStateListener.NotifyDocumentWillBeDestroyed\n"
+ );
+ },
+
+ NotifyDocumentStateChanged(nowDirty) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: EDSL.enigDocStateListener.NotifyDocumentStateChanged\n"
+ );
+ },
+ };
+
+ var docStateListener = new enigDocStateListener();
+
+ Enigmail.msg.editor.addDocumentStateListener(docStateListener);
+ },
+
+ ComposeProcessDone(aResult) {
+ // Note: called after a mail was sent (or saved)
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: ECSL.ComposeProcessDone: " + aResult + "\n"
+ );
+
+ if (aResult != Cr.NS_OK) {
+ Enigmail.msg.removeAttachedKey();
+ }
+
+ // ensure that securityInfo is set back to S/MIME flags (especially required if draft was saved)
+ if (gSMFields) {
+ Enigmail.msg.setSecurityParams(gSMFields);
+ }
+ },
+
+ NotifyComposeBodyReady() {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: ECSL.ComposeBodyReady\n");
+
+ var isEmpty, isEditable;
+
+ isEmpty = Enigmail.msg.editor.documentIsEmpty;
+ isEditable = Enigmail.msg.editor.isDocumentEditable;
+ Enigmail.msg.composeBodyReady = true;
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgComposeOverlay.js: ECSL.ComposeBodyReady: isEmpty=" +
+ isEmpty +
+ ", isEditable=" +
+ isEditable +
+ "\n"
+ );
+
+ /*
+ if (Enigmail.msg.disableSmime) {
+ if (gMsgCompose && gMsgCompose.compFields && Enigmail.msg.getSecurityParams()) {
+ let si = Enigmail.msg.getSecurityParams(null);
+ si.signMessage = false;
+ si.requireEncryptMessage = false;
+ }
+ else {
+ EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: ECSL.ComposeBodyReady: could not disable S/MIME\n");
+ }
+ }
+ */
+
+ if (isEditable && !isEmpty) {
+ if (!Enigmail.msg.timeoutId && !Enigmail.msg.dirty) {
+ Enigmail.msg.timeoutId = setTimeout(function () {
+ Enigmail.msg.decryptQuote(false);
+ }, 0);
+ }
+ }
+
+ // This must be called by the last registered NotifyComposeBodyReady()
+ // stateListener. We need this in order to know when the entire init
+ // sequence of the composeWindow has finished, so the WebExtension compose
+ // API can do its final modifications.
+ window.composeEditorReady = true;
+ window.dispatchEvent(new CustomEvent("compose-editor-ready"));
+ },
+
+ SaveInFolderDone(folderURI) {
+ //EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: ECSL.SaveInFolderDone\n");
+ },
+};
+
+window.addEventListener(
+ "load",
+ Enigmail.msg.composeStartup.bind(Enigmail.msg),
+ {
+ capture: false,
+ once: true,
+ }
+);
+
+window.addEventListener("compose-window-unload", () => {
+ if (gMsgCompose) {
+ gMsgCompose.UnregisterStateListener(Enigmail.composeStateListener);
+ }
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js b/comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js
new file mode 100644
index 0000000000..5bb4619793
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js
@@ -0,0 +1,1214 @@
+/* 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";
+
+/* import-globals-from ../../../../base/content/aboutMessage.js */
+/* import-globals-from ../../../../base/content/msgHdrView.js */
+/* import-globals-from ../../../smime/content/msgHdrViewSMIMEOverlay.js */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailCore: "chrome://openpgp/content/modules/core.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",
+ EnigmailMime: "chrome://openpgp/content/modules/mime.jsm",
+ EnigmailMsgRead: "chrome://openpgp/content/modules/msgRead.jsm",
+ EnigmailSingletons: "chrome://openpgp/content/modules/singletons.jsm",
+ EnigmailURIs: "chrome://openpgp/content/modules/uris.jsm",
+ EnigmailVerify: "chrome://openpgp/content/modules/mimeVerify.jsm",
+ EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+ // EnigmailWks: "chrome://openpgp/content/modules/webKey.jsm",
+});
+
+Enigmail.hdrView = {
+ lastEncryptedMsgKey: null,
+ lastEncryptedUri: null,
+ flexbuttonAction: null,
+
+ msgSignedStateString: null,
+ msgEncryptedStateString: null,
+ msgSignatureState: EnigmailConstants.MSG_SIG_NONE,
+ msgEncryptionState: EnigmailConstants.MSG_ENC_NONE,
+ msgSignatureKeyId: "",
+ msgSignatureDate: null,
+ msgEncryptionKeyId: null,
+ msgEncryptionAllKeyIds: null,
+ msgHasKeyAttached: false,
+
+ ignoreStatusFromMimePart: "",
+ receivedStatusFromParts: new Set(),
+
+ reset() {
+ this.msgSignedStateString = null;
+ this.msgEncryptedStateString = null;
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_NONE;
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_NONE;
+ this.msgSignatureKeyId = "";
+ this.msgSignatureDate = null;
+ this.msgEncryptionKeyId = null;
+ this.msgEncryptionAllKeyIds = null;
+ this.msgHasKeyAttached = false;
+ for (let value of ["decryptionFailed", "brokenExchange"]) {
+ Enigmail.msg.removeNotification(value);
+ }
+ this.ignoreStatusFromMimePart = "";
+ this.receivedStatusFromParts = new Set();
+ },
+
+ hdrViewLoad() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.hdrViewLoad\n");
+
+ this.msgHdrViewLoad();
+
+ let addrPopup = document.getElementById("emailAddressPopup");
+ if (addrPopup) {
+ addrPopup.addEventListener(
+ "popupshowing",
+ Enigmail.hdrView.displayAddressPopup.bind(addrPopup)
+ );
+ }
+
+ // Thunderbird
+ let attCtx = document.getElementById("attachmentItemContext");
+ if (attCtx) {
+ attCtx.addEventListener(
+ "popupshowing",
+ this.onShowAttachmentContextMenu.bind(Enigmail.hdrView)
+ );
+ }
+ },
+
+ displayAddressPopup(event) {
+ let target = event.target;
+ EnigmailFuncs.collapseAdvanced(target, "hidden");
+ },
+
+ statusBarHide() {
+ /* elements might not have been set yet, so we try and ignore */
+ try {
+ this.reset();
+
+ Enigmail.msg.setAttachmentReveal(null);
+ if (Enigmail.msg.securityInfo) {
+ Enigmail.msg.securityInfo.statusFlags = 0;
+ }
+
+ let bodyElement = document.getElementById("messagepane");
+ bodyElement.removeAttribute("collapsed");
+ } catch (ex) {
+ console.debug(ex);
+ }
+ },
+
+ updatePgpStatus(
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ encToDetails,
+ xtraStatus,
+ mimePartNumber
+ ) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: this.updatePgpStatus: exitCode=" +
+ exitCode +
+ ", statusFlags=" +
+ statusFlags +
+ ", extStatusFlags=" +
+ extStatusFlags +
+ ", keyId=" +
+ keyId +
+ ", userId=" +
+ userId +
+ ", " +
+ errorMsg +
+ "\n"
+ );
+
+ /*
+ if (
+ Enigmail.msg.securityInfo &&
+ Enigmail.msg.securityInfo.xtraStatus &&
+ Enigmail.msg.securityInfo.xtraStatus === "wks-request"
+ ) {
+ return;
+ }
+ */
+
+ if (gMessageURI) {
+ this.lastEncryptedMsgKey = gMessageURI;
+ }
+
+ if (!errorMsg) {
+ errorMsg = "";
+ } else {
+ console.debug("OpenPGP error status: " + errorMsg);
+ }
+
+ var replaceUid = null;
+ if (keyId && gMessage) {
+ replaceUid = EnigmailMsgRead.matchUidToSender(keyId, gMessage.author);
+ }
+
+ if (!replaceUid && userId) {
+ replaceUid = userId.replace(/\n.*$/gm, "");
+ }
+
+ if (
+ Enigmail.msg.savedHeaders &&
+ "x-pgp-encoding-format" in Enigmail.msg.savedHeaders &&
+ Enigmail.msg.savedHeaders["x-pgp-encoding-format"].search(
+ /partitioned/i
+ ) === 0
+ ) {
+ if (currentAttachments && currentAttachments.length) {
+ Enigmail.msg.setAttachmentReveal(currentAttachments);
+ }
+ }
+
+ if (userId && replaceUid) {
+ // no EnigmailData.convertGpgToUnicode here; strings are already UTF-8
+ replaceUid = replaceUid.replace(/\\[xe]3a/gi, ":");
+ errorMsg = errorMsg.replace(userId, replaceUid);
+ }
+
+ var errorLines = "";
+
+ if (exitCode == EnigmailConstants.POSSIBLE_PGPMIME) {
+ exitCode = 0;
+ } else if (errorMsg) {
+ // no EnigmailData.convertGpgToUnicode here; strings are already UTF-8
+ errorLines = errorMsg.split(/\r?\n/);
+ }
+
+ if (errorLines && errorLines.length > 22) {
+ // Retain only first twenty lines and last two lines of error message
+ var lastLines =
+ errorLines[errorLines.length - 2] +
+ "\n" +
+ errorLines[errorLines.length - 1] +
+ "\n";
+
+ while (errorLines.length > 20) {
+ errorLines.pop();
+ }
+
+ errorMsg = errorLines.join("\n") + "\n...\n" + lastLines;
+ }
+
+ let encryptedMimePart = "";
+ if (statusFlags & EnigmailConstants.PGP_MIME_ENCRYPTED) {
+ encryptedMimePart = mimePartNumber;
+ }
+
+ var msgSigned =
+ statusFlags &
+ (EnigmailConstants.BAD_SIGNATURE |
+ EnigmailConstants.GOOD_SIGNATURE |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE |
+ EnigmailConstants.UNCERTAIN_SIGNATURE |
+ EnigmailConstants.REVOKED_KEY |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE);
+
+ if (msgSigned && statusFlags & EnigmailConstants.IMPORTED_KEY) {
+ console.debug("unhandled status IMPORTED_KEY");
+ statusFlags &= ~EnigmailConstants.IMPORTED_KEY;
+ }
+
+ // TODO: visualize the following signature attributes,
+ // cross-check with corresponding email attributes
+ // - date
+ // - signer uid
+ // - signer key
+ // - signing and hash alg
+
+ this.msgSignatureKeyId = keyId;
+
+ if (encToDetails) {
+ this.msgEncryptionKeyId = encToDetails.myRecipKey;
+ this.msgEncryptionAllKeyIds = encToDetails.allRecipKeys;
+ }
+
+ this.msgSignatureDate = sigDetails?.sigDate;
+
+ let tmp = {
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ msgSigned,
+ blockSeparation,
+ xtraStatus,
+ encryptedMimePart,
+ };
+ Enigmail.msg.securityInfo = tmp;
+
+ //Enigmail.msg.createArtificialAutocryptHeader();
+
+ /*
+ if (statusFlags & EnigmailConstants.UNCERTAIN_SIGNATURE) {
+ this.tryImportAutocryptHeader();
+ }
+ */
+
+ this.updateStatusFlags(mimePartNumber);
+ this.updateMsgDb();
+ },
+
+ /**
+ * Check whether we got a WKS request
+ */
+ /*
+ checkWksConfirmRequest(jsonStr) {
+ let requestObj;
+ try {
+ requestObj = JSON.parse(jsonStr);
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: checkWksConfirmRequest parsing JSON failed\n"
+ );
+ return;
+ }
+
+ if (
+ "type" in requestObj &&
+ requestObj.type.toLowerCase() === "confirmation-request"
+ ) {
+ EnigmailWks.getWksClientPathAsync(window, function(wksClientPath) {
+ if (!wksClientPath) {
+ return;
+ }
+
+ Enigmail.hdrView.displayFlexAction(
+ "Web Key Directory Confirmation Request",
+ "Confirm Request",
+ "wks-request"
+ );
+ Enigmail.hdrView.displayWksMessage();
+ });
+ } else {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: checkWksConfirmRequest failed condition\n"
+ );
+ }
+ },
+ */
+
+ /**
+ * Display a localized message in lieu of the original message text
+ */
+ /*
+ displayWksMessage() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: displayWksMessage()\n");
+
+ if (Enigmail.msg.securityInfo.xtraStatus === "wks-request") {
+ let enigMsgPane = document.getElementById("enigmailMsgDisplay");
+ let bodyElement = document.getElementById("messagepane");
+ bodyElement.setAttribute("collapsed", true);
+ enigMsgPane.removeAttribute("collapsed");
+ enigMsgPane.textContent = "This message has been sent by your email provider to confirm deployment of your OpenPGP public key\nin their Web Key Directory.\nProviding your public key helps others to discover your key and thus being able to encrypt messages to you.\n\nIf you want to deploy your key in the Web Key Directory now, please click on the button "Confirm Request" in the status bar.\nOtherwise, simply ignore this message."
+ );
+ }
+ },
+ */
+
+ /**
+ * Update the various variables that track the OpenPGP status of
+ * the current message.
+ *
+ * @param {string} triggeredByMimePartNumber - the MIME part that
+ * was processed and has triggered this status update request.
+ */
+ async updateStatusFlags(triggeredByMimePartNumber) {
+ let secInfo = Enigmail.msg.securityInfo;
+ let statusFlags = secInfo.statusFlags;
+ let extStatusFlags =
+ "extStatusFlags" in secInfo ? secInfo.extStatusFlags : 0;
+
+ let signed;
+ let encrypted;
+
+ if (
+ statusFlags &
+ (EnigmailConstants.DECRYPTION_FAILED |
+ EnigmailConstants.DECRYPTION_INCOMPLETE)
+ ) {
+ encrypted = "notok";
+ let unhideBar = false;
+ let infoId;
+ if (statusFlags & EnigmailConstants.NO_SECKEY) {
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_NO_SECRET_KEY;
+
+ unhideBar = true;
+ infoId = "openpgp-cannot-decrypt-because-missing-key";
+ } else {
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_FAILURE;
+ if (statusFlags & EnigmailConstants.MISSING_MDC) {
+ unhideBar = true;
+ infoId = "openpgp-cannot-decrypt-because-mdc";
+ }
+ }
+
+ if (unhideBar) {
+ Enigmail.msg.notificationBox.appendNotification(
+ "decryptionFailed",
+ {
+ label: await document.l10n.formatValue(infoId),
+ image: "chrome://global/skin/icons/warning.svg",
+ priority: Enigmail.msg.notificationBox.PRIORITY_CRITICAL_MEDIUM,
+ },
+ null
+ );
+ }
+
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_NONE;
+ } else if (statusFlags & EnigmailConstants.DECRYPTION_OKAY) {
+ EnigmailURIs.rememberEncryptedUri(this.lastEncryptedMsgKey);
+ encrypted = "ok";
+ this.msgEncryptionState = EnigmailConstants.MSG_ENC_OK;
+ if (secInfo.xtraStatus && secInfo.xtraStatus == "buggyMailFormat") {
+ console.log(
+ await document.l10n.formatValue("decrypted-msg-with-format-error")
+ );
+ }
+ }
+
+ if (
+ statusFlags &
+ (EnigmailConstants.BAD_SIGNATURE |
+ EnigmailConstants.REVOKED_KEY |
+ EnigmailConstants.EXPIRED_KEY_SIGNATURE |
+ EnigmailConstants.EXPIRED_SIGNATURE)
+ ) {
+ if (statusFlags & EnigmailConstants.INVALID_RECIPIENT) {
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_INVALID_KEY_REJECTED;
+ } else {
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_INVALID;
+ }
+ signed = "notok";
+ } else if (statusFlags & EnigmailConstants.GOOD_SIGNATURE) {
+ if (statusFlags & EnigmailConstants.TRUSTED_IDENTITY) {
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_VALID_KEY_VERIFIED;
+ signed = "verified";
+ } else if (extStatusFlags & EnigmailConstants.EXT_SELF_IDENTITY) {
+ signed = "ok";
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_VALID_SELF;
+ } else {
+ signed = "unverified";
+ this.msgSignatureState = EnigmailConstants.MSG_SIG_VALID_KEY_UNVERIFIED;
+ }
+ } else if (statusFlags & EnigmailConstants.UNCERTAIN_SIGNATURE) {
+ signed = "unknown";
+ if (statusFlags & EnigmailConstants.INVALID_RECIPIENT) {
+ signed = "mismatch";
+ this.msgSignatureState =
+ EnigmailConstants.MSG_SIG_UNCERTAIN_UID_MISMATCH;
+ } else if (statusFlags & EnigmailConstants.NO_PUBKEY) {
+ this.msgSignatureState =
+ EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_UNAVAILABLE;
+ Enigmail.msg.notifySigKeyMissing(secInfo.keyId);
+ } else {
+ this.msgSignatureState =
+ EnigmailConstants.MSG_SIG_UNCERTAIN_KEY_NOT_ACCEPTED;
+ }
+ }
+ // (statusFlags & EnigmailConstants.INLINE_KEY) ???
+
+ if (encrypted) {
+ this.msgEncryptedStateString = encrypted;
+ }
+ if (signed) {
+ this.msgSignedStateString = signed;
+ }
+ this.updateVisibleSecurityStatus(triggeredByMimePartNumber);
+
+ /*
+ // special handling after trying to fix buggy mail format (see buggyExchangeEmailContent in code)
+ if (secInfo.xtraStatus && secInfo.xtraStatus == "buggyMailFormat") {
+ }
+ */
+
+ if (encrypted) {
+ // For telemetry purposes.
+ window.dispatchEvent(
+ new CustomEvent("secureMsgLoaded", {
+ detail: {
+ key: "encrypted-openpgp",
+ data: encrypted,
+ },
+ })
+ );
+ }
+ if (signed) {
+ window.dispatchEvent(
+ new CustomEvent("secureMsgLoaded", {
+ detail: {
+ key: "signed-openpgp",
+ data: signed,
+ },
+ })
+ );
+ }
+ },
+
+ /**
+ * Should be called as soon as it is known that the message has
+ * an OpenPGP key attached.
+ */
+ notifyHasKeyAttached() {
+ this.msgHasKeyAttached = true;
+ this.updateVisibleSecurityStatus();
+ },
+
+ /**
+ * Should be called whenever more information about the OpenPGP
+ * message state became available, such as encryption or signature
+ * status, or the availability of an attached key.
+ *
+ * @param {string} triggeredByMimePartNumber - optional number of the
+ * MIME part that was processed and has triggered this status update
+ * request.
+ */
+ updateVisibleSecurityStatus(triggeredByMimePartNumber = undefined) {
+ setMessageCryptoBox(
+ "OpenPGP",
+ this.msgEncryptedStateString,
+ this.msgSignedStateString,
+ this.msgHasKeyAttached,
+ triggeredByMimePartNumber
+ );
+ },
+
+ editKeyExpiry() {
+ EnigmailWindows.editKeyExpiry(
+ window,
+ [Enigmail.msg.securityInfo.userId],
+ [Enigmail.msg.securityInfo.keyId]
+ );
+ ReloadMessage();
+ },
+
+ editKeyTrust() {
+ let key = EnigmailKeyRing.getKeyById(Enigmail.msg.securityInfo.keyId);
+
+ EnigmailWindows.editKeyTrust(
+ window,
+ [Enigmail.msg.securityInfo.userId],
+ [key.keyId]
+ );
+ ReloadMessage();
+ },
+
+ signKey() {
+ let key = EnigmailKeyRing.getKeyById(Enigmail.msg.securityInfo.keyId);
+
+ EnigmailWindows.signKey(
+ window,
+ Enigmail.msg.securityInfo.userId,
+ key.keyId,
+ null
+ );
+ ReloadMessage();
+ },
+
+ msgHdrViewLoad() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.msgHdrViewLoad\n");
+
+ this.messageListener = {
+ onStartHeaders() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: _listener_onStartHeaders\n"
+ );
+
+ try {
+ Enigmail.hdrView.statusBarHide();
+ EnigmailVerify.setWindow(window, Enigmail.msg.getCurrentMsgUriSpec());
+
+ let msgFrame = document.getElementById("messagepane").contentDocument;
+
+ if (msgFrame) {
+ msgFrame.addEventListener(
+ "unload",
+ Enigmail.hdrView.messageUnload.bind(Enigmail.hdrView),
+ true
+ );
+ msgFrame.addEventListener(
+ "load",
+ Enigmail.hdrView.messageLoad.bind(Enigmail.hdrView),
+ true
+ );
+ }
+
+ Enigmail.hdrView.forgetEncryptedMsgKey();
+ Enigmail.hdrView.setWindowCallback();
+ } catch (ex) {
+ console.debug(ex);
+ }
+ },
+
+ onEndHeaders() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: _listener_onEndHeaders\n"
+ );
+ },
+
+ onEndAttachments() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: _listener_onEndAttachments\n"
+ );
+
+ try {
+ EnigmailVerify.setWindow(null, null);
+ } catch (ex) {}
+
+ Enigmail.hdrView.messageLoad();
+ },
+
+ beforeStartHeaders() {
+ return true;
+ },
+ };
+
+ gMessageListeners.push(this.messageListener);
+
+ // fire the handlers since some windows open directly with a visible message
+ this.messageListener.onStartHeaders();
+ this.messageListener.onEndAttachments();
+ },
+
+ messageUnload(event) {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.messageUnload\n");
+ if (Enigmail.hdrView.flexbuttonAction === null) {
+ if (Enigmail.msg.securityInfo && Enigmail.msg.securityInfo.xtraStatus) {
+ Enigmail.msg.securityInfo.xtraStatus = "";
+ }
+ this.forgetEncryptedMsgKey();
+ }
+ },
+
+ async messageLoad(event) {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.messageLoad\n");
+
+ await Enigmail.msg.messageAutoDecrypt();
+ Enigmail.msg.handleAttachmentEvent();
+ },
+
+ dispKeyDetails() {
+ if (!Enigmail.msg.securityInfo) {
+ return;
+ }
+
+ let key = EnigmailKeyRing.getKeyById(Enigmail.msg.securityInfo.keyId);
+
+ EnigmailWindows.openKeyDetails(window, key.keyId, false);
+ },
+
+ forgetEncryptedMsgKey() {
+ if (Enigmail.hdrView.lastEncryptedMsgKey) {
+ EnigmailURIs.forgetEncryptedUri(Enigmail.hdrView.lastEncryptedMsgKey);
+ Enigmail.hdrView.lastEncryptedMsgKey = null;
+ }
+
+ if (Enigmail.hdrView.lastEncryptedUri && gEncryptedURIService) {
+ gEncryptedURIService.forgetEncrypted(Enigmail.hdrView.lastEncryptedUri);
+ Enigmail.hdrView.lastEncryptedUri = null;
+ }
+ },
+
+ onShowAttachmentContextMenu(event) {
+ let contextMenu = document.getElementById("attachmentItemContext");
+ let separator = document.getElementById("openpgpCtxItemsSeparator");
+ let decryptOpenMenu = document.getElementById("enigmail_ctxDecryptOpen");
+ let decryptSaveMenu = document.getElementById("enigmail_ctxDecryptSave");
+ let importMenu = document.getElementById("enigmail_ctxImportKey");
+ let verifyMenu = document.getElementById("enigmail_ctxVerifyAtt");
+
+ if (contextMenu.attachments.length == 1) {
+ let attachment = contextMenu.attachments[0];
+
+ if (/^application\/pgp-keys/i.test(attachment.contentType)) {
+ importMenu.hidden = false;
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+ verifyMenu.hidden = true;
+ } else if (Enigmail.msg.checkEncryptedAttach(attachment)) {
+ if (
+ (typeof attachment.name !== "undefined" &&
+ attachment.name.match(/\.asc\.(gpg|pgp)$/i)) ||
+ (typeof attachment.displayName !== "undefined" &&
+ attachment.displayName.match(/\.asc\.(gpg|pgp)$/i))
+ ) {
+ importMenu.hidden = false;
+ } else {
+ importMenu.hidden = true;
+ }
+ decryptOpenMenu.hidden = false;
+ decryptSaveMenu.hidden = false;
+ if (
+ EnigmailMsgRead.checkSignedAttachment(
+ attachment,
+ null,
+ currentAttachments
+ )
+ ) {
+ verifyMenu.hidden = false;
+ } else {
+ verifyMenu.hidden = true;
+ }
+ if (typeof attachment.displayName == "undefined") {
+ if (!attachment.name) {
+ attachment.name = "message.pgp";
+ }
+ } else if (!attachment.displayName) {
+ attachment.displayName = "message.pgp";
+ }
+ } else if (
+ EnigmailMsgRead.checkSignedAttachment(
+ attachment,
+ null,
+ currentAttachments
+ )
+ ) {
+ importMenu.hidden = true;
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+
+ verifyMenu.hidden = false;
+ } else {
+ importMenu.hidden = true;
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+ verifyMenu.hidden = true;
+ }
+
+ separator.hidden =
+ decryptOpenMenu.hidden &&
+ decryptSaveMenu.hidden &&
+ importMenu.hidden &&
+ verifyMenu.hidden;
+ } else {
+ decryptOpenMenu.hidden = true;
+ decryptSaveMenu.hidden = true;
+ importMenu.hidden = true;
+ verifyMenu.hidden = true;
+ separator.hidden = true;
+ }
+ },
+
+ updateMsgDb() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: this.updateMsgDb\n");
+ var msg = gMessage;
+ if (!msg || !msg.folder) {
+ return;
+ }
+
+ var msgHdr = msg.folder.GetMessageHeader(msg.messageKey);
+
+ if (this.msgEncryptionState === EnigmailConstants.MSG_ENC_OK) {
+ Enigmail.msg.securityInfo.statusFlags |=
+ EnigmailConstants.DECRYPTION_OKAY;
+ }
+ msgHdr.setUint32Property("enigmail", Enigmail.msg.securityInfo.statusFlags);
+ },
+
+ enigCanDetachAttachments() {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: this.enigCanDetachAttachments\n"
+ );
+
+ var canDetach = true;
+ if (
+ Enigmail.msg.securityInfo &&
+ typeof Enigmail.msg.securityInfo.statusFlags != "undefined"
+ ) {
+ canDetach = !(
+ Enigmail.msg.securityInfo.statusFlags &
+ (EnigmailConstants.PGP_MIME_SIGNED |
+ EnigmailConstants.PGP_MIME_ENCRYPTED)
+ );
+ }
+ return canDetach;
+ },
+
+ setSubject(subject) {
+ // Strip multiple localized Re: prefixes. This emulates NS_MsgStripRE().
+ let prefixes = Services.prefs
+ .getComplexValue("mailnews.localizedRe", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .filter(Boolean);
+ if (!prefixes.includes("Re")) {
+ prefixes.push("Re");
+ }
+ // Construct a regular expression like this: ^(Re: |Aw: )+
+ let newSubject = subject.replace(
+ new RegExp(`^(${prefixes.join(": |")}: )+`, "i"),
+ ""
+ );
+ let hadRe = newSubject != subject;
+
+ // Update the message.
+ gMessage.subject = newSubject;
+ let oldFlags = gMessage.flags;
+ if (hadRe) {
+ gMessage.flags |= Ci.nsMsgMessageFlags.HasRe;
+ newSubject = "Re: " + newSubject;
+ }
+ document.title = newSubject;
+ currentHeaderData.subject.headerValue = newSubject;
+ document.getElementById("expandedsubjectBox").headerValue = newSubject;
+ // This even works if the flags haven't changed. Causes repaint in all thread trees.
+ gMessage.folder?.msgDatabase.notifyHdrChangeAll(
+ gMessage,
+ oldFlags,
+ gMessage.flags,
+ {}
+ );
+ },
+
+ updateHdrBox(header, value) {
+ let e = document.getElementById("expanded" + header + "Box");
+ if (e) {
+ e.headerValue = value;
+ }
+ },
+
+ setWindowCallback() {
+ EnigmailLog.DEBUG("enigmailMsgHdrViewOverlay.js: setWindowCallback\n");
+
+ EnigmailSingletons.messageReader = this.headerPane;
+ },
+
+ clearWindowCallback() {
+ if (EnigmailSingletons.messageReader == this.headerPane) {
+ EnigmailSingletons.messageReader = null;
+ }
+ },
+
+ headerPane: {
+ isCurrentMessage(uri) {
+ let uriSpec = uri ? uri.spec : null;
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: EnigMimeHeaderSink.isCurrentMessage: uri.spec=" +
+ uriSpec +
+ "\n"
+ );
+
+ return true;
+ },
+
+ /**
+ * Determine if a given MIME part number is a multipart/related message or a child thereof
+ *
+ * @param mimePart: Object - The MIME Part object to evaluate from the MIME tree
+ * @param searchPartNum: String - The part number to determine
+ */
+ isMultipartRelated(mimePart, searchPartNum) {
+ if (
+ searchPartNum.indexOf(mimePart.partNum) == 0 &&
+ mimePart.partNum.length <= searchPartNum.length
+ ) {
+ if (mimePart.fullContentType.search(/^multipart\/related/i) === 0) {
+ return true;
+ }
+
+ for (let i in mimePart.subParts) {
+ if (this.isMultipartRelated(mimePart.subParts[i], searchPartNum)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Determine if a given mime part number should be displayed.
+ * Returns true if one of these conditions is true:
+ * - this is the 1st displayed block of the message
+ * - the message part displayed corresponds to the decrypted part
+ *
+ * @param mimePartNumber: String - the MIME part number that was decrypted/verified
+ * @param uriSpec: String - the URI spec that is being displayed
+ */
+ displaySubPart(mimePartNumber, uriSpec) {
+ if (!mimePartNumber || !uriSpec) {
+ return true;
+ }
+ let part = EnigmailMime.getMimePartNumber(uriSpec);
+
+ if (part.length === 0) {
+ // only display header if 1st message part
+ if (mimePartNumber.search(/^1(\.1)*$/) < 0) {
+ return false;
+ }
+ } else {
+ let r = EnigmailFuncs.compareMimePartLevel(mimePartNumber, part);
+
+ // analyzed mime part is contained in viewed message part
+ if (r === 2) {
+ if (mimePartNumber.substr(part.length).search(/^\.1(\.1)*$/) < 0) {
+ return false;
+ }
+ } else if (r !== 0) {
+ return false;
+ }
+
+ if (Enigmail.msg.mimeParts) {
+ if (this.isMultipartRelated(Enigmail.msg.mimeParts, mimePartNumber)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Determine if there are message parts that are not encrypted
+ *
+ * @param mimePartNumber String - the MIME part number that was authenticated
+ *
+ * @returns Boolean: true: there are siblings / false: no siblings
+ */
+ hasUnauthenticatedParts(mimePartNumber) {
+ function hasUnauthenticatedSiblings(
+ mimeSubTree,
+ mimePartToCheck,
+ parentOfMimePartToCheck
+ ) {
+ if (mimeSubTree.partNum === parentOfMimePartToCheck) {
+ // If this is an encrypted message that is the parent of mimePartToCheck,
+ // then we know that all its childs (including mimePartToCheck) are authenticated.
+ if (
+ mimeSubTree.fullContentType.search(
+ /^multipart\/encrypted.{1,255}protocol="?application\/pgp-encrypted"?/i
+ ) === 0
+ ) {
+ return false;
+ }
+ }
+ if (
+ mimeSubTree.partNum.indexOf(parentOfMimePartToCheck) == 0 &&
+ mimeSubTree.partNum !== mimePartToCheck
+ ) {
+ // This is a sibling (same parent, different part number).
+ return true;
+ }
+
+ for (let i in mimeSubTree.subParts) {
+ if (
+ hasUnauthenticatedSiblings(
+ mimeSubTree.subParts[i],
+ mimePartToCheck,
+ parentOfMimePartToCheck
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (!mimePartNumber || !Enigmail.msg.mimeParts) {
+ return false;
+ }
+
+ let parentNum = "";
+ if (mimePartNumber.includes(".")) {
+ parentNum = mimePartNumber.replace(/\.\d+$/, "");
+ }
+
+ return hasUnauthenticatedSiblings(
+ Enigmail.msg.mimeParts,
+ mimePartNumber,
+ parentNum
+ );
+ },
+
+ /**
+ * Request that OpenPGP security status from the given MIME part
+ * shall be ignored (not shown in the UI). If status for that
+ * MIME part was already received, then reset the status.
+ *
+ * @param {string} originMimePartNumber - Ignore security status
+ * of this MIME part.
+ */
+ ignoreStatusFrom(originMimePartNumber) {
+ Enigmail.hdrView.ignoreStatusFromMimePart = originMimePartNumber;
+ setIgnoreStatusFromMimePart(originMimePartNumber);
+ if (Enigmail.hdrView.receivedStatusFromParts.has(originMimePartNumber)) {
+ Enigmail.hdrView.reset();
+ Enigmail.hdrView.ignoreStatusFromMimePart = originMimePartNumber;
+ }
+ },
+
+ async updateSecurityStatus(
+ unusedUriSpec,
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ uri,
+ extraDetails,
+ mimePartNumber
+ ) {
+ if (
+ Enigmail.hdrView.ignoreStatusFromMimePart != "" &&
+ mimePartNumber == Enigmail.hdrView.ignoreStatusFromMimePart
+ ) {
+ return;
+ }
+
+ Enigmail.hdrView.receivedStatusFromParts.add(mimePartNumber);
+
+ // uriSpec is not used for Enigmail anymore. It is here because other addons and pEp rely on it
+
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: updateSecurityStatus: mimePart=" +
+ mimePartNumber +
+ "\n"
+ );
+
+ let uriSpec = uri ? uri.spec : null;
+
+ if (this.isCurrentMessage(uri)) {
+ if (statusFlags & EnigmailConstants.DECRYPTION_OKAY) {
+ if (gEncryptedURIService) {
+ // remember encrypted message URI to enable TB prevention against EFAIL attack
+ Enigmail.hdrView.lastEncryptedUri = gMessageURI;
+ gEncryptedURIService.rememberEncrypted(
+ Enigmail.hdrView.lastEncryptedUri
+ );
+ }
+ }
+
+ if (!this.displaySubPart(mimePartNumber, uriSpec)) {
+ return;
+ }
+ if (this.hasUnauthenticatedParts(mimePartNumber)) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: updateSecurityStatus: found unauthenticated part\n"
+ );
+ statusFlags |= EnigmailConstants.PARTIALLY_PGP;
+ }
+
+ let encToDetails = null;
+
+ if (extraDetails && extraDetails.length > 0) {
+ try {
+ let o = JSON.parse(extraDetails);
+ if ("encryptedTo" in o) {
+ encToDetails = o.encryptedTo;
+ }
+ } catch (x) {
+ console.debug(x);
+ }
+ }
+
+ Enigmail.hdrView.updatePgpStatus(
+ exitCode,
+ statusFlags,
+ extStatusFlags,
+ keyId,
+ userId,
+ sigDetails,
+ errorMsg,
+ blockSeparation,
+ encToDetails,
+ null,
+ mimePartNumber
+ );
+ }
+ },
+
+ processDecryptionResult(uri, actionType, processData, mimePartNumber) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: EnigMimeHeaderSink.processDecryptionResult:\n"
+ );
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: actionType= " +
+ actionType +
+ ", mimePart=" +
+ mimePartNumber +
+ "\n"
+ );
+
+ let msg = gMessage;
+ if (!msg) {
+ return;
+ }
+ if (!this.isCurrentMessage(uri)) {
+ return;
+ }
+
+ switch (actionType) {
+ case "modifyMessageHeaders":
+ this.modifyMessageHeaders(uri, processData, mimePartNumber);
+ break;
+ /*
+ case "wksConfirmRequest":
+ Enigmail.hdrView.checkWksConfirmRequest(processData);
+ */
+ }
+ },
+
+ modifyMessageHeaders(uri, headerData, mimePartNumber) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: EnigMimeHeaderSink.modifyMessageHeaders:\n"
+ );
+
+ let uriSpec = uri ? uri.spec : null;
+ let hdr;
+
+ try {
+ hdr = JSON.parse(headerData);
+ } catch (ex) {
+ EnigmailLog.DEBUG(
+ "enigmailMsgHdrViewOverlay.js: modifyMessageHeaders: - no headers to display\n"
+ );
+ return;
+ }
+
+ if (typeof hdr !== "object") {
+ return;
+ }
+ if (!this.displaySubPart(mimePartNumber, uriSpec)) {
+ return;
+ }
+
+ let msg = gMessage;
+
+ if ("subject" in hdr) {
+ Enigmail.hdrView.setSubject(hdr.subject);
+ }
+
+ if ("date" in hdr) {
+ msg.date = Date.parse(hdr.date) * 1000;
+ }
+ /*
+ if ("newsgroups" in hdr) {
+ updateHdrBox("newsgroups", hdr.newsgroups);
+ }
+
+ if ("followup-to" in hdr) {
+ updateHdrBox("followup-to", hdr["followup-to"]);
+ }
+
+ if ("from" in hdr) {
+ gExpandedHeaderView.from.outputFunction(gExpandedHeaderView.from, hdr.from);
+ msg.setStringProperty("Enigmail-From", hdr.from);
+ }
+
+ if ("to" in hdr) {
+ gExpandedHeaderView.to.outputFunction(gExpandedHeaderView.to, hdr.to);
+ msg.setStringProperty("Enigmail-To", hdr.to);
+ }
+
+ if ("cc" in hdr) {
+ gExpandedHeaderView.cc.outputFunction(gExpandedHeaderView.cc, hdr.cc);
+ msg.setStringProperty("Enigmail-Cc", hdr.cc);
+ }
+
+ if ("reply-to" in hdr) {
+ gExpandedHeaderView["reply-to"].outputFunction(gExpandedHeaderView["reply-to"], hdr["reply-to"]);
+ msg.setStringProperty("Enigmail-ReplyTo", hdr["reply-to"]);
+ }
+ */
+ },
+
+ handleSMimeMessage(uri) {
+ if (
+ Enigmail.hdrView.msgSignedStateString != null ||
+ Enigmail.hdrView.msgEncryptedStateString != null
+ ) {
+ // If we already processed an OpenPGP part, then we are handling
+ // a message with an inner S/MIME part. We must not reload
+ // the message here, because we'd run into an endless loop.
+ return;
+ }
+ if (this.isCurrentMessage(uri)) {
+ EnigmailVerify.unregisterPGPMimeHandler();
+ Enigmail.msg.messageReload(false);
+ }
+ },
+ },
+
+ /*
+ onUnloadEnigmail() {
+ window.removeEventListener("load-enigmail", Enigmail.hdrView.hdrViewLoad);
+ for (let i = 0; i < gMessageListeners.length; i++) {
+ if (gMessageListeners[i] === Enigmail.hdrView.messageListener) {
+ gMessageListeners.splice(i, 1);
+ break;
+ }
+ }
+
+ let signedHdrElement = document.getElementById("signedHdrIcon");
+ if (signedHdrElement) {
+ signedHdrElement.setAttribute(
+ "onclick",
+ "showMessageReadSecurityInfo();"
+ );
+ }
+
+ let encryptedHdrElement = document.getElementById("encryptedHdrIcon");
+ if (encryptedHdrElement) {
+ encryptedHdrElement.setAttribute(
+ "onclick",
+ "showMessageReadSecurityInfo();"
+ );
+ }
+
+ let addrPopup = document.getElementById("emailAddressPopup");
+ if (addrPopup) {
+ addrPopup.removeEventListener(
+ "popupshowing",
+ Enigmail.hdrView.displayAddressPopup
+ );
+ }
+
+ let attCtx = document.getElementById("attachmentItemContext");
+ if (attCtx) {
+ attCtx.removeEventListener(
+ "popupshowing",
+ this.onShowAttachmentContextMenu
+ );
+ }
+
+ let msgFrame = EnigmailWindows.getFrame(window, "messagepane");
+ if (msgFrame) {
+ msgFrame.removeEventListener(
+ "unload",
+ Enigmail.hdrView.messageUnload,
+ true
+ );
+ msgFrame.removeEventListener("load", Enigmail.hdrView.messageLoad);
+ }
+ },
+ */
+};
+
+window.addEventListener(
+ "load-enigmail",
+ Enigmail.hdrView.hdrViewLoad.bind(Enigmail.hdrView)
+);
+window.addEventListener("unload", () => Enigmail.hdrView.clearWindowCallback());
diff --git a/comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml b/comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml
new file mode 100644
index 0000000000..e3a56edf77
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml
@@ -0,0 +1,119 @@
+# 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/.
+
+<html:dialog id="keyAssistant" xmlns="http://www.w3.org/1999/xhtml"
+ class="modal-dialog">
+
+ <h1 class="dialog-title" data-l10n-id="openpgp-key-assistant-title"></h1>
+
+ <div id="discoverView" class="modal-dialog-body dialog-body-view">
+ <div>
+ <p data-l10n-id="openpgp-key-assistant-discover-title"></p>
+ <div id="discoveryOutput"></div>
+ </div>
+ <menu class="dialog-menu-container menu-in-body">
+ <button data-l10n-id="openpgp-key-assistant-cancel-button"
+ onclick="gKeyAssistant.resetViews();"></button>
+ </menu>
+ </div><!-- #discoverView -->
+
+ <div id="resolveView" class="modal-dialog-body dialog-body-view">
+ <div>
+ <p id="resolveViewTitle"></p>
+
+ <!-- Usable section. -->
+ <section id="resolveViewValid" hidden="hidden">
+ <p id="resolveViewValidDescription" class="font-bold"
+ data-l10n-id="openpgp-key-assistant-valid-description"></p>
+ <ul id="resolveValidKeysList" class="reset-list radio-list"></ul>
+ <p id="openpgp-key-assistant-help-accept"
+ class="tip-caption margin-top-1em"
+ data-l10n-id="openpgp-key-assistant-rogue-warning">
+ <a onclick="openContentTab(this.href);"
+ href="https://support.mozilla.org/kb/thunderbird-help-openpgp-counterfeit-key"
+ data-l10n-name="openpgp-link"></a>
+ </p>
+ </section>
+
+ <!-- Unusable section. -->
+ <section id="resolveViewInvalid" hidden="hidden">
+ <p id="resolveViewExpiredDescription" class="font-bold margin-top-1em"></p>
+ <ul id="resolveInvalidKeysList" class="reset-list radio-list"></ul>
+ </section>
+
+ <button data-l10n-id="openpgp-key-assistant-discover-online-button"
+ onclick="gKeyAssistant.changeView('discover', 'resolving');"></button>
+ <button data-l10n-id="openpgp-key-assistant-import-keys-button"
+ onclick="gKeyAssistant.importFromFile('resolving');"></button>
+ </div>
+
+ <menu class="dialog-menu-container two-columns menu-in-body">
+ <button data-l10n-id="openpgp-key-assistant-back-button"
+ onclick="gKeyAssistant.resetViews();"></button>
+ <button id="resolveViewAcceptKey"
+ data-l10n-id="openpgp-key-assistant-accept-button"
+ class="primary"
+ disabled="disabled"></button>
+ </menu>
+ </div><!-- #resolveView -->
+
+ <div id="mainView" class="modal-dialog-body dialog-body-view">
+ <div id="modalDialogNotification" class="modal-dialog-notifications">
+ <!-- Notifications will be lazily loaded here. -->
+ </div>
+
+ <!-- Issues section. -->
+ <section id="keyAssistantIssues" hidden="hidden">
+ <p id="keyAssistantIssuesHeader" class="font-bold"
+ data-l10n-id="openpgp-key-assistant-recipients-issue-header"/>
+ <p id="keyAssistantIssuesDescription">
+ <a onclick="openContentTab(this.href);"
+ href="https://support.mozilla.org/kb/thunderbird-help-cannot-encrypt"
+ data-l10n-name="openpgp-link"></a>
+ </p>
+
+ <ul id="keysListIssues" class="reset-list key-list"></ul>
+
+ <button data-l10n-id="openpgp-key-assistant-discover-online-button"
+ onclick="gKeyAssistant.changeView('discover', 'overview');"></button>
+ <button data-l10n-id="openpgp-key-assistant-import-keys-button"
+ onclick="gKeyAssistant.importFromFile('overview');"></button>
+
+ <p id="openpgp-key-assistant-help-alias"
+ class="tip-caption margin-top-1em"
+ data-l10n-id="openpgp-key-assistant-info-alias">
+ <a onclick="openContentTab(this.href);"
+ href="https://support.mozilla.org/kb/thunderbird-help-openpgp-alias"
+ data-l10n-name="openpgp-link"></a>
+ </p>
+ </section>
+
+ <!-- No issues section. -->
+ <section id="keyAssistantValid" class="margin-top-1em" hidden="hidden">
+ <div class="container-with-link">
+ <p id="keyAssistantValidDescription"></p>
+ <button id="toggleRecipientsButton"
+ class="button-link"
+ data-l10n-id="openpgp-key-assistant-recipients-show-button"></button>
+ </div>
+
+ <ul id="keysListValid" class="reset-list key-list" hidden="hidden"></ul>
+ </section>
+ </div><!-- #mainView -->
+
+ <menu id="mainButtons" class="dialog-menu-container two-columns">
+ <div>
+ <button data-l10n-id="openpgp-key-assistant-close-button"
+ onclick="gKeyAssistant.close();"></button>
+ </div>
+ <div>
+ <button id="disableEncryptionButton"
+ data-l10n-id="openpgp-key-assistant-disable-button"></button>
+ <button id="sendEncryptedButton"
+ data-l10n-id="openpgp-key-assistant-confirm-button"
+ class="primary"
+ disabled="disabled"></button>
+ </div>
+ </menu>
+</html:dialog>
diff --git a/comm/mail/extensions/openpgp/content/ui/keyAssistant.js b/comm/mail/extensions/openpgp/content/ui/keyAssistant.js
new file mode 100644
index 0000000000..ca6104c2cb
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyAssistant.js
@@ -0,0 +1,956 @@
+/*
+ * 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/.
+ */
+
+/* global MozElements */
+/* import-globals-from ../../../../components/compose/content/MsgComposeCommands.js */
+/* import-globals-from commonWorkflows.js */
+/* globals goDoCommand */ // From globalOverlay.js
+
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ EnigmailKeyRing: "chrome://openpgp/content/modules/keyRing.jsm",
+ KeyLookupHelper: "chrome://openpgp/content/modules/keyLookupHelper.jsm",
+ OpenPGPAlias: "chrome://openpgp/content/modules/OpenPGPAlias.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+ // FIXME: using this creates a conflict with another file where this symbol
+ // was imported with ChromeUtils instead of defined as lazy getter.
+ // EnigmailWindows: "chrome://openpgp/content/modules/windows.jsm",
+});
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+
+window.addEventListener("load", () => {
+ gKeyAssistant.onLoad();
+});
+window.addEventListener("unload", () => {
+ gKeyAssistant.onUnload();
+});
+
+var gKeyAssistant = {
+ dialog: null,
+ recipients: [],
+ currentRecip: null,
+
+ /*
+ * Variable ignoreExternal should be set to true whenever a
+ * keyAsssistant window is open that cannot tolerate changes to
+ * the keyAsssistant's own variables, that track the current user
+ * interaction.
+ *
+ * While the key assistant is showing, it takes care to update the
+ * elements on screen, based on the expected changes. Usually,
+ * it will perform a refresh after a current action is completed.
+ *
+ * Without this protection, you'd get data races and side effects like
+ * email addresses being shown twice, and worse.
+ */
+ ignoreExternal: false,
+
+ /**
+ * Initialize the main notification box for the account setup process.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("modalDialogNotification").append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ onLoad() {
+ this.dialog = document.getElementById("keyAssistant");
+
+ this._setupEventListeners();
+ },
+
+ _setupEventListeners() {
+ document
+ .getElementById("disableEncryptionButton")
+ .addEventListener("click", () => {
+ gSendEncrypted = false;
+ gUserTouchedSendEncrypted = true;
+ checkEncryptionState();
+ this.close();
+ });
+ document
+ .getElementById("sendEncryptedButton")
+ .addEventListener("click", () => {
+ goDoCommand("cmd_sendWithCheck");
+ this.close();
+ });
+ document
+ .getElementById("toggleRecipientsButton")
+ .addEventListener("click", () => {
+ this.toggleRecipientsList();
+ });
+
+ this.dialog.addEventListener("close", () => {
+ this.close();
+ });
+ },
+
+ async close() {
+ await checkEncryptionState();
+ this.dialog.close();
+ },
+
+ onUnload() {
+ this.recipients = [];
+ },
+
+ setMainDisableButton() {
+ document.getElementById("disableEncryptionButton").hidden =
+ !gSendEncrypted || (this.usableKeys && !this.problematicKeys);
+ },
+
+ /**
+ * Open the key assistant modal dialog.
+ *
+ * @param {string[]} recipients - An array of strings containing all currently
+ * written recipients.
+ * @param {boolean} isSending - If the key assistant was triggered during a
+ * sending attempt.
+ */
+ show(recipients, isSending) {
+ this.recipients = recipients;
+ this.buildMainView();
+ this.resetViews();
+
+ document.getElementById("sendEncryptedButton").hidden = !isSending;
+ this.setMainDisableButton();
+ this.dialog.showModal();
+ },
+
+ resetViews() {
+ this.notificationBox.removeAllNotifications();
+ this.dialog.removeAttribute("style");
+
+ for (let view of document.querySelectorAll(".dialog-body-view")) {
+ view.hidden = true;
+ }
+
+ document.getElementById("mainButtons").hidden = false;
+ document.getElementById("mainView").hidden = false;
+ },
+
+ changeView(view, context) {
+ this.resetViews();
+
+ this.dialog.setAttribute(
+ "style",
+ `min-height: ${this.dialog.getBoundingClientRect().height}px`
+ );
+
+ document.getElementById("mainView").hidden = true;
+ document.getElementById(`${view}View`).hidden = false;
+
+ switch (view) {
+ case "discover":
+ this.hideMainButtons();
+ this.initOnlineDiscovery(context);
+ break;
+
+ case "resolve":
+ this.hideMainButtons();
+ break;
+
+ default:
+ break;
+ }
+ },
+
+ hideMainButtons() {
+ document.getElementById("mainButtons").hidden = true;
+ },
+
+ usableKeys: 0,
+ problematicKeys: 0,
+
+ /**
+ * Populate the main view of the key assistant with the list of recipients and
+ * its keys, separating the recipients that have issues from those without
+ * issues.
+ */
+ async buildMainView() {
+ // Restore empty UI state.
+ document.getElementById("keyAssistantIssues").hidden = true;
+ document.getElementById("keysListIssues").replaceChildren();
+ document.getElementById("keyAssistantValid").hidden = true;
+ document.getElementById("keysListValid").replaceChildren();
+
+ this.usableKeys = 0;
+ this.problematicKeys = 0;
+
+ for (let addr of this.recipients) {
+ // Fetch all keys for the current recipient.
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(addr);
+ if (keyMetas.some(k => k.readiness == "alias")) {
+ let aliasKeyList = EnigmailKeyRing.getAliasKeyList(addr);
+ let aliasKeys = EnigmailKeyRing.getAliasKeys(aliasKeyList);
+ if (!aliasKeys.length) {
+ // failure, at least one alias key is unusable/unavailable
+
+ let descriptionDiv = document.createElement("div");
+ document.l10n.setAttributes(
+ descriptionDiv,
+ "openpgp-compose-alias-status-error"
+ );
+
+ this.addToProblematicList(addr, descriptionDiv, null);
+ this.problematicKeys++;
+ } else {
+ let aliasText = document.createElement("div");
+ document.l10n.setAttributes(
+ aliasText,
+ "openpgp-compose-alias-status-direct",
+ { count: aliasKeys.length }
+ );
+
+ this.addToReadyList(addr, aliasText);
+ this.usableKeys++;
+ }
+ } else {
+ // not alias
+
+ let acceptedKeys = keyMetas.filter(k => k.readiness == "accepted");
+ if (acceptedKeys.length) {
+ let button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ "openpgp-key-assistant-view-key-button"
+ );
+ button.addEventListener("click", () => {
+ gKeyAssistant.viewKeyFromOverview(addr, acceptedKeys[0]);
+ });
+
+ this.addToReadyList(addr, button);
+ this.usableKeys++;
+ } else {
+ let descriptionDiv = document.createElement("div");
+
+ let canOfferResolving = keyMetas.some(
+ k =>
+ k.readiness == "collected" ||
+ k.readiness == "expiredAccepted" ||
+ k.readiness == "expiredUndecided" ||
+ k.readiness == "expiredOtherAccepted" ||
+ k.readiness == "undecided" ||
+ k.readiness == "otherAccepted" ||
+ k.readiness == "expiredRejected" ||
+ k.readiness == "rejected"
+ );
+
+ let button = null;
+ if (canOfferResolving) {
+ this.fillKeysStatus(descriptionDiv, keyMetas);
+
+ button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ "openpgp-key-assistant-issue-resolve-button"
+ );
+ button.addEventListener("click", () => {
+ this.buildResolveView(addr, keyMetas);
+ });
+ } else {
+ document.l10n.setAttributes(
+ descriptionDiv,
+ "openpgp-key-assistant-no-key-available"
+ );
+ }
+
+ this.addToProblematicList(addr, descriptionDiv, button);
+ this.problematicKeys++;
+ }
+ }
+ }
+
+ document.getElementById("keyAssistantIssues").hidden =
+ !this.problematicKeys;
+ document.l10n.setAttributes(
+ document.getElementById("keyAssistantIssuesDescription"),
+ "openpgp-key-assistant-recipients-issue-description",
+ { count: this.problematicKeys }
+ );
+
+ document.getElementById("keyAssistantValid").hidden = !this.usableKeys;
+
+ if (!this.problematicKeys && this.usableKeys) {
+ document.l10n.setAttributes(
+ document.getElementById("keyAssistantValidDescription"),
+ "openpgp-key-assistant-recipients-description-no-issues"
+ );
+ document.getElementById("toggleRecipientsButton").click();
+ } else {
+ document.l10n.setAttributes(
+ document.getElementById("keyAssistantValidDescription"),
+ "openpgp-key-assistant-recipients-description",
+ { count: this.usableKeys }
+ );
+ }
+
+ document.getElementById("sendEncryptedButton").disabled =
+ this.problematicKeys || !this.usableKeys;
+ this.setMainDisableButton();
+ },
+
+ isAccepted(acc) {
+ return (
+ acc.emailDecided &&
+ (acc.fingerprintAcceptance == "verified" ||
+ acc.fingerprintAcceptance == "unverified")
+ );
+ },
+
+ async viewKeyFromResolve(keyMeta) {
+ let oldAccept = {};
+ await PgpSqliteDb2.getAcceptance(
+ keyMeta.keyObj.fpr,
+ this.currentRecip,
+ oldAccept
+ );
+
+ this.ignoreExternal = true;
+ await this._viewKey(keyMeta);
+ this.ignoreExternal = false;
+
+ // If the key is not yet accepted, then we want to automatically
+ // close the email-resolve view, if the user accepts the key
+ // while viewing the key details.
+ let autoCloseOnAccept = !this.isAccepted(oldAccept);
+
+ let newAccept = {};
+ await PgpSqliteDb2.getAcceptance(
+ keyMeta.keyObj.fpr,
+ this.currentRecip,
+ newAccept
+ );
+
+ if (autoCloseOnAccept && this.isAccepted(newAccept)) {
+ this.resetViews();
+ this.buildMainView();
+ } else {
+ // While viewing the key, the user could have triggered a refresh,
+ // which could have changed the validity of the key.
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(
+ this.currentRecip
+ );
+ this.buildResolveView(this.currentRecip, keyMetas);
+ }
+ },
+
+ async viewKeyFromOverview(recip, keyMeta) {
+ this.ignoreExternal = true;
+ await this._viewKey(keyMeta);
+ this.ignoreExternal = false;
+
+ // While viewing the key, the user could have triggered a refresh,
+ // which could have changed the validity of the key.
+ // In theory it would be sufficient to refresh the main view
+ // for the single email address.
+ await checkEncryptionState("openpgp-key-assistant-refresh");
+ this.buildMainView();
+ },
+
+ async _viewKey(keyMeta) {
+ let exists = EnigmailKeyRing.getKeyById(keyMeta.keyObj.keyId);
+
+ if (!exists) {
+ if (keyMeta.readiness != "collected") {
+ return;
+ }
+ await EnigmailKeyRing.importKeyDataSilent(
+ window,
+ keyMeta.collectedKey.pubKey,
+ true
+ );
+ }
+
+ EnigmailWindows.openKeyDetails(window, keyMeta.keyObj.keyId, false);
+ },
+
+ addToReadyList(recipient, detailElement) {
+ let list = document.getElementById("keysListValid");
+ let row = document.createElement("li");
+ row.classList.add("key-row");
+
+ let info = document.createElement("div");
+ info.classList.add("key-info");
+ let title = document.createElement("b");
+ title.textContent = recipient;
+
+ info.appendChild(title);
+ row.append(info, detailElement);
+ list.appendChild(row);
+ },
+
+ fillKeysStatus(element, keyMetas) {
+ let unaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "undecided" ||
+ k.readiness == "rejected" ||
+ k.readiness == "otherAccepted"
+ );
+ let collected = keyMetas.filter(k => k.readiness == "collected");
+
+ // Multiple keys available.
+ if (unaccepted.length + collected.length > 1) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-multiple-keys"
+ );
+ // TODO: add note to be careful?
+ return;
+ }
+
+ // Not expired but not accepted keys.
+ if (unaccepted.length) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-unaccepted",
+ {
+ count: unaccepted.length,
+ }
+ );
+ if (unaccepted.length == 1) {
+ element.before("0x" + unaccepted[0].keyObj.keyId);
+ }
+ return;
+ }
+
+ let expiredAccepted = keyMetas.filter(
+ k => k.readiness == "expiredAccepted"
+ );
+
+ // Key accepted but expired.
+ if (expiredAccepted.length) {
+ if (expiredAccepted.length == 1) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-accepted-expired",
+ {
+ date: expiredAccepted[0].keyObj.effectiveExpiry,
+ }
+ );
+ element.before("0x" + expiredAccepted[0].keyObj.keyId);
+ } else {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-keys-accepted-expired"
+ );
+ }
+ return;
+ }
+
+ let expiredUnaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "expiredUndecided" ||
+ k.readiness == "expiredRejected" ||
+ k.readiness == "expiredOtherAccepted"
+ );
+
+ // Key not accepted and expired.
+ if (expiredUnaccepted.length) {
+ if (expiredUnaccepted.length == 1) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-unaccepted-expired-one",
+ {
+ date: expiredUnaccepted[0].keyObj.effectiveExpiry,
+ }
+ );
+ element.before("0x" + expiredUnaccepted[0].keyObj.keyId);
+ } else {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-unaccepted-expired-many"
+ );
+ }
+ return;
+ }
+
+ let unacceptedNotYetImported = keyMetas.filter(
+ k => k.readiness == "collected"
+ );
+
+ if (unacceptedNotYetImported.length) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-keys-has-collected",
+ {
+ count: unacceptedNotYetImported.length,
+ }
+ );
+ if (unacceptedNotYetImported.length == 1) {
+ element.before("0x" + unacceptedNotYetImported[0].keyObj.keyId);
+ }
+ return;
+ }
+
+ // We found nothing, so let's return a default message.
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-no-key-available"
+ );
+ },
+
+ addToProblematicList(recipient, descriptionDiv, resolveButton) {
+ let list = document.getElementById("keysListIssues");
+ let row = document.createElement("li");
+ row.classList.add("key-row");
+
+ let info = document.createElement("div");
+ info.classList.add("key-info");
+ let title = document.createElement("b");
+ title.textContent = recipient;
+ info.append(title, descriptionDiv);
+
+ if (resolveButton) {
+ row.append(info, resolveButton);
+ } else {
+ row.appendChild(info);
+ }
+
+ list.appendChild(row);
+ },
+
+ fillKeyOriginAndStatus(element, keyMeta) {
+ // The key was collected from somewhere.
+ if (keyMeta.collectedKey) {
+ let sourceSpan = document.createElement("span");
+ document.l10n.setAttributes(
+ sourceSpan,
+ "openpgp-key-assistant-key-source",
+ {
+ count: keyMeta.collectedKey.sources.length,
+ }
+ );
+ element.append(sourceSpan, ": ");
+ let linkSpan = document.createElement("span");
+ linkSpan.classList.add("comma-separated");
+
+ let sourceLinks = keyMeta.collectedKey.sources.map(source => {
+ source.type = source.type.toLowerCase(); // Earlier "WKD" was "wkd".
+ let a = document.createElement("a");
+ if (source.uri) {
+ a.href = source.uri;
+ a.title = source.uri;
+ }
+ if (source.description) {
+ if (a.title) {
+ a.title += " - ";
+ }
+ a.title += source.description;
+ }
+ let span = document.createElement("span");
+ // openpgp-key-assistant-key-collected-attachment
+ // openpgp-key-assistant-key-collected-autocrypt
+ // openpgp-key-assistant-key-collected-keyserver
+ // openpgp-key-assistant-key-collected-wkd
+ document.l10n.setAttributes(
+ span,
+ `openpgp-key-assistant-key-collected-${source.type}`
+ );
+ a.appendChild(span);
+ return a;
+ });
+ linkSpan.append(...sourceLinks);
+ element.appendChild(linkSpan);
+ return;
+ }
+
+ // The key was rejected.
+ if (keyMeta.readiness == "rejected") {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-rejected"
+ );
+ return;
+ }
+
+ // Key is expired.
+ if (
+ keyMeta.readiness == "expiredAccepted" ||
+ keyMeta.readiness == "expiredUndecided" ||
+ keyMeta.readiness == "expiredOtherAccepted" ||
+ keyMeta.readiness == "expiredRejected"
+ ) {
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-this-key-accepted-expired",
+ {
+ date: keyMeta.keyObj.effectiveExpiry,
+ }
+ );
+ return;
+ }
+
+ if (keyMeta.readiness == "otherAccepted") {
+ // Was the key already accepted for another email address?
+ document.l10n.setAttributes(
+ element,
+ "openpgp-key-assistant-key-accepted-other",
+ {
+ date: keyMeta.keyObj.effectiveExpiry,
+ }
+ );
+ }
+ },
+
+ async buildResolveView(recipient, keyMetas) {
+ this.currentRecip = recipient;
+ document.getElementById("resolveViewAcceptKey").disabled = true;
+
+ let unaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "undecided" ||
+ k.readiness == "rejected" ||
+ k.readiness == "otherAccepted"
+ );
+ let collected = keyMetas.filter(k => k.readiness == "collected");
+ let expiredAccepted = keyMetas.filter(
+ k => k.readiness == "expiredAccepted"
+ );
+ let expiredUnaccepted = keyMetas.filter(
+ k =>
+ k.readiness == "expiredUndecided" ||
+ k.readiness == "expiredRejected" ||
+ k.readiness == "expiredOtherAccepted"
+ );
+
+ this.usableKeys = unaccepted.length + collected.length;
+ let problematicKeys = expiredAccepted.length + expiredUnaccepted.length;
+ let numKeys = this.usableKeys + problematicKeys;
+
+ document.l10n.setAttributes(
+ document.getElementById("resolveViewTitle"),
+ "openpgp-key-assistant-resolve-title",
+ {
+ recipient,
+ numKeys,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("resolveViewExpiredDescription"),
+ "openpgp-key-assistant-invalid-title",
+ { numKeys }
+ );
+
+ document.getElementById("resolveViewValid").hidden = !this.usableKeys;
+ let usableList = document.getElementById("resolveValidKeysList");
+ usableList.replaceChildren();
+
+ function createKeyRow(keyMeta, isValid) {
+ let row = document.createElement("li");
+ let label = document.createElement("label");
+ label.classList.add("flex-center");
+
+ let input = document.createElement("input");
+ input.type = "radio";
+ input.name = isValid ? "valid-key" : "invalid-key";
+ input.value = keyMeta.keyObj.keyId;
+ input.disabled = !isValid;
+
+ if (isValid) {
+ input.addEventListener("change", () => {
+ document.getElementById("resolveViewAcceptKey").disabled = false;
+ });
+ }
+ label.appendChild(input);
+
+ let keyId = document.createElement("b");
+ keyId.textContent = "0x" + keyMeta.keyObj.keyId;
+
+ let creationTime = document.createElement("time");
+ creationTime.setAttribute(
+ "datetime",
+ new Date(keyMeta.keyObj.keyCreated * 1000).toISOString()
+ );
+ document.l10n.setAttributes(
+ creationTime,
+ "openpgp-key-assistant-key-created",
+ { date: keyMeta.keyObj.created }
+ );
+ label.append(keyId, " - ", creationTime);
+ row.appendChild(label);
+
+ let fingerprint = document.createElement("div");
+ fingerprint.classList.add("key-info-block");
+ let fpDesc = document.createElement("span");
+ let fpLink = document.createElement("a");
+ fpLink.href = "#";
+ fpLink.textContent = EnigmailKey.formatFpr(keyMeta.keyObj.fpr);
+ fpLink.addEventListener("click", event => {
+ event.preventDefault();
+ gKeyAssistant.viewKeyFromResolve(keyMeta);
+ });
+ document.l10n.setAttributes(
+ fpDesc,
+ "openpgp-key-assistant-key-fingerprint"
+ );
+ fingerprint.append(fpDesc, ": ", fpLink);
+ row.appendChild(fingerprint);
+
+ let info = document.createElement("div");
+ info.classList.add("key-info-block");
+ row.append(info);
+
+ gKeyAssistant.fillKeyOriginAndStatus(info, keyMeta);
+ return row;
+ }
+
+ for (let meta of unaccepted) {
+ usableList.appendChild(createKeyRow(meta, true));
+ }
+
+ for (let meta of collected) {
+ usableList.appendChild(createKeyRow(meta, true));
+ }
+
+ document.getElementById("resolveViewInvalid").hidden = !problematicKeys;
+ let problematicList = document.getElementById("resolveInvalidKeysList");
+ problematicList.replaceChildren();
+
+ for (let meta of expiredAccepted) {
+ problematicList.appendChild(createKeyRow(meta, false));
+ }
+ for (let meta of expiredUnaccepted) {
+ problematicList.appendChild(createKeyRow(meta, false));
+ }
+
+ document.getElementById("resolveViewAcceptKey").onclick = () => {
+ this.acceptSelectedKey(recipient, keyMetas);
+ };
+ this.changeView("resolve");
+ },
+
+ async acceptSelectedKey(recipient, keyMetas) {
+ let selectedKey = document.querySelector(
+ 'input[name="valid-key"]:checked'
+ )?.value;
+ if (!selectedKey) {
+ // The accept button was enabled but nothing was selected.
+ return;
+ }
+ let fingerprint;
+
+ this.ignoreExternal = true;
+
+ let existingKey = EnigmailKeyRing.getKeyById(selectedKey);
+ if (existingKey) {
+ fingerprint = existingKey.fpr;
+ } else {
+ let unacceptedNotYetImported = keyMetas.filter(
+ k => k.readiness == "collected"
+ );
+
+ for (let keyMeta of unacceptedNotYetImported) {
+ if (keyMeta.keyObj.keyId != selectedKey) {
+ continue;
+ }
+ await EnigmailKeyRing.importKeyDataSilent(
+ window,
+ keyMeta.collectedKey.pubKey,
+ true
+ );
+ fingerprint = keyMeta.keyObj.fpr;
+ }
+ }
+
+ if (!fingerprint) {
+ throw new Error(`Key not found for id=${selectedKey}`);
+ }
+
+ await PgpSqliteDb2.addAcceptedEmail(fingerprint, recipient).catch(
+ console.error
+ );
+
+ // Trigger the UI refresh of the compose window.
+ await checkEncryptionState("openpgp-key-assistant-refresh");
+
+ this.ignoreExternal = false;
+ this.resetViews();
+ this.buildMainView();
+ },
+
+ async initOnlineDiscovery(context) {
+ let container = document.getElementById("discoveryOutput");
+ container.replaceChildren();
+
+ function write(recipient) {
+ let p = document.createElement("p");
+ let span = document.createElement("span");
+ document.l10n.setAttributes(span, "openpgp-key-assistant-discover-keys", {
+ recipient,
+ });
+ let span2 = document.createElement("span");
+ span2.classList.add("loading-inline");
+ p.append(span, " ", span2);
+ container.appendChild(p);
+ }
+
+ let gotNewData = false; // XXX: not used for anything atm
+
+ // Checking gotNewData isn't really sufficient, because the discovery could
+ // find an update for an existing key, which was expired, and is now valid
+ // again. Let's always rebuild for now.
+
+ if (context == "overview") {
+ this.ignoreExternal = true;
+ for (let email of this.recipients) {
+ if (OpenPGPAlias.hasAliasDefinition(email)) {
+ continue;
+ }
+ write(email);
+ let rv = await KeyLookupHelper.fullOnlineDiscovery(
+ "silent-collection",
+ window,
+ email,
+ null
+ );
+ gotNewData = gotNewData || rv;
+ }
+
+ // Wait a sec before closing the view, so the user has time to see what
+ // happened.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ this.resetViews();
+ this.buildMainView();
+
+ // Online discovery and key collection triggered key change
+ // notifications. We must allow those notifications arrive while
+ // ignoreExternal is still true.
+ // Use settimeout to reset ignoreExternal to false afterwards.
+ setTimeout(function () {
+ this.ignoreExternal = false;
+ });
+ return;
+ }
+
+ // We should never arrive here for an email address that has an
+ // alias rule, because for those we don't want to perform online
+ // discovery.
+
+ if (OpenPGPAlias.hasAliasDefinition(this.currentRecip)) {
+ throw new Error(`${this.currentRecip} has an alias rule`);
+ }
+
+ write(this.currentRecip);
+
+ this.ignoreExternal = true;
+ gotNewData = await KeyLookupHelper.fullOnlineDiscovery(
+ "silent-collection",
+ window,
+ this.currentRecip,
+ null
+ );
+ // Online discovery and key collection triggered key change
+ // notifications. We must allow those notifications arrive while
+ // ignoreExternal is still true.
+ // Use settimeout to reset ignoreExternal to false afterwards.
+ setTimeout(function () {
+ this.ignoreExternal = false;
+ });
+
+ // If the recipient now has a usable previously accepted key, go back to
+ // the main view and show a successful notification.
+ let keyMetas = await EnigmailKeyRing.getEncryptionKeyMeta(
+ this.currentRecip
+ );
+
+ if (keyMetas.some(k => k.readiness == "accepted")) {
+ // Trigger the UI refresh of the compose window.
+ await checkEncryptionState("openpgp-key-assistant-refresh");
+
+ // Wait a sec before closing the view, so the user has time to see what
+ // happened.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ this.resetViews();
+ this.buildMainView();
+
+ let notification =
+ this.notificationBox.getNotificationWithValue("acceptedKeyUpdated");
+
+ // If a notification already exists, simply update the message.
+ if (notification) {
+ document.l10n.setAttributes(
+ notification.messageText,
+ "openpgp-key-assistant-expired-key-update",
+ {
+ recipient: this.currentRecip,
+ }
+ );
+ return;
+ }
+
+ notification = this.notificationBox.appendNotification(
+ "acceptedKeyUpdated",
+ {
+ label: {
+ "l10n-id": "openpgp-key-assistant-expired-key-update",
+ "l10n-args": { recipient: this.currentRecip },
+ },
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ notification.setAttribute("type", "success");
+ return;
+ }
+
+ this.buildResolveView(this.currentRecip, keyMetas);
+ gKeyAssistant.changeView("resolve");
+ },
+
+ toggleRecipientsList() {
+ let list = document.getElementById("keysListValid");
+ list.hidden = !list.hidden;
+
+ document.l10n.setAttributes(
+ document.getElementById("toggleRecipientsButton"),
+ list.hidden
+ ? "openpgp-key-assistant-recipients-show-button"
+ : "openpgp-key-assistant-recipients-hide-button"
+ );
+ },
+
+ async importFromFile(context) {
+ await EnigmailCommon_importObjectFromFile("pub");
+ if (context == "overview") {
+ this.buildMainView();
+ } else {
+ this.buildResolveView(
+ this.currentRecip,
+ await EnigmailKeyRing.getEncryptionKeyMeta(this.currentRecip)
+ );
+ }
+ },
+
+ onExternalKeyChange() {
+ if (!this.dialog || !this.dialog.open) {
+ return;
+ }
+
+ if (this.ignoreExternal) {
+ return;
+ }
+
+ // Refresh the "overview", which will potentially close a currently
+ // shown "resolve" view.
+ this.resetViews();
+ this.buildMainView();
+ },
+};
diff --git a/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js
new file mode 100644
index 0000000000..d82848f932
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js
@@ -0,0 +1,1119 @@
+/* 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/. */
+
+// from enigmailKeyManager.js:
+/* global l10n */
+
+"use strict";
+
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var { EnigmailLog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/log.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { KeyLookupHelper } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyLookupHelper.jsm"
+);
+var { RNP, RnpPrivateKeyUnlockTracker } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNP.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+});
+
+var gModePersonal = false;
+
+// This is the ID that was given to us as a parameter.
+// Note that it might be the ID of a subkey.
+var gKeyId = null;
+
+var gUserId = null;
+var gKeyList = null;
+var gSigTree = null;
+
+var gAllEmails = [];
+var gOriginalAcceptedEmails = null;
+var gAcceptedEmails = null;
+
+var gHaveUnacceptedEmails = false;
+var gFingerprint = "";
+var gHasMissingSecret = false;
+
+var gAcceptanceRadio = null;
+var gPersonalRadio = null;
+
+var gOriginalAcceptance;
+var gOriginalPersonal;
+var gUpdateAllowed = false;
+
+let gAllEmailCheckboxes = [];
+let gOkButton;
+
+let gPrivateKeyTrackers = [];
+
+window.addEventListener("DOMContentLoaded", onLoad);
+window.addEventListener("unload", onUnload);
+
+function onUnload() {
+ releasePrivateKeys();
+}
+
+function releasePrivateKeys() {
+ for (let tracker of gPrivateKeyTrackers) {
+ tracker.release();
+ }
+ gPrivateKeyTrackers = [];
+}
+
+async function onLoad() {
+ if (window.arguments[1]) {
+ window.arguments[1].refresh = false;
+ }
+
+ gAcceptanceRadio = document.getElementById("acceptanceRadio");
+ gPersonalRadio = document.getElementById("personalRadio");
+
+ gKeyId = window.arguments[0].keyId;
+
+ gOkButton = document.querySelector("dialog").getButton("accept");
+ gOkButton.focus();
+
+ await reloadData(true);
+
+ let sepPassphraseEnabled =
+ gModePersonal &&
+ Services.prefs.getBoolPref("mail.openpgp.passphrases.enabled");
+ document.getElementById("passphraseTab").hidden = !sepPassphraseEnabled;
+ document.getElementById("passphrasePanel").hidden = !sepPassphraseEnabled;
+ if (sepPassphraseEnabled) {
+ await loadPassphraseProtection();
+ }
+
+ onAcceptanceChanged();
+}
+
+/***
+ * Set the label text of a HTML element
+ */
+function setLabel(elementId, label) {
+ let node = document.getElementById(elementId);
+ node.setAttribute("value", label);
+}
+
+async function changeExpiry() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj || !keyObj.secretAvailable) {
+ return;
+ }
+
+ if (!keyObj.iSimpleOneSubkeySameExpiry()) {
+ Services.prompt.alert(
+ null,
+ document.title,
+ await document.l10n.formatValue("openpgp-cannot-change-expiry")
+ );
+ return;
+ }
+
+ let args = {
+ keyId: keyObj.keyId,
+ modified: onDataModified,
+ };
+
+ // The keyDetailsDlg can be opened from different locations, some of which
+ // don't belong to the Account Settings, therefore they won't have access to
+ // the gSubDialog object.
+ if (parent.gSubDialog) {
+ parent.gSubDialog.open(
+ "chrome://openpgp/content/ui/changeExpiryDlg.xhtml",
+ undefined,
+ args
+ );
+ return;
+ }
+
+ window.openDialog(
+ "chrome://openpgp/content/ui/changeExpiryDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable",
+ args
+ );
+}
+
+async function refreshOnline() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj) {
+ return;
+ }
+
+ let imported = await KeyLookupHelper.lookupAndImportByKeyID(
+ "interactive-import",
+ window,
+ keyObj.fpr,
+ true
+ );
+ if (imported) {
+ onDataModified();
+ }
+}
+
+async function loadPassphraseProtection() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj || !keyObj.secretAvailable) {
+ return;
+ }
+
+ let primaryKey = RnpPrivateKeyUnlockTracker.constructFromFingerprint(
+ keyObj.fpr
+ );
+ primaryKey.setAllowPromptingUserForPassword(false);
+ primaryKey.setAllowAutoUnlockWithCachedPasswords(false);
+ let isSecretForPrimaryAvailable = primaryKey.available();
+ let canUnlockSecretForPrimary = false;
+ if (isSecretForPrimaryAvailable) {
+ await primaryKey.unlock();
+ canUnlockSecretForPrimary = primaryKey.isUnlocked();
+ gPrivateKeyTrackers.push(primaryKey);
+ }
+
+ let countSubkeysWithSecretAvailable = 0;
+ let countSubkeysCanAutoUnlock = 0;
+
+ for (let i = 0; i < keyObj.subKeys.length; i++) {
+ let subKey = RnpPrivateKeyUnlockTracker.constructFromFingerprint(
+ keyObj.subKeys[i].fpr
+ );
+ subKey.setAllowPromptingUserForPassword(false);
+ subKey.setAllowAutoUnlockWithCachedPasswords(false);
+ let isSecretForPrimaryAvailable = subKey.available();
+ let canUnlockSecretForPrimary = false;
+ if (isSecretForPrimaryAvailable) {
+ ++countSubkeysWithSecretAvailable;
+ await subKey.unlock();
+ canUnlockSecretForPrimary = subKey.isUnlocked();
+ if (canUnlockSecretForPrimary) {
+ countSubkeysCanAutoUnlock++;
+ }
+ gPrivateKeyTrackers.push(subKey);
+ }
+ }
+
+ let userPassphraseMode = "user-passphrase";
+ let usingPP = LoginHelper.isPrimaryPasswordSet();
+ let protectionMode;
+
+ // Could we use the automatic passphrase to unlock all secret keys for
+ // which the key material is available?
+
+ if (
+ (!isSecretForPrimaryAvailable || canUnlockSecretForPrimary) &&
+ countSubkeysWithSecretAvailable == countSubkeysCanAutoUnlock
+ ) {
+ protectionMode = usingPP ? "primary-password" : "unprotected";
+ } else {
+ protectionMode = userPassphraseMode;
+ }
+
+ // Strings used here:
+ // openpgp-passphrase-status-unprotected
+ // openpgp-passphrase-status-primary-password
+ // openpgp-passphrase-status-user-passphrase
+ document.l10n.setAttributes(
+ document.getElementById("passphraseStatus"),
+ `openpgp-passphrase-status-${protectionMode}`
+ );
+
+ // Strings used here:
+ // openpgp-passphrase-instruction-unprotected
+ // openpgp-passphrase-instruction-primary-password
+ // openpgp-passphrase-instruction-user-passphrase
+ document.l10n.setAttributes(
+ document.getElementById("passphraseInstruction"),
+ `openpgp-passphrase-instruction-${protectionMode}`
+ );
+
+ document.getElementById("unlockBox").hidden =
+ protectionMode != userPassphraseMode;
+ document.getElementById("lockBox").hidden =
+ protectionMode == userPassphraseMode;
+ document.getElementById("usePrimaryPassword").hidden = true;
+ document.getElementById("removeProtection").hidden = true;
+
+ document.l10n.setAttributes(
+ document.getElementById("setPassphrase"),
+ protectionMode == userPassphraseMode
+ ? "openpgp-passphrase-change"
+ : "openpgp-passphrase-set"
+ );
+
+ document.getElementById("passwordInput").value = "";
+ document.getElementById("passwordConfirm").value = "";
+}
+
+async function unlock() {
+ let pwCache = {
+ passwords: [],
+ };
+
+ for (let tracker of gPrivateKeyTrackers) {
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ tracker.setPasswordCache(pwCache);
+ await tracker.unlock();
+ if (!tracker.isUnlocked()) {
+ return;
+ }
+ }
+
+ document.l10n.setAttributes(
+ document.getElementById("passphraseInstruction"),
+ "openpgp-passphrase-unlocked"
+ );
+ document.getElementById("unlockBox").hidden = true;
+ document.getElementById("lockBox").hidden = false;
+ document.getElementById("passwordInput").value = "";
+ document.getElementById("passwordConfirm").value = "";
+
+ document.getElementById(
+ LoginHelper.isPrimaryPasswordSet()
+ ? "usePrimaryPassword"
+ : "removeProtection"
+ ).hidden = false;
+
+ // Necessary to set the disabled status of the button
+ onPasswordInput();
+}
+
+function onPasswordInput() {
+ let pw1 = document.getElementById("passwordInput").value;
+ let pw2 = document.getElementById("passwordConfirm").value;
+
+ // Disable the button if the two passwords don't match, and enable it
+ // if the passwords do match.
+ let disabled = pw1 != pw2 || !pw1.length;
+
+ document.getElementById("setPassphrase").disabled = disabled;
+}
+
+async function setPassphrase() {
+ let pw = document.getElementById("passwordInput").value;
+
+ for (let tracker of gPrivateKeyTrackers) {
+ tracker.setPassphrase(pw);
+ }
+ await RNP.saveKeyRings();
+
+ releasePrivateKeys();
+ loadPassphraseProtection();
+}
+
+async function useAutoPassphrase() {
+ for (let tracker of gPrivateKeyTrackers) {
+ await tracker.setAutoPassphrase();
+ }
+ await RNP.saveKeyRings();
+
+ releasePrivateKeys();
+ loadPassphraseProtection();
+}
+
+function onAcceptanceChanged() {
+ // The check for gAcceptedEmails.size is to handle an edge case.
+ // If a key was previously accepted, for an email address that is
+ // now revoked, and another email address has been added,
+ // then the key can be marked as accepted without any accepted
+ // email address.
+ // In this scenario, we must allow the user to edit the accepted
+ // email addresses, even if there's just one email address available.
+ // Another scenario is a data inconsistency, with accepted key,
+ // but no accepted email.
+
+ let originalAccepted = isAccepted(gOriginalAcceptance);
+ let wantAccepted = isAccepted(gAcceptanceRadio.value);
+
+ let disableEmailsTab =
+ (wantAccepted &&
+ gAllEmails.length < 2 &&
+ gAcceptedEmails.size != 0 &&
+ (!originalAccepted || !gHaveUnacceptedEmails)) ||
+ !wantAccepted;
+
+ document.getElementById("emailAddressesTab").disabled = disableEmailsTab;
+ document.getElementById("emailAddressesPanel").disabled = disableEmailsTab;
+
+ setOkButtonState();
+}
+
+function onDataModified() {
+ EnigmailKeyRing.clearCache();
+ enableRefresh();
+ reloadData(false);
+}
+
+function isAccepted(value) {
+ return value == "unverified" || value == "verified";
+}
+
+async function reloadData(firstLoad) {
+ gUserId = null;
+
+ var treeChildren = document.getElementById("keyListChildren");
+
+ // clean lists
+ while (treeChildren.firstChild) {
+ treeChildren.firstChild.remove();
+ }
+
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+ if (!keyObj) {
+ return;
+ }
+
+ let acceptanceIntroText = "";
+ let acceptanceVerificationText = "";
+
+ if (keyObj.fpr) {
+ gFingerprint = keyObj.fpr;
+ setLabel("fingerprint", EnigmailKey.formatFpr(keyObj.fpr));
+ }
+
+ gSigTree = document.getElementById("signatures_tree");
+ let cApi = EnigmailCryptoAPI();
+ let signatures = await cApi.getKeyObjSignatures(keyObj);
+ gSigTree.view = new SigListView(signatures);
+
+ document.getElementById("subkeyList").view = new SubkeyListView(keyObj);
+
+ gUserId = keyObj.userId;
+
+ setLabel("keyId", "0x" + keyObj.keyId);
+ setLabel("keyCreated", keyObj.created);
+
+ let keyIsExpired =
+ keyObj.effectiveExpiryTime &&
+ keyObj.effectiveExpiryTime < Math.floor(Date.now() / 1000);
+
+ let expiryInfo;
+ let expireArgument = null;
+ let expiryInfoKey = "";
+ if (keyObj.keyTrust == "r") {
+ expiryInfoKey = "key-revoked-simple";
+ } else if (keyObj.keyTrust == "e" || keyIsExpired) {
+ expiryInfoKey = "key-expired-date";
+ expireArgument = keyObj.effectiveExpiry;
+ } else if (keyObj.effectiveExpiry.length === 0) {
+ expiryInfoKey = "key-does-not-expire";
+ } else {
+ expiryInfo = keyObj.effectiveExpiry;
+ }
+ if (expiryInfoKey) {
+ expiryInfo = l10n.formatValueSync(expiryInfoKey, {
+ keyExpiry: expireArgument,
+ });
+ }
+ setLabel("keyExpiry", expiryInfo);
+
+ gModePersonal = keyObj.secretAvailable;
+
+ document.getElementById("passphraseTab").hidden = !gModePersonal;
+ document.getElementById("passphrasePanel").hidden = !gModePersonal;
+
+ if (gModePersonal) {
+ gPersonalRadio.removeAttribute("hidden");
+ gAcceptanceRadio.setAttribute("hidden", "true");
+ acceptanceIntroText = "key-accept-personal";
+ let value = l10n.formatValueSync("key-type-pair");
+ setLabel("keyType", value);
+
+ gUpdateAllowed = true;
+ if (firstLoad) {
+ gOriginalPersonal = await PgpSqliteDb2.isAcceptedAsPersonalKey(
+ keyObj.fpr
+ );
+ gPersonalRadio.value = gOriginalPersonal ? "personal" : "not_personal";
+ }
+
+ if (keyObj.keyTrust != "r") {
+ document.getElementById("changeExpiryButton").removeAttribute("hidden");
+ }
+ } else {
+ gPersonalRadio.setAttribute("hidden", "true");
+ let value = l10n.formatValueSync("key-type-public");
+ setLabel("keyType", value);
+
+ let isStillValid = !(
+ keyObj.keyTrust == "r" ||
+ keyObj.keyTrust == "e" ||
+ keyIsExpired
+ );
+ if (!isStillValid) {
+ gAcceptanceRadio.setAttribute("hidden", "true");
+ if (keyObj.keyTrust == "r") {
+ acceptanceIntroText = "key-revoked-simple";
+ } else if (keyObj.keyTrust == "e" || keyIsExpired) {
+ acceptanceIntroText = "key-expired-simple";
+ }
+ } else {
+ gAcceptanceRadio.removeAttribute("hidden");
+ acceptanceIntroText = "key-do-you-accept";
+ acceptanceVerificationText = "key-verification";
+ gUpdateAllowed = true;
+
+ //await RNP.calculateAcceptance(keyObj.keyId, null);
+
+ let acceptanceResult = await PgpSqliteDb2.getFingerprintAcceptance(
+ null,
+ keyObj.fpr
+ );
+
+ if (firstLoad) {
+ if (!acceptanceResult) {
+ gOriginalAcceptance = "undecided";
+ } else {
+ gOriginalAcceptance = acceptanceResult;
+ }
+ gAcceptanceRadio.value = gOriginalAcceptance;
+ }
+ }
+
+ if (firstLoad) {
+ gAcceptedEmails = new Set();
+
+ for (let i = 0; i < keyObj.userIds.length; i++) {
+ if (keyObj.userIds[i].type === "uid") {
+ let uidEmail = EnigmailFuncs.getEmailFromUserID(
+ keyObj.userIds[i].userId
+ );
+ if (uidEmail) {
+ gAllEmails.push(uidEmail);
+
+ if (isAccepted(gOriginalAcceptance)) {
+ let rv = {};
+ await PgpSqliteDb2.getAcceptance(keyObj.fpr, uidEmail, rv);
+ if (rv.emailDecided) {
+ gAcceptedEmails.add(uidEmail);
+ } else {
+ gHaveUnacceptedEmails = true;
+ }
+ } else {
+ // For not-yet-accepted keys, our default is to accept
+ // all shown email addresses.
+ gAcceptedEmails.add(uidEmail);
+ }
+ }
+ }
+ }
+
+ // clone
+ gOriginalAcceptedEmails = new Set(gAcceptedEmails);
+ }
+ }
+
+ await createUidData(keyObj);
+
+ if (acceptanceIntroText) {
+ let acceptanceIntro = document.getElementById("acceptanceIntro");
+ document.l10n.setAttributes(acceptanceIntro, acceptanceIntroText);
+ }
+
+ if (acceptanceVerificationText) {
+ let acceptanceVerification = document.getElementById(
+ "acceptanceVerification"
+ );
+ document.l10n.setAttributes(
+ acceptanceVerification,
+ acceptanceVerificationText,
+ {
+ addr: EnigmailFuncs.getEmailFromUserID(gUserId).toLowerCase(),
+ }
+ );
+ }
+
+ document.getElementById("key-detail-has-insecure").hidden =
+ !keyObj.hasIgnoredAttributes;
+}
+
+function setOkButtonState() {
+ let atLeastOneChecked = gAllEmailCheckboxes.some(c => c.checked);
+ gOkButton.disabled = !atLeastOneChecked && isAccepted(gAcceptanceRadio.value);
+}
+
+async function createUidData(keyDetails) {
+ var uidList = document.getElementById("userIds");
+ while (uidList.firstChild) {
+ uidList.firstChild.remove();
+ }
+
+ let primaryIdIndex = 0;
+
+ for (let i = 0; i < keyDetails.userIds.length; i++) {
+ if (keyDetails.userIds[i].type === "uid") {
+ if (keyDetails.userIds[i].userId == keyDetails.userId) {
+ primaryIdIndex = i;
+ break;
+ }
+ }
+ }
+
+ for (let i = -1; i < keyDetails.userIds.length; i++) {
+ // Handle entry primaryIdIndex first.
+
+ let indexToUse;
+ if (i == -1) {
+ indexToUse = primaryIdIndex;
+ } else if (i == primaryIdIndex) {
+ // already handled when i was -1
+ continue;
+ } else {
+ indexToUse = i;
+ }
+
+ if (keyDetails.userIds[indexToUse].type === "uid") {
+ let uidStr = keyDetails.userIds[indexToUse].userId;
+
+ /* - attempted code with <ul id="userIds">, doesn't work yet
+ let item = document.createElement("li");
+
+ let text = document.createElement("div");
+ text.textContent = uidStr;
+ item.append(text);
+
+ let lf = document.createElement("br");
+ item.append(lf);
+ uidList.appendChild(item);
+ */
+
+ uidList.appendItem(uidStr);
+ }
+ }
+
+ if (gModePersonal) {
+ document.getElementById("emailAddressesTab").hidden = true;
+ } else {
+ let emailList = document.getElementById("addressesList");
+
+ let atLeastOneChecked = false;
+ let gUniqueEmails = new Set();
+
+ for (let i = 0; i < gAllEmails.length; i++) {
+ let email = gAllEmails[i];
+ if (gUniqueEmails.has(email)) {
+ continue;
+ }
+ gUniqueEmails.add(email);
+
+ let checkbox = document.createXULElement("checkbox");
+
+ checkbox.value = email;
+ checkbox.setAttribute("label", email);
+
+ checkbox.checked = gAcceptedEmails.has(email);
+ if (checkbox.checked) {
+ atLeastOneChecked = true;
+ }
+
+ checkbox.disabled = !gUpdateAllowed;
+ checkbox.addEventListener("command", () => {
+ setOkButtonState();
+ });
+
+ emailList.appendChild(checkbox);
+ gAllEmailCheckboxes.push(checkbox);
+ }
+
+ // Usually, if we have only one email address available,
+ // we want to hide the tab.
+ // There are edge cases - if we have a data inconsistency
+ // (key accepted, but no email accepted), then we must show,
+ // to allow the user to repair.
+
+ document.getElementById("emailAddressesTab").hidden =
+ gUniqueEmails.size < 2 && atLeastOneChecked;
+ }
+}
+
+function setAttr(attribute, value) {
+ var elem = document.getElementById(attribute);
+ if (elem) {
+ elem.value = value;
+ }
+}
+
+function enableRefresh() {
+ if (window.arguments[1]) {
+ window.arguments[1].refresh = true;
+ }
+
+ window.arguments[0].modified();
+}
+
+// ------------------ onCommand Functions -----------------
+
+/*
+function signKey() {
+ if (EnigmailWindows.signKey(window, gUserId, gKeyId)) {
+ enableRefresh();
+ reloadData(false);
+ }
+}
+*/
+
+/*
+function manageUids() {
+ let keyObj = EnigmailKeyRing.getKeyById(gKeyId);
+
+ var inputObj = {
+ keyId: keyObj.keyId,
+ ownKey: keyObj.secretAvailable,
+ };
+
+ var resultObj = {
+ refresh: false,
+ };
+ window.openDialog(
+ "chrome://openpgp/content/ui/enigmailManageUidDlg.xhtml",
+ "",
+ "dialog,modal,centerscreen,resizable=yes",
+ inputObj,
+ resultObj
+ );
+ if (resultObj.refresh) {
+ enableRefresh();
+ reloadData(false);
+ }
+}
+*/
+
+function genRevocationCert() {
+ throw new Error("Not implemented");
+
+ /*
+ var defaultFileName = userId.replace(/[<>]/g, "");
+ defaultFileName += " (0x" + keyId + ") rev.asc";
+ var outFile = EnigFilePicker("XXXsaveRevokeCertAs",
+ "", true, "*.asc",
+ defaultFileName, ["XXXasciiArmorFile", "*.asc"];
+ if (!outFile) return -1;
+
+ return 0;
+ */
+}
+
+/**
+ * @param {Object[]] signatures - list of signature objects
+ * signatures.userId {string} - User ID.
+ * signatures.uidLabel {string} - UID label.
+ * signatures.created
+ * signatures.fpr {string} - Fingerprint.
+ * signatures.sigList {Object[]} - Objects
+ * signatures.sigList.userId
+ * signatures.sigList.created
+ * signatures.sigList.signerKeyId
+ * signatures.sigList.sigType
+ * signatures.sigList.sigKnown
+ */
+function SigListView(signatures) {
+ this.keyObj = [];
+
+ for (let sig of signatures) {
+ let k = {
+ uid: sig.userId,
+ keyId: sig.keyId,
+ created: sig.created,
+ expanded: true,
+ sigList: [],
+ };
+
+ for (let s of sig.sigList) {
+ k.sigList.push({
+ uid: s.userId,
+ created: s.created,
+ keyId: s.signerKeyId,
+ sigType: s.sigType,
+ });
+ }
+ this.keyObj.push(k);
+ }
+
+ this.prevKeyObj = null;
+ this.prevRow = -1;
+
+ this.updateRowCount();
+}
+
+/**
+ * @implements {nsITreeView}
+ */
+SigListView.prototype = {
+ updateRowCount() {
+ let rc = 0;
+
+ for (let i in this.keyObj) {
+ rc += this.keyObj[i].expanded ? this.keyObj[i].sigList.length + 1 : 1;
+ }
+
+ this.rowCount = rc;
+ },
+
+ setLastKeyObj(keyObj, row) {
+ this.prevKeyObj = keyObj;
+ this.prevRow = row;
+ return keyObj;
+ },
+
+ getSigAtIndex(row) {
+ if (this.lastIndex == row) {
+ return this.lastKeyObj;
+ }
+
+ let j = 0,
+ l = 0;
+
+ for (let i in this.keyObj) {
+ if (j === row) {
+ return this.setLastKeyObj(this.keyObj[i], row);
+ }
+ j++;
+
+ if (this.keyObj[i].expanded) {
+ l = this.keyObj[i].sigList.length;
+
+ if (j + l >= row && row - j < l) {
+ return this.setLastKeyObj(this.keyObj[i].sigList[row - j], row);
+ }
+ j += l;
+ }
+ }
+
+ return null;
+ },
+
+ getCellText(row, column) {
+ let s = this.getSigAtIndex(row);
+
+ if (s) {
+ switch (column.id) {
+ case "sig_uid_col":
+ return s.uid;
+ case "sig_keyid_col":
+ return "0x" + s.keyId;
+ case "sig_created_col":
+ return s.created;
+ }
+ }
+
+ return "";
+ },
+
+ setTree(treebox) {
+ this.treebox = treebox;
+ },
+
+ isContainer(row) {
+ let s = this.getSigAtIndex(row);
+ return "sigList" in s;
+ },
+
+ isSeparator(row) {
+ return false;
+ },
+
+ isSorted() {
+ return false;
+ },
+
+ getLevel(row) {
+ let s = this.getSigAtIndex(row);
+ return "sigList" in s ? 0 : 1;
+ },
+
+ cycleHeader(col, elem) {},
+
+ getImageSrc(row, col) {
+ return null;
+ },
+
+ getRowProperties(row, props) {},
+
+ getCellProperties(row, col) {
+ return "";
+ },
+
+ canDrop(row, orientation, data) {
+ return false;
+ },
+
+ getColumnProperties(colid, col, props) {},
+
+ isContainerEmpty(row) {
+ return false;
+ },
+
+ getParentIndex(idx) {
+ return -1;
+ },
+
+ getProgressMode(row, col) {},
+
+ isContainerOpen(row) {
+ let s = this.getSigAtIndex(row);
+ return s.expanded;
+ },
+
+ isSelectable(row, col) {
+ return true;
+ },
+
+ toggleOpenState(row) {
+ let s = this.getSigAtIndex(row);
+ s.expanded = !s.expanded;
+ let r = this.rowCount;
+ this.updateRowCount();
+ gSigTree.rowCountChanged(row, this.rowCount - r);
+ },
+};
+
+function createSubkeyItem(mainKeyIsSecret, subkey) {
+ // Get expiry state of this subkey
+ let expire;
+ if (subkey.keyTrust === "r") {
+ expire = l10n.formatValueSync("key-valid-revoked");
+ } else if (subkey.expiryTime === 0) {
+ expire = l10n.formatValueSync("key-expiry-never");
+ } else {
+ expire = subkey.expiry;
+ }
+
+ let subkeyType = "";
+
+ if (mainKeyIsSecret && (!subkey.secretAvailable || !subkey.secretMaterial)) {
+ subkeyType = "(!) ";
+ gHasMissingSecret = true;
+ }
+ if (subkey.type === "pub") {
+ subkeyType += l10n.formatValueSync("key-type-primary");
+ } else {
+ subkeyType += l10n.formatValueSync("key-type-subkey");
+ }
+
+ let usagetext = "";
+ let i;
+ // e = encrypt
+ // s = sign
+ // c = certify
+ // a = authentication
+ // Capital Letters are ignored, as these reflect summary properties of a key
+
+ var singlecode = "";
+ for (i = 0; i < subkey.keyUseFor.length; i++) {
+ singlecode = subkey.keyUseFor.substr(i, 1);
+ switch (singlecode) {
+ case "e":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext = usagetext + l10n.formatValueSync("key-usage-encrypt");
+ break;
+ case "s":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext = usagetext + l10n.formatValueSync("key-usage-sign");
+ break;
+ case "c":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext = usagetext + l10n.formatValueSync("key-usage-certify");
+ break;
+ case "a":
+ if (usagetext.length > 0) {
+ usagetext = usagetext + ", ";
+ }
+ usagetext =
+ usagetext + l10n.formatValueSync("key-usage-authentication");
+ break;
+ } // * case *
+ } // * for *
+
+ let keyObj = {
+ keyType: subkeyType,
+ keyId: "0x" + subkey.keyId,
+ algo: subkey.algoSym,
+ size: subkey.keySize,
+ creationDate: subkey.created,
+ expiry: expire,
+ usage: usagetext,
+ };
+
+ return keyObj;
+}
+
+function SubkeyListView(keyObj) {
+ gHasMissingSecret = false;
+
+ this.subkeys = [];
+ this.rowCount = keyObj.subKeys.length + 1;
+ this.subkeys.push(createSubkeyItem(keyObj.secretAvailable, keyObj));
+
+ for (let i = 0; i < keyObj.subKeys.length; i++) {
+ this.subkeys.push(
+ createSubkeyItem(keyObj.secretAvailable, keyObj.subKeys[i])
+ );
+ }
+
+ document.getElementById("legendMissingSecret").hidden = !gHasMissingSecret;
+}
+
+// implements nsITreeView
+SubkeyListView.prototype = {
+ getCellText(row, column) {
+ let s = this.subkeys[row];
+
+ if (s) {
+ switch (column.id) {
+ case "keyTypeCol":
+ return s.keyType;
+ case "keyIdCol":
+ return s.keyId;
+ case "algoCol":
+ return s.algo;
+ case "sizeCol":
+ return s.size;
+ case "createdCol":
+ return s.creationDate;
+ case "expiryCol":
+ return s.expiry;
+ case "keyUsageCol":
+ return s.usage;
+ }
+ }
+
+ return "";
+ },
+
+ setTree(treebox) {
+ this.treebox = treebox;
+ },
+
+ isContainer(row) {
+ return false;
+ },
+
+ isSeparator(row) {
+ return false;
+ },
+
+ isSorted() {
+ return false;
+ },
+
+ getLevel(row) {
+ return 0;
+ },
+
+ cycleHeader(col, elem) {},
+
+ getImageSrc(row, col) {
+ return null;
+ },
+
+ getRowProperties(row, props) {},
+
+ getCellProperties(row, col) {
+ return "";
+ },
+
+ canDrop(row, orientation, data) {
+ return false;
+ },
+
+ getColumnProperties(colid, col, props) {},
+
+ isContainerEmpty(row) {
+ return false;
+ },
+
+ getParentIndex(idx) {
+ return -1;
+ },
+
+ getProgressMode(row, col) {},
+
+ isContainerOpen(row) {
+ return false;
+ },
+
+ isSelectable(row, col) {
+ return true;
+ },
+
+ toggleOpenState(row) {},
+};
+
+function sigHandleDblClick(event) {}
+
+document.addEventListener("dialogaccept", async function (event) {
+ // Prevent the closing of the dialog to wait until all the SQLite operations
+ // have properly been executed.
+ event.preventDefault();
+
+ // The user's personal OpenPGP key acceptance was edited.
+ if (gModePersonal) {
+ if (gUpdateAllowed && gPersonalRadio.value != gOriginalPersonal) {
+ if (gPersonalRadio.value == "personal") {
+ await PgpSqliteDb2.acceptAsPersonalKey(gFingerprint);
+ } else {
+ await PgpSqliteDb2.deletePersonalKeyAcceptance(gFingerprint);
+ }
+
+ enableRefresh();
+ }
+ window.close();
+ return;
+ }
+
+ // If the recipient's key hasn't been revoked or invalidated, and the
+ // signature acceptance was edited.
+ if (gUpdateAllowed) {
+ let selectedEmails = new Set();
+ for (let checkbox of gAllEmailCheckboxes) {
+ if (checkbox.checked) {
+ selectedEmails.add(checkbox.value);
+ }
+ }
+
+ if (
+ gAcceptanceRadio.value != gOriginalAcceptance ||
+ !CommonUtils.setEqual(gAcceptedEmails, selectedEmails)
+ ) {
+ await PgpSqliteDb2.updateAcceptance(
+ gFingerprint,
+ [...selectedEmails],
+ gAcceptanceRadio.value
+ );
+
+ enableRefresh();
+ }
+ }
+
+ window.close();
+});
diff --git a/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml
new file mode 100644
index 0000000000..a7f57d0339
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml
@@ -0,0 +1,405 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/keyDetails.css" type="text/css"?>
+
+<!DOCTYPE html>
+<html
+ id="enigmailKeyDetailsDlg"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title data-l10n-id="openpgp-key-details-doc-title"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js" />
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js" />
+ <script defer="defer" src="chrome://openpgp/content/ui/enigmailCommon.js" />
+ <script
+ defer="defer"
+ src="chrome://openpgp/content/ui/enigmailKeyManager.js"
+ />
+ <script defer="defer" src="chrome://openpgp/content/ui/keyDetailsDlg.js" />
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="openpgp-card-details-close-window-label"
+ >
+ <html:div class="key-details-container">
+ <html:aside class="key-details-grid">
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-user-id3-label"
+ />
+
+ <richlistbox
+ id="userIds"
+ class="additional-key-identity plain"
+ flex="1"
+ />
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-key-type-label"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyType"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-key-id-label"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyId"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-fingerprint-label"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="fingerprint"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-created-header"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyCreated"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+
+ <label
+ class="key-detail-label"
+ data-l10n-id="openpgp-key-details-expiry-header"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="keyExpiry"
+ type="text"
+ class="plain"
+ readonly="readonly"
+ value="?"
+ />
+ </hbox>
+ </html:aside>
+
+ <html:aside>
+ <vbox>
+ <button
+ id="refreshOnlineButton"
+ data-l10n-id="openpgp-key-man-refresh-online"
+ oncommand="refreshOnline()"
+ />
+ <button
+ id="changeExpiryButton"
+ data-l10n-id="openpgp-key-man-change-expiry"
+ oncommand="changeExpiry()"
+ hidden="true"
+ />
+ </vbox>
+ </html:aside>
+ </html:div>
+
+ <html:div id="key-detail-has-insecure" hidden="hidden">
+ <html:span
+ class="tail-with-learn-more"
+ data-l10n-id="openpgp-key-details-attr-ignored"
+ ></html:span>
+ <label
+ is="text-link"
+ href="https://support.mozilla.org/kb/openpgp-unsafe-key-properties-ignored"
+ data-l10n-id="e2e-learn-more"
+ />
+ </html:div>
+
+ <separator />
+
+ <tabbox flex="1" style="margin: 5px" id="mainTabs">
+ <tabs id="mainTabBox">
+ <tab id="acceptanceTab" data-l10n-id="openpgp-acceptance-label" />
+ <tab
+ id="emailAddressesTab"
+ data-l10n-id="openpgp-key-man-ignored-ids"
+ />
+ <tab
+ id="passphraseTab"
+ data-l10n-id="openpgp-passphrase-protection"
+ />
+ <tab
+ id="signaturesTab"
+ data-l10n-id="openpgp-key-details-signatures-tab"
+ />
+ <tab
+ id="structureTab"
+ data-l10n-id="openpgp-key-details-structure-tab"
+ />
+ </tabs>
+
+ <tabpanels flex="1" id="mainTabPanel">
+ <!-- Acceptance Tab -->
+ <vbox id="acceptancePanel" flex="1">
+ <description id="acceptanceIntro" />
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <radiogroup
+ id="acceptanceRadio"
+ hidden="true"
+ class="indent"
+ oncommand="onAcceptanceChanged();"
+ >
+ <radio
+ id="acceptRejected"
+ value="rejected"
+ data-l10n-id="openpgp-acceptance-rejected-label"
+ />
+ <radio
+ id="acceptUndecided"
+ value="undecided"
+ data-l10n-id="openpgp-acceptance-undecided-label"
+ />
+ <radio
+ id="acceptUnverified"
+ value="unverified"
+ data-l10n-id="openpgp-acceptance-unverified-label"
+ />
+ <radio
+ id="acceptVerified"
+ value="verified"
+ data-l10n-id="openpgp-acceptance-verified-label"
+ />
+ </radiogroup>
+ <radiogroup id="personalRadio" class="indent" hidden="true">
+ <radio
+ id="notPersonal"
+ value="not_personal"
+ data-l10n-id="openpgp-personal-no-label"
+ />
+ <radio
+ id="yesPersonal"
+ value="personal"
+ data-l10n-id="openpgp-personal-yes-label"
+ />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+ <description id="acceptanceVerification" />
+ </vbox>
+
+ <!-- email addresses tab -->
+ <vbox id="emailAddressesPanel" flex="1">
+ <description data-l10n-id="openpgp-ign-addr-intro" />
+ <separator class="thin" />
+
+ <vbox id="addressesListContainer">
+ <vbox id="addressesList" class="indent" />
+ </vbox>
+ </vbox>
+
+ <!-- passphrase tab -->
+ <vbox id="passphrasePanel" flex="1">
+ <description id="passphraseStatus" />
+ <separator class="thin" />
+ <description id="passphraseInstruction" />
+ <separator class="thin" />
+
+ <vbox id="unlockBox">
+ <hbox>
+ <button
+ id="unlock"
+ data-l10n-id="openpgp-passphrase-unlock"
+ oncommand="unlock()"
+ />
+ </hbox>
+ </vbox>
+
+ <vbox id="lockBox">
+ <hbox>
+ <label data-l10n-id="openpgp-passphrase-new" />
+ <html:input
+ id="passwordInput"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput();"
+ />
+ </hbox>
+ <hbox>
+ <label data-l10n-id="openpgp-passphrase-new-repeat" />
+ <html:input
+ id="passwordConfirm"
+ type="password"
+ class="input-inline"
+ oninput="onPasswordInput();"
+ />
+ <button
+ id="setPassphrase"
+ disabled="true"
+ oncommand="setPassphrase();"
+ />
+ </hbox>
+ <separator class="thin" />
+ </vbox>
+
+ <hbox>
+ <button
+ id="removeProtection"
+ hidden="true"
+ data-l10n-id="openpgp-remove-protection"
+ oncommand="useAutoPassphrase()"
+ />
+ <button
+ id="usePrimaryPassword"
+ hidden="true"
+ data-l10n-id="openpgp-use-primary-password"
+ oncommand="useAutoPassphrase()"
+ />
+ </hbox>
+ </vbox>
+
+ <!-- certifications tab -->
+ <vbox id="signaturesPanel">
+ <tree
+ id="signatures_tree"
+ flex="1"
+ hidecolumnpicker="true"
+ ondblclick="sigHandleDblClick(event)"
+ >
+ <treecols>
+ <treecol
+ id="sig_uid_col"
+ style="flex: 1 auto"
+ data-l10n-id="openpgp-key-details-uid-certified-col"
+ primary="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="sig_keyid_col"
+ data-l10n-id="openpgp-key-id-label"
+ persist="width"
+ minwidth="140"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="sig_created_col"
+ data-l10n-id="openpgp-key-details-created-label"
+ persist="width"
+ minwidth="100"
+ />
+ </treecols>
+
+ <treechildren />
+ </tree>
+ </vbox>
+
+ <!-- structure tab -->
+ <vbox id="structurePanel">
+ <hbox flex="1">
+ <tree
+ id="subkeyList"
+ flex="1"
+ enableColumnDrag="true"
+ hidecolumnpicker="false"
+ >
+ <treecols>
+ <treecol
+ id="keyTypeCol"
+ data-l10n-id="openpgp-key-details-key-part-label"
+ style="width: 71px"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="keyUsageCol"
+ data-l10n-id="openpgp-key-details-usage-label"
+ style="flex: 1 auto"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="keyIdCol"
+ style="width: 77px"
+ data-l10n-id="openpgp-key-details-id-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="algoCol"
+ style="width: 60px"
+ data-l10n-id="openpgp-key-details-algorithm-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="sizeCol"
+ style="width: 37px"
+ data-l10n-id="openpgp-key-details-size-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="createdCol"
+ style="width: 70px"
+ data-l10n-id="openpgp-key-details-created-label"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="expiryCol"
+ style="width: 70px"
+ data-l10n-id="openpgp-key-details-expiry-label"
+ persist="width"
+ />
+ </treecols>
+
+ <treechildren id="keyListChildren" />
+ </tree>
+ </hbox>
+ <label
+ id="legendMissingSecret"
+ class="tip-caption"
+ data-l10n-id="openpgp-key-details-legend-secret-missing"
+ hidden="true"
+ />
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mail/extensions/openpgp/content/ui/keyWizard.js b/comm/mail/extensions/openpgp/content/ui/keyWizard.js
new file mode 100644
index 0000000000..fe699bcc7e
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyWizard.js
@@ -0,0 +1,1195 @@
+/* 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";
+
+/* global GetEnigmailSvc */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { EnigmailCryptoAPI } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/cryptoAPI.jsm"
+);
+var { OpenPGPMasterpass } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/masterpass.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+});
+
+// UI variables.
+var gIdentity;
+var gIdentityList;
+var gSubDialog;
+var kStartSection;
+var kDialog;
+var kCurrentSection = "start";
+var kGenerating = false;
+var kButtonLabel;
+
+// OpenPGP variables.
+var gKeygenRequest;
+var gAllData = "";
+var gGeneratedKey = null;
+var gFiles;
+
+const DEFAULT_FILE_PERMS = 0o600;
+
+// The revocation strings are not localization since the revocation certificate
+// will be published to others who may not know the native language of the user.
+const revocationFilePrefix1 =
+ "This is a revocation certificate for the OpenPGP key:";
+const revocationFilePrefix2 = `
+A revocation certificate is kind of a "kill switch" to publicly
+declare that a key shall no longer be used. It is not possible
+to retract such a revocation certificate once it has been published.
+
+Use it to revoke this key in case of a secret key compromise, or loss of
+the secret key, or loss of passphrase of the secret key.
+
+To avoid an accidental use of this file, a colon has been inserted
+before the 5 dashes below. Remove this colon with a text editor
+before importing and publishing this revocation certificate.
+
+:`;
+
+var syncl10n = new Localization(["messenger/openpgp/keyWizard.ftl"], true);
+
+// Dialog event listeners.
+document.addEventListener("dialogaccept", wizardContinue);
+document.addEventListener("dialogextra1", goBack);
+document.addEventListener("dialogcancel", onClose);
+
+/**
+ * Initialize the keyWizard dialog.
+ */
+async function init() {
+ gSubDialog = window.arguments[0].gSubDialog;
+ gIdentity = window.arguments[0].identity || null;
+ gIdentityList = document.getElementById("userIdentity");
+
+ kStartSection = document.getElementById("wizardStart");
+ kDialog = document.querySelector("dialog");
+
+ await initIdentity();
+
+ // Show the GnuPG radio selection if the pref is enabled.
+ if (Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg")) {
+ document.getElementById("externalOpenPgp").removeAttribute("hidden");
+ }
+
+ // After the dialog is visible, disable the event listeners causing it to
+ // close when clicking on the overlay or hitting the Esc key, and remove the
+ // close button from the header. This is necessary to control the escape
+ // point and prevent the accidental dismiss of the dialog during important
+ // processes, like the generation or importing of a key.
+ setTimeout(() => {
+ // Check if the attribute is not null. This can be removed after the full
+ // conversion of the Key Manager into a SubDialog in Bug 1652537.
+ if (gSubDialog) {
+ gSubDialog._topDialog._removeDialogEventListeners();
+ gSubDialog._topDialog._closeButton.remove();
+ resizeDialog();
+ }
+ }, 150);
+
+ // Switch directly to the create screen if requested by the user.
+ if (window.arguments[0].isCreate) {
+ document.getElementById("openPgpKeyChoices").value = 0;
+
+ switchSection();
+ }
+
+ // Switch directly to the import screen if requested by the user.
+ if (window.arguments[0].isImport) {
+ document.getElementById("openPgpKeyChoices").value = 1;
+
+ // Disable the "Continue" button so the user can't accidentally click on it.
+ // See bug 1689980.
+ kDialog.getButton("accept").setAttribute("disabled", true);
+
+ switchSection();
+ }
+}
+
+function onProtectionChange() {
+ let pw1Element = document.getElementById("passwordInput");
+ let pw2Element = document.getElementById("passwordConfirm");
+
+ let pw1 = pw1Element.value;
+ let pw2 = pw2Element.value;
+
+ let inputDisabled = document.getElementById("keygenAutoProtection").selected;
+ pw1Element.disabled = inputDisabled;
+ pw2Element.disabled = inputDisabled;
+
+ let buttonEnabled = inputDisabled || (!inputDisabled && pw1 == pw2 && pw1);
+ let ok = kDialog.getButton("accept");
+ ok.disabled = !buttonEnabled;
+}
+
+/**
+ * Populate the identity menulist with all the valid and available identities
+ * and autoselect the current identity if available.
+ */
+async function initIdentity() {
+ let identityListPopup = document.getElementById("userIdentityPopup");
+
+ for (let identity of MailServices.accounts.allIdentities) {
+ // Skip invalid and non-email identities.
+ if (!identity.valid || !identity.email) {
+ continue;
+ }
+
+ // Interrupt if no server was defined for this identity.
+ let servers = MailServices.accounts.getServersForIdentity(identity);
+ if (servers.length == 0) {
+ continue;
+ }
+
+ let item = document.createXULElement("menuitem");
+ item.setAttribute(
+ "label",
+ `${identity.identityName} - ${servers[0].prettyName}`
+ );
+ item.setAttribute("class", "identity-popup-item");
+ item.setAttribute("accountname", servers[0].prettyName);
+ item.setAttribute("identitykey", identity.key);
+ item.setAttribute("email", identity.email);
+
+ identityListPopup.appendChild(item);
+
+ if (gIdentity && gIdentity.key == identity.key) {
+ gIdentityList.selectedItem = item;
+ }
+ }
+
+ // If not identity was originally passed during the creation of this dialog,
+ // select the first available value.
+ if (!gIdentity) {
+ gIdentityList.selectedIndex = 0;
+ }
+
+ await setIdentity();
+}
+
+/**
+ * Update the currently used identity to reflect the user selection from the
+ * identity menulist.
+ */
+async function setIdentity() {
+ if (gIdentityList.selectedItem) {
+ gIdentity = MailServices.accounts.getIdentity(
+ gIdentityList.selectedItem.getAttribute("identitykey")
+ );
+
+ document.l10n.setAttributes(
+ document.documentElement,
+ "key-wizard-dialog-window",
+ {
+ identity: gIdentity.email,
+ }
+ );
+ }
+}
+
+/**
+ * Intercept the dialogaccept command to implement a wizard like setup workflow.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function wizardContinue(event) {
+ event.preventDefault();
+
+ // Pretty impossible scenario but just in case if no radio button is
+ // currently selected, bail out.
+ if (!document.getElementById("openPgpKeyChoices").value) {
+ return;
+ }
+
+ // Trigger an action based on the currently visible section.
+ if (kCurrentSection != "start") {
+ wizardNextStep();
+ return;
+ }
+
+ // Disable the `Continue` button.
+ kDialog.getButton("accept").setAttribute("disabled", true);
+
+ kStartSection.addEventListener("transitionend", switchSection, {
+ once: true,
+ });
+ kStartSection.classList.add("hide");
+}
+
+/**
+ * Separated method dealing with the section switching to allow the removal of
+ * the event listener to prevent stacking.
+ */
+function switchSection() {
+ kStartSection.setAttribute("hidden", true);
+
+ // Save the current label of the accept button in order to restore it later.
+ kButtonLabel = kDialog.getButton("accept").label;
+
+ // Update the UI based on the radiogroup selection.
+ switch (document.getElementById("openPgpKeyChoices").value) {
+ case "0":
+ wizardCreateKey();
+ break;
+
+ case "1":
+ wizardImportKey();
+ break;
+
+ case "2":
+ wizardExternalKey();
+ break;
+ }
+
+ // Show the `Go back` button.
+ kDialog.getButton("extra1").hidden = false;
+ resizeDialog();
+}
+
+/**
+ * Handle the next step of the wizard based on the currently visible section.
+ */
+async function wizardNextStep() {
+ switch (kCurrentSection) {
+ case "create":
+ await openPgpKeygenStart();
+ break;
+
+ case "import":
+ await openPgpImportStart();
+ break;
+
+ case "importComplete":
+ openPgpImportComplete();
+ break;
+
+ case "external":
+ openPgpExternalComplete();
+ break;
+ }
+}
+
+/**
+ * Go back to the initial view of the wizard.
+ */
+function goBack() {
+ let section = document.querySelector(".wizard-section:not([hidden])");
+ section.addEventListener("transitionend", backToStart, { once: true });
+ section.classList.add("hide-reverse");
+}
+
+/**
+ * Hide the currently visible section at the end of the animation, remove the
+ * listener to prevent stacking, and trigger the reveal of the first section.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function backToStart(event) {
+ // Hide the `Go Back` button.
+ kDialog.getButton("extra1").hidden = true;
+
+ // Enable the `Continue` button.
+ kDialog.getButton("accept").removeAttribute("disabled");
+
+ kDialog.getButton("accept").label = kButtonLabel;
+ kDialog.getButton("accept").classList.remove("primary");
+
+ // Reset the import section.
+ clearImportWarningNotifications();
+ document.getElementById("importKeyIntro").hidden = false;
+ document.getElementById("importKeyListContainer").collapsed = true;
+
+ event.target.setAttribute("hidden", true);
+
+ // Reset section key.
+ kCurrentSection = "start";
+
+ revealSection("wizardStart");
+}
+
+/**
+ * Create a new inline notification to append to the import warning container.
+ *
+ * @returns {XULElement} - The description element inside the notification.
+ */
+async function addImportWarningNotification() {
+ let notification = document.createXULElement("hbox");
+ notification.classList.add(
+ "inline-notification-container",
+ "error-container"
+ );
+
+ let wrapper = document.createXULElement("hbox");
+ wrapper.classList.add("inline-notification-wrapper", "align-center");
+
+ let image = document.createElement("img");
+ image.classList.add("notification-image");
+ image.setAttribute("src", "chrome://global/skin/icons/warning.svg");
+ image.setAttribute("alt", "");
+
+ let description = document.createXULElement("description");
+
+ wrapper.appendChild(image);
+ wrapper.appendChild(description);
+
+ notification.appendChild(wrapper);
+
+ let container = document.getElementById("openPgpImportWarning");
+ container.appendChild(notification);
+
+ // Show the notification container.
+ container.removeAttribute("hidden");
+
+ return description;
+}
+
+/**
+ * Remove all inline errors from the notification area of the import section.
+ */
+function clearImportWarningNotifications() {
+ let container = document.getElementById("openPgpImportWarning");
+
+ // Remove any existing notification.
+ for (let notification of container.querySelectorAll(
+ ".inline-notification-container"
+ )) {
+ notification.remove();
+ }
+
+ // Hide the entire notification container.
+ container.hidden = true;
+}
+
+/**
+ * Show the Key Creation section.
+ */
+async function wizardCreateKey() {
+ kCurrentSection = "create";
+ revealSection("wizardCreateKey");
+
+ kDialog.getButton("accept").label = await document.l10n.formatValue(
+ "openpgp-keygen-button"
+ );
+ kDialog.getButton("accept").classList.add("primary");
+
+ if (!gIdentity.fullName) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-long-expiry"
+ );
+ return;
+ }
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ document.getElementById("keygenPassphraseSection").hidden =
+ !sepPassphraseEnabled;
+
+ if (sepPassphraseEnabled) {
+ let usingPP = LoginHelper.isPrimaryPasswordSet();
+ let autoProt = document.getElementById("keygenAutoProtection");
+
+ document.l10n.setAttributes(
+ autoProt,
+ usingPP
+ ? "radio-keygen-protect-primary-pass"
+ : "radio-keygen-no-protection"
+ );
+
+ autoProt.setAttribute("selected", true);
+ document
+ .getElementById("keygenPassphraseProtection")
+ .removeAttribute("selected");
+ }
+
+ // This also handles enable/disabling the accept/ok button.
+ onProtectionChange();
+}
+
+/**
+ * Show the Key Import section.
+ */
+function wizardImportKey() {
+ kCurrentSection = "import";
+ revealSection("wizardImportKey");
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ let keepPassphrasesItem = document.getElementById(
+ "openPgpKeygenKeepPassphrases"
+ );
+ keepPassphrasesItem.hidden = !sepPassphraseEnabled;
+ keepPassphrasesItem.checked = false;
+}
+
+/**
+ * Show the Key Setup via external smartcard section.
+ */
+async function wizardExternalKey() {
+ kCurrentSection = "external";
+ revealSection("wizardExternalKey");
+
+ kDialog.getButton("accept").label = await document.l10n.formatValue(
+ "openpgp-save-external-button"
+ );
+ kDialog.getButton("accept").classList.add("primary");
+
+ // If the user is already using an external GnuPG key, populate the input,
+ // show the warning description, and enable the primary button.
+ if (gIdentity.getBoolAttribute("is_gnupg_key_id")) {
+ document.getElementById("externalKey").value =
+ gIdentity.getUnicharAttribute("last_entered_external_gnupg_key_id");
+ document.getElementById("openPgpExternalWarning").collapsed = false;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ } else {
+ document.getElementById("openPgpExternalWarning").collapsed = true;
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ }
+}
+
+/**
+ * Animate the reveal of a section of the wizard.
+ *
+ * @param {string} id - The id of the section to reveal.
+ */
+function revealSection(id) {
+ let section = document.getElementById(id);
+ section.removeAttribute("hidden");
+
+ // Timeout to animate after the hidden attribute has been removed.
+ setTimeout(() => {
+ section.classList.remove("hide", "hide-reverse");
+ });
+
+ resizeDialog();
+}
+
+/**
+ * Enable or disable the elements based on the radiogroup selection.
+ *
+ * @param {Event} event - The DOM event triggered on change.
+ */
+function onExpirationChange(event) {
+ document
+ .getElementById("expireInput")
+ .toggleAttribute("disabled", event.target.value != 0);
+ document.getElementById("timeScale").disabled = event.target.value != 0;
+
+ validateExpiration();
+}
+
+/**
+ * Enable or disable the #keySize input field based on the current selection of
+ * the #keyType radio group.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function onKeyTypeChange(event) {
+ document.getElementById("keySize").disabled = event.target.value == "ECC";
+}
+
+/**
+ * Intercept the cancel event to prevent accidental closing if the generation of
+ * a key is currently in progress.
+ *
+ * @param {Event} event - The DOM event.
+ */
+function onClose(event) {
+ if (kGenerating) {
+ event.preventDefault();
+ }
+
+ window.arguments[0].cancelCallback();
+}
+
+/**
+ * Validate the expiration time of a newly generated key when the user changes
+ * values. Disable the "Generate Key" button and show an alert if the selected
+ * value is less than 1 day or more than 100 years.
+ */
+async function validateExpiration() {
+ // If the key doesn't have an expiration date, hide the warning message and
+ // enable the "Generate Key" button.
+ if (document.getElementById("openPgpKeygeExpiry").value == 1) {
+ document.getElementById("openPgpWarning").collapsed = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ return;
+ }
+
+ // Calculate the selected expiration date.
+ let expiryTime =
+ Number(document.getElementById("expireInput").value) *
+ Number(document.getElementById("timeScale").value);
+
+ // If the expiration date exceeds 100 years.
+ if (expiryTime > 36500) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-long-expiry"
+ );
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ resizeDialog();
+ return;
+ }
+
+ // If the expiration date is shorter than 1 day.
+ if (expiryTime <= 0) {
+ document.getElementById("openPgpWarning").collapsed = false;
+ document.l10n.setAttributes(
+ document.getElementById("openPgpWarningDescription"),
+ "openpgp-keygen-short-expiry"
+ );
+ kDialog.getButton("accept").setAttribute("disabled", true);
+ resizeDialog();
+ return;
+ }
+
+ // If the previous conditions are false, hide the warning message and
+ // enable the "Generate Key" button since the expiration date is valid.
+ document.getElementById("openPgpWarning").collapsed = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+}
+
+/**
+ * Resize the dialog to account for the newly visible sections.
+ */
+function resizeDialog() {
+ // Check if the attribute is not null. This can be removed after the full
+ // conversion of the Key Manager into a SubDialog in Bug 1652537.
+ if (gSubDialog && gSubDialog._topDialog) {
+ gSubDialog._topDialog.resizeVertically();
+ } else {
+ window.sizeToContent();
+ }
+}
+
+/**
+ * Start the generation of a new OpenPGP Key.
+ */
+async function openPgpKeygenStart() {
+ let openPgpWarning = document.getElementById("openPgpWarning");
+ let openPgpWarningText = document.getElementById("openPgpWarningDescription");
+ openPgpWarning.collapsed = true;
+
+ // If a key generation request is already pending, warn the user and
+ // don't proceed.
+ if (gKeygenRequest) {
+ let req = gKeygenRequest.QueryInterface(Ci.nsIRequest);
+
+ if (req.isPending()) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(openPgpWarningText, "openpgp-keygen-ongoing");
+ return;
+ }
+ }
+
+ // Reset global variables to be sure.
+ gGeneratedKey = null;
+ gAllData = "";
+
+ let enigmailSvc = GetEnigmailSvc();
+ if (!enigmailSvc) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-core"
+ );
+ closeOverlay();
+
+ throw new Error("GetEnigmailSvc failed");
+ }
+
+ // Show wizard overlay before the start of the generation process. This is
+ // necessary because the generation happens synchronously and blocks the UI.
+ // We need to show the overlay before it, otherwise it would flash and freeze.
+ // This should be moved after the Services.prompt.confirmEx() method
+ // once Bug 1617444 is implemented.
+ let overlay = document.getElementById("wizardOverlay");
+ overlay.removeAttribute("hidden");
+ overlay.classList.remove("hide");
+
+ // Ask for confirmation before triggering the generation of a new key.
+ document.l10n.setAttributes(
+ document.getElementById("wizardOverlayQuestion"),
+ "openpgp-key-confirm",
+ {
+ identity: `${gIdentity.fullName} <b>"${gIdentity.email}"</b>`,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("wizardOverlayTitle"),
+ "openpgp-keygen-progress-title"
+ );
+}
+
+async function openPgpKeygenConfirm() {
+ document.getElementById("openPgpKeygenConfirm").collapsed = true;
+ document.getElementById("openPgpKeygenProcess").removeAttribute("collapsed");
+
+ let openPgpWarning = document.getElementById("openPgpWarning");
+ let openPgpWarningText = document.getElementById("openPgpWarningDescription");
+ openPgpWarning.collapsed = true;
+
+ kGenerating = true;
+
+ let password;
+ let cApi = EnigmailCryptoAPI();
+ let newId = null;
+
+ let sepPassphraseEnabled = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+
+ if (
+ !sepPassphraseEnabled ||
+ document.getElementById("keygenAutoProtection").selected
+ ) {
+ password = await OpenPGPMasterpass.retrieveOpenPGPPassword();
+ } else {
+ password = document.getElementById("passwordInput").value;
+ }
+ newId = await cApi.genKey(
+ `${gIdentity.fullName} <${gIdentity.email}>`,
+ document.getElementById("keyType").value,
+ Number(document.getElementById("keySize").value),
+ document.getElementById("openPgpKeygeExpiry").value == 1
+ ? 0
+ : Number(document.getElementById("expireInput").value) *
+ Number(document.getElementById("timeScale").value),
+ password
+ );
+
+ gGeneratedKey = newId;
+
+ EnigmailWindows.keyManReloadKeys();
+
+ gKeygenRequest = null;
+ kGenerating = false;
+
+ // For wathever reason, the key wasn't generated. Show an error message and
+ // hide the processing overlay.
+ if (!gGeneratedKey) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-failed"
+ );
+ closeOverlay();
+
+ throw new Error("key generation failed");
+ }
+
+ console.debug("saving new key id " + gGeneratedKey);
+ Services.prefs.savePrefFile(null);
+
+ // Hide wizard overlay at the end of the generation process.
+ closeOverlay();
+ EnigmailKeyRing.clearCache();
+
+ let rev = await cApi.unlockAndGetNewRevocation(
+ `0x${gGeneratedKey}`,
+ password
+ );
+ if (!rev) {
+ openPgpWarning.collapsed = false;
+ document.l10n.setAttributes(
+ openPgpWarningText,
+ "openpgp-keygen-error-revocation",
+ {
+ key: gGeneratedKey,
+ }
+ );
+ closeOverlay();
+
+ throw new Error("failed to obtain revocation for key " + gGeneratedKey);
+ }
+
+ let revFull =
+ revocationFilePrefix1 +
+ "\n\n" +
+ gGeneratedKey +
+ "\n" +
+ revocationFilePrefix2 +
+ rev;
+
+ let revFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ revFile.append(`0x${gGeneratedKey}_rev.asc`);
+
+ // Create a revokation cert in the Thunderbird profile directory.
+ await IOUtils.writeUTF8(revFile.path, revFull);
+
+ // Key successfully created. Close the dialog and show a confirmation message.
+ // Assigning the key to an identity is the responsibility of the caller,
+ // so we pass back what we created.
+ window.arguments[0].okCallback(gGeneratedKey);
+ window.close();
+}
+
+/**
+ * Cancel the keygen process, ask for confirmation before proceeding.
+ */
+async function openPgpKeygenCancel() {
+ let [abortTitle, abortText] = await document.l10n.formatValues([
+ { id: "openpgp-keygen-abort-title" },
+ { id: "openpgp-keygen-abort" },
+ ]);
+
+ if (
+ kGenerating &&
+ Services.prompt.confirmEx(
+ window,
+ abortTitle,
+ abortText,
+ Services.prompt.STD_YES_NO_BUTTONS,
+ "",
+ "",
+ "",
+ "",
+ {}
+ ) != 0
+ ) {
+ return;
+ }
+
+ closeOverlay();
+ gKeygenRequest.kill(false);
+ kGenerating = false;
+}
+
+/**
+ * Close the processing wizard overlay.
+ */
+function closeOverlay() {
+ document.getElementById("openPgpKeygenConfirm").removeAttribute("collapsed");
+ document.getElementById("openPgpKeygenProcess").collapsed = true;
+
+ let overlay = document.getElementById("wizardOverlay");
+
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+}
+
+/**
+ * Add the "hidden" attribute tot he processing wizard overlay after the CSS
+ * transition ended.
+ *
+ * @param {Event} event - The DOM Event.
+ */
+function hideOverlay(event) {
+ event.target.setAttribute("hidden", true);
+ resizeDialog();
+}
+
+async function importSecretKey() {
+ let [importTitle, importType] = await document.l10n.formatValues([
+ { id: "import-key-file" },
+ { id: "gnupg-file" },
+ ]);
+
+ // Reset the array of selected files.
+ gFiles = [];
+
+ let files = EnigmailDialog.filePicker(
+ window,
+ importTitle,
+ "",
+ false,
+ true,
+ "*.asc",
+ "",
+ [importType, "*.asc;*.gpg;*.pgp"]
+ );
+
+ if (!files.length) {
+ return;
+ }
+
+ // Clear and hide the warning notification section.
+ clearImportWarningNotifications();
+
+ // Clear the key list from any previously listed key.
+ let keyList = document.getElementById("importKeyList");
+ while (keyList.lastChild) {
+ keyList.lastChild.remove();
+ }
+
+ let keyCount = 0;
+ for (let file of files) {
+ // Skip the file and show a warning message if larger than 5MB.
+ if (file.fileSize > 5000000) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "import-error-file-size"
+ );
+ continue;
+ }
+
+ let errorMsgObj = {};
+ // Fetch the list of all the available keys inside the selected file.
+ let importKeys = await EnigmailKey.getKeyListFromKeyFile(
+ file,
+ errorMsgObj,
+ false,
+ true
+ );
+
+ // Skip the file and show a warning message if the import failed.
+ if (!importKeys || !importKeys.length || errorMsgObj.value) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "import-error-failed",
+ {
+ error: errorMsgObj.value,
+ }
+ );
+ continue;
+ }
+
+ await appendFetchedKeys(importKeys);
+ keyCount += importKeys.length;
+
+ // Add the current file to the list of valid files to import.
+ gFiles.push(file);
+ }
+
+ // Update the list count recap and show the container.
+ document.l10n.setAttributes(
+ document.getElementById("keyListCount"),
+ "openpgp-import-key-list-amount-2",
+ {
+ count: keyCount,
+ }
+ );
+
+ document.getElementById("importKeyListContainer").collapsed = !keyCount;
+
+ // Hide the intro section and enable the import of keys only if we have valid
+ // keys currently listed.
+ if (keyCount) {
+ document.getElementById("importKeyIntro").hidden = true;
+ kDialog.getButton("accept").removeAttribute("disabled");
+ kDialog.getButton("accept").classList.add("primary");
+ }
+
+ resizeDialog();
+}
+
+/**
+ * Populate the key list in the import dialog with all the valid keys fetched
+ * from a single file.
+ *
+ * @param {string[]} importKeys - The array of keys fetched from a single file.
+ */
+async function appendFetchedKeys(importKeys) {
+ let keyList = document.getElementById("importKeyList");
+
+ // List all the keys fetched from the file.
+ for (let key of importKeys) {
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row", "selected");
+
+ let titleContainer = document.createXULElement("vbox");
+
+ let id = document.createXULElement("label");
+ id.classList.add("openpgp-key-id");
+ id.value = `0x${key.id}`;
+
+ let name = document.createXULElement("label");
+ name.classList.add("openpgp-key-name");
+ name.value = key.name;
+
+ titleContainer.appendChild(id);
+ titleContainer.appendChild(name);
+
+ // Allow users to treat imported keys as "Personal".
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.setAttribute("id", `${key.id}-set-personal`);
+ document.l10n.setAttributes(checkbox, "import-key-personal-checkbox");
+ checkbox.checked = true;
+
+ container.appendChild(titleContainer);
+ container.appendChild(checkbox);
+
+ keyList.appendChild(container);
+ }
+}
+
+async function openPgpImportStart() {
+ if (!gFiles.length) {
+ return;
+ }
+
+ kGenerating = true;
+
+ // Show the overlay.
+ let overlay = document.getElementById("wizardImportOverlay");
+ overlay.removeAttribute("hidden");
+ overlay.classList.remove("hide");
+
+ // Clear and hide the warning notification section.
+ clearImportWarningNotifications();
+
+ // Clear the list of any previously improted keys from the DOM.
+ let keyList = document.getElementById("importKeyListRecap");
+ while (keyList.lastChild) {
+ keyList.lastChild.remove();
+ }
+
+ let keyCount = 0;
+ for (let file of gFiles) {
+ let resultKeys = {};
+ let errorMsgObj = {};
+
+ // keepPassphrases false is the classic behavior.
+ let keepPassphrases = false;
+
+ // If the pref is on, we allow the user to decide what to do.
+ let allowSeparatePassphrases = Services.prefs.getBoolPref(
+ "mail.openpgp.passphrases.enabled"
+ );
+ if (allowSeparatePassphrases) {
+ keepPassphrases = document.getElementById(
+ "openPgpKeygenKeepPassphrases"
+ ).checked;
+ }
+
+ let exitCode = await EnigmailKeyRing.importSecKeyFromFile(
+ window,
+ passphrasePromptCallback,
+ keepPassphrases,
+ file,
+ errorMsgObj,
+ resultKeys
+ );
+
+ // Skip this file if something went wrong.
+ if (exitCode !== 0) {
+ document.l10n.setAttributes(
+ await addImportWarningNotification(),
+ "openpgp-import-keys-failed",
+ {
+ error: errorMsgObj.value,
+ }
+ );
+ continue;
+ }
+
+ await appendImportedKeys(resultKeys);
+ keyCount += resultKeys.keys.length;
+ }
+
+ // Hide the previous key list container and title.
+ document.getElementById("importKeyListContainer").collapsed = keyCount;
+ document.getElementById("importKeyTitle").hidden = keyCount;
+
+ // Show the successful final screen only if at least one key was imported.
+ if (keyCount) {
+ // Update the dialog buttons for the final stage.
+ kDialog.getButton("extra1").hidden = true;
+ kDialog.getButton("cancel").hidden = true;
+
+ // Update the `Continue` button.
+ document.l10n.setAttributes(
+ kDialog.getButton("accept"),
+ "openpgp-keygen-import-complete"
+ );
+ kCurrentSection = "importComplete";
+
+ // Show the recently built key list.
+ document.getElementById("importKeyListSuccess").collapsed = false;
+ }
+
+ // Hide the loading overlay.
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+
+ resizeDialog();
+ kGenerating = false;
+}
+
+/**
+ * Populate the key list in the import dialog with all the valid keys imported
+ * from a single file.
+ *
+ * @param {string[]} resultKeys - The array of keys imported from a single file.
+ */
+async function appendImportedKeys(resultKeys) {
+ let keyList = document.getElementById("importKeyListRecap");
+
+ for (let keyId of resultKeys.keys) {
+ if (keyId.search(/^0x/) === 0) {
+ keyId = keyId.substr(2).toUpperCase();
+ }
+
+ let key = EnigmailKeyRing.getKeyById(keyId);
+
+ if (key && key.fpr) {
+ // If the checkbox was checked, update the acceptance of the key.
+ if (document.getElementById(`${key.keyId}-set-personal`).checked) {
+ PgpSqliteDb2.acceptAsPersonalKey(key.fpr);
+ }
+
+ let container = document.createXULElement("hbox");
+ container.classList.add("key-import-row");
+
+ // Start key info section.
+ let grid = document.createXULElement("hbox");
+ grid.classList.add("extra-information-label");
+
+ // Key identity.
+ let identityLabel = document.createXULElement("label");
+ identityLabel.classList.add("extra-information-label-type");
+ document.l10n.setAttributes(
+ identityLabel,
+ "openpgp-import-identity-label"
+ );
+
+ let identityValue = document.createXULElement("label");
+ identityValue.value = key.userId;
+
+ grid.appendChild(identityLabel);
+ grid.appendChild(identityValue);
+
+ // Key fingerprint.
+ let fingerprintLabel = document.createXULElement("label");
+ document.l10n.setAttributes(
+ fingerprintLabel,
+ "openpgp-import-fingerprint-label"
+ );
+ fingerprintLabel.classList.add("extra-information-label-type");
+
+ let fingerprintInput = document.createXULElement("label");
+ fingerprintInput.value = EnigmailKey.formatFpr(key.fpr);
+
+ grid.appendChild(fingerprintLabel);
+ grid.appendChild(fingerprintInput);
+
+ // Key creation date.
+ let createdLabel = document.createXULElement("label");
+ document.l10n.setAttributes(createdLabel, "openpgp-import-created-label");
+ createdLabel.classList.add("extra-information-label-type");
+
+ let createdValue = document.createXULElement("label");
+ createdValue.value = key.created;
+
+ grid.appendChild(createdLabel);
+ grid.appendChild(createdValue);
+
+ // Key bits.
+ let bitsLabel = document.createXULElement("label");
+ bitsLabel.classList.add("extra-information-label-type");
+ document.l10n.setAttributes(bitsLabel, "openpgp-import-bits-label");
+
+ let bitsValue = document.createXULElement("label");
+ bitsValue.value = key.keySize;
+
+ grid.appendChild(bitsLabel);
+ grid.appendChild(bitsValue);
+ // End key info section.
+
+ let info = document.createXULElement("button");
+ info.classList.add("openpgp-image-btn", "openpgp-props-btn");
+ document.l10n.setAttributes(info, "openpgp-import-key-props");
+ info.addEventListener("command", () => {
+ window.arguments[0].keyDetailsDialog(key.keyId);
+ });
+
+ container.appendChild(grid);
+ container.appendChild(info);
+
+ keyList.appendChild(container);
+ }
+ }
+}
+
+function openPgpImportComplete() {
+ window.arguments[0].okImportCallback();
+ window.close();
+}
+
+/**
+ * Opens a prompt asking the user to enter the passphrase for a given key id.
+ *
+ * @param {object} win - The current window.
+ * @param {string} keyId - The ID of the imported key.
+ * @param {object} resultFlags - Keep track of the cancelled action.
+ *
+ * @returns {string} - The entered passphrase or empty.
+ */
+function passphrasePromptCallback(win, promptString, resultFlags) {
+ let passphrase = { value: "" };
+
+ // We need to fetch these strings synchronously in order to properly work with
+ // the RNP key import method, which is not async.
+ let title = syncl10n.formatValueSync("openpgp-passphrase-prompt-title");
+
+ let prompt = Services.prompt.promptPassword(
+ win,
+ title,
+ promptString,
+ passphrase,
+ null,
+ {}
+ );
+
+ if (!prompt) {
+ let overlay = document.getElementById("wizardImportOverlay");
+ overlay.addEventListener("transitionend", hideOverlay, { once: true });
+ overlay.classList.add("hide");
+ kGenerating = false;
+ }
+
+ resultFlags.canceled = !prompt;
+ return !prompt ? "" : passphrase.value;
+}
+
+function toggleSaveButton(event) {
+ kDialog
+ .getButton("accept")
+ .toggleAttribute("disabled", !event.target.value.trim());
+}
+
+/**
+ * Save the GnuPG Key for the current identity and trigger a callback.
+ */
+function openPgpExternalComplete() {
+ gIdentity.setBoolAttribute("is_gnupg_key_id", true);
+
+ let externalKey = document.getElementById("externalKey").value;
+ gIdentity.setUnicharAttribute("openpgp_key_id", externalKey);
+
+ window.arguments[0].okExternalCallback(externalKey);
+ window.close();
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml b/comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml
new file mode 100644
index 0000000000..c630eacb48
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml
@@ -0,0 +1,506 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/keyWizard.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="init();"
+ lightweightthemes="true"
+ style="min-width: 50em"
+>
+ <dialog
+ id="openPgpKeyWizardDialog"
+ data-l10n-id="key-wizard-dialog"
+ data-l10n-attrs="buttonlabelaccept,buttonlabelextra1"
+ buttons="accept,cancel"
+ >
+ <script src="chrome://openpgp/content/ui/enigmailCommon.js" />
+ <script src="chrome://openpgp/content/ui/keyWizard.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="messenger/openpgp/keyWizard.ftl" />
+ </linkset>
+
+ <html:div
+ id="wizardOverlay"
+ class="wizard-section overlay hide"
+ hidden="hidden"
+ >
+ <hbox class="inline-notification-container info-container">
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <description data-l10n-id="openpgp-generate-key-info" />
+ </hbox>
+ </hbox>
+
+ <vbox id="openPgpKeygenConfirm" class="self-center" align="center">
+ <description id="wizardOverlayQuestion" />
+ <separator class="thin" />
+ <hbox>
+ <button
+ data-l10n-id="openpgp-keygen-dismiss"
+ oncommand="closeOverlay();"
+ />
+ <button
+ id="openPgpKeygenConfirmButton"
+ data-l10n-id="openpgp-keygen-confirm"
+ oncommand="openPgpKeygenConfirm();"
+ />
+ </hbox>
+ </vbox>
+
+ <vbox
+ id="openPgpKeygenProcess"
+ class="self-center"
+ align="center"
+ collapsed="true"
+ >
+ <html:legend id="wizardOverlayTitle"></html:legend>
+ <html:img
+ class="loading-status"
+ src="chrome://global/skin/icons/loading.png"
+ alt=""
+ />
+ <button
+ data-l10n-id="openpgp-keygen-cancel"
+ class="self-center"
+ oncommand="openPgpKeygenCancel();"
+ />
+ </vbox>
+ </html:div>
+
+ <html:div
+ id="wizardImportOverlay"
+ class="wizard-section overlay hide"
+ hidden="hidden"
+ >
+ <vbox id="importLoading" class="self-center" align="center">
+ <html:legend
+ data-l10n-id="openpgp-keygen-import-progress-title"
+ ></html:legend>
+ <html:img
+ class="loading-status"
+ src="chrome://global/skin/icons/loading.png"
+ alt=""
+ />
+ </vbox>
+ </html:div>
+
+ <vbox id="wizardStart" class="wizard-section">
+ <hbox class="inline-notification-container info-container">
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <description>
+ <html:span
+ class="tail-with-learn-more"
+ data-l10n-id="key-wizard-warning"
+ >
+ </html:span>
+ <label
+ is="text-link"
+ href="https://support.mozilla.org/kb/introduction-to-e2e-encryption"
+ data-l10n-id="key-wizard-learn-more"
+ class="learnMore text-link"
+ />
+ </description>
+ </hbox>
+ </hbox>
+
+ <html:div>
+ <html:fieldset>
+ <radiogroup id="openPgpKeyChoices" class="indent">
+ <radio
+ id="createOpenPgp"
+ value="0"
+ data-l10n-id="radio-create-key"
+ />
+ <radio
+ id="importOpenPgp"
+ value="1"
+ data-l10n-id="radio-import-key"
+ />
+ <radio
+ id="externalOpenPgp"
+ value="2"
+ data-l10n-id="radio-gnupg-key"
+ hidden="true"
+ />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+
+ <vbox
+ id="wizardCreateKey"
+ class="wizard-section hide-reverse"
+ hidden="true"
+ >
+ <label
+ data-l10n-id="openpgp-generate-key-title"
+ class="dialogheader-title"
+ />
+
+ <html:div>
+ <html:fieldset>
+ <hbox align="center">
+ <html:legend
+ class="identity-legend"
+ data-l10n-id="openpgp-import-identity-label"
+ >
+ </html:legend>
+ <menulist id="userIdentity" flex="1" oncommand="setIdentity();">
+ <menupopup id="userIdentityPopup" />
+ </menulist>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div id="keygenPassphraseSection">
+ <html:fieldset>
+ <html:legend
+ data-l10n-id="openpgp-keygen-secret-protection"
+ ></html:legend>
+
+ <radiogroup id="openPgpKeyProtection" class="indent">
+ <radio
+ id="keygenAutoProtection"
+ value="0"
+ oncommand="onProtectionChange();"
+ />
+ <vbox>
+ <hbox>
+ <radio
+ id="keygenPassphraseProtection"
+ value="1"
+ data-l10n-id="radio-keygen-passphrase-protection"
+ oncommand="onProtectionChange();"
+ />
+ <html:input
+ id="passwordInput"
+ type="password"
+ oninput="onProtectionChange();"
+ />
+ </hbox>
+ <hbox class="indent">
+ <label data-l10n-id="openpgp-passphrase-repeat" />
+ <html:input
+ id="passwordConfirm"
+ type="password"
+ class="input-inline"
+ oninput="onProtectionChange();"
+ />
+ </hbox>
+ </vbox>
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="openpgp-keygen-expiry-title"></html:legend>
+ <description data-l10n-id="openpgp-keygen-expiry-description" />
+
+ <radiogroup id="openPgpKeygeExpiry" class="indent">
+ <hbox flex="1" align="center">
+ <radio
+ id="keygenExpiration"
+ value="0"
+ data-l10n-id="radio-keygen-expiry"
+ oncommand="onExpirationChange(event);"
+ />
+ <html:input
+ id="expireInput"
+ type="number"
+ class="size4 input-inline autosync"
+ maxlength="5"
+ value="3"
+ min="1"
+ max="100"
+ aria-labelledby="keygenExpiration"
+ oninput="validateExpiration();"
+ />
+ <menulist id="timeScale">
+ <menupopup>
+ <menuitem
+ id="years"
+ value="365"
+ data-l10n-id="openpgp-keygen-years-label"
+ selected="true"
+ oncommand="validateExpiration();"
+ />
+ <menuitem
+ id="months"
+ value="30"
+ data-l10n-id="openpgp-keygen-months-label"
+ oncommand="validateExpiration();"
+ />
+ <menuitem
+ id="days"
+ value="1"
+ data-l10n-id="openpgp-keygen-days-label"
+ oncommand="validateExpiration();"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <radio
+ id="keygenDoesNotExpire"
+ value="1"
+ data-l10n-id="radio-keygen-no-expiry"
+ oncommand="onExpirationChange(event);"
+ />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend
+ data-l10n-id="openpgp-keygen-advanced-title"
+ ></html:legend>
+ <description data-l10n-id="openpgp-keygen-advanced-description" />
+
+ <vbox class="indent grid-size">
+ <hbox align="center">
+ <label for="keyType" data-l10n-id="openpgp-keygen-keytype" />
+ </hbox>
+ <hbox align="center">
+ <menulist id="keyType">
+ <menupopup>
+ <menuitem
+ id="keyType_rsa"
+ value="RSA"
+ data-l10n-id="openpgp-keygen-type-rsa"
+ selected="true"
+ oncommand="onKeyTypeChange(event);"
+ />
+ <menuitem
+ id="keyType_ecc"
+ value="ECC"
+ data-l10n-id="openpgp-keygen-type-ecc"
+ oncommand="onKeyTypeChange(event);"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <spacer />
+
+ <hbox align="center">
+ <label for="keySize" data-l10n-id="openpgp-keygen-keysize" />
+ </hbox>
+ <hbox align="center">
+ <menulist id="keySize">
+ <menupopup>
+ <menuitem
+ id="keySize_3072"
+ value="3072"
+ label="3072"
+ selected="true"
+ />
+ <menuitem id="keySize_4096" value="4096" label="4096" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <spacer />
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator />
+
+ <hbox
+ id="openPgpWarning"
+ class="inline-notification-container error-container"
+ collapsed="true"
+ >
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://global/skin/icons/warning.svg"
+ alt=""
+ />
+ <description id="openPgpWarningDescription" />
+ </hbox>
+ </hbox>
+ </vbox>
+
+ <vbox
+ id="wizardImportKey"
+ class="wizard-section hide-reverse"
+ hidden="true"
+ >
+ <label
+ id="importKeyTitle"
+ data-l10n-id="openpgp-import-key-title"
+ class="dialogheader-title"
+ />
+
+ <vbox id="openPgpImportWarning" hidden="true" />
+
+ <vbox id="importKeyIntro" align="start">
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="openpgp-import-key-legend"></html:legend>
+ <description data-l10n-id="openpgp-import-key-description" />
+ <description
+ data-l10n-id="openpgp-import-key-info"
+ class="tip-caption"
+ />
+
+ <separator />
+
+ <button
+ data-l10n-id="openpgp-import-key-button"
+ oncommand="importSecretKey();"
+ />
+
+ <separator class="thin" />
+ </html:fieldset>
+ </html:div>
+ </vbox>
+
+ <vbox id="importKeyListContainer" collapsed="true">
+ <hbox class="inline-notification-container success-container">
+ <hbox class="inline-notification-wrapper align-center">
+ <html:img
+ class="notification-image"
+ src="chrome://global/skin/icons/check.svg"
+ alt=""
+ />
+ <description id="keyListCount" />
+ </hbox>
+ </hbox>
+
+ <description data-l10n-id="openpgp-import-key-list-description" />
+
+ <vbox id="importKeyList" />
+
+ <description
+ data-l10n-id="openpgp-import-key-list-caption"
+ class="tip-caption"
+ />
+
+ <separator class="thin" />
+ <checkbox
+ id="openPgpKeygenKeepPassphrases"
+ data-l10n-id="openpgp-import-keep-passphrases"
+ />
+
+ <separator />
+ </vbox>
+
+ <vbox id="importKeyListSuccess" collapsed="true">
+ <hbox class="inline-notification-container success-container">
+ <hbox class="inline-notification-wrapper align-center">
+ <html:img
+ class="notification-image"
+ src="chrome://global/skin/icons/check.svg"
+ alt=""
+ />
+ <description data-l10n-id="openpgp-import-success" />
+ </hbox>
+ </hbox>
+
+ <separator />
+
+ <vbox id="importKeyListRecap" />
+
+ <vbox align="center">
+ <html:legend
+ data-l10n-id="openpgp-import-success-title"
+ ></html:legend>
+ <description
+ data-l10n-id="openpgp-import-success-description"
+ class="description-centered"
+ />
+ </vbox>
+
+ <separator class="thin" />
+ </vbox>
+ </vbox>
+
+ <vbox
+ id="wizardExternalKey"
+ class="wizard-section hide-reverse"
+ hidden="true"
+ >
+ <label
+ data-l10n-id="openpgp-external-key-title"
+ class="dialogheader-title"
+ />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="openpgp-external-key-description">
+ </html:legend>
+
+ <description data-l10n-id="openpgp-external-key-info" />
+
+ <separator />
+
+ <hbox align="center">
+ <label
+ for="externalKey"
+ data-l10n-id="openpgp-external-key-label"
+ ></label>
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="externalKey"
+ type="text"
+ class="input-inline"
+ data-l10n-id="openpgp-external-key-input"
+ oninput="toggleSaveButton(event);"
+ />
+ </hbox>
+ </hbox>
+
+ <separator />
+
+ <hbox
+ id="openPgpExternalWarning"
+ class="inline-notification-container info-container"
+ collapsed="true"
+ >
+ <hbox class="inline-notification-wrapper">
+ <html:img
+ class="notification-image"
+ src="chrome://messenger/skin/icons/information.svg"
+ alt=""
+ />
+ <description data-l10n-id="openpgp-external-key-warning" />
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js
new file mode 100644
index 0000000000..e1d369e1ab
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js
@@ -0,0 +1,177 @@
+/* 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/. */
+
+var { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+var EnigmailKeyRing = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+).EnigmailKeyRing;
+var { EnigmailWindows } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/windows.jsm"
+);
+var { EnigmailDialog } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/dialog.jsm"
+);
+var { EnigmailKey } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/key.jsm"
+);
+var KeyLookupHelper = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyLookupHelper.jsm"
+).KeyLookupHelper;
+const { PgpSqliteDb2 } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/sqliteDb.jsm"
+);
+
+var gListBox;
+var gViewButton;
+
+var gAddr;
+var gRowToKey = [];
+
+async function setListEntries(keys = null) {
+ let index = 0;
+
+ // Temporary code for debugging/development, should be removed when
+ // a final patch for bug 1627956 lands.
+ console.log(await EnigmailKeyRing.getEncryptionKeyMeta(gAddr));
+
+ if (!keys) {
+ keys = await EnigmailKeyRing.getMultValidKeysForOneRecipient(gAddr, true);
+ }
+
+ for (let keyObj of keys) {
+ let listitem = document.createXULElement("richlistitem");
+
+ let keyId = document.createXULElement("label");
+ keyId.setAttribute("value", "0x" + keyObj.keyId);
+ keyId.setAttribute("crop", "end");
+ keyId.setAttribute("style", "width: var(--keyWidth)");
+ listitem.appendChild(keyId);
+
+ let acceptanceText;
+
+ // Further above, we called getMultValidKeysForOneRecipient
+ // and asked to ignore if a key is expired.
+ // If the following check fails, the key must be expired.
+ if (!EnigmailKeyRing.isValidForEncryption(keyObj)) {
+ acceptanceText = "openpgp-key-expired";
+ } else if (keyObj.secretAvailable) {
+ if (await PgpSqliteDb2.isAcceptedAsPersonalKey(keyObj.fpr)) {
+ acceptanceText = "openpgp-key-own";
+ } else {
+ acceptanceText = "openpgp-key-secret-not-personal";
+ }
+ } else {
+ if (!("acceptance" in keyObj)) {
+ throw new Error(
+ "expected getMultValidKeysForOneRecipient to set acceptance"
+ );
+ }
+ switch (keyObj.acceptance) {
+ case "rejected":
+ acceptanceText = "openpgp-key-rejected";
+ break;
+ case "unverified":
+ acceptanceText = "openpgp-key-unverified";
+ break;
+ case "verified":
+ acceptanceText = "openpgp-key-verified";
+ break;
+ case "undecided":
+ acceptanceText = "openpgp-key-undecided";
+ break;
+ default:
+ throw new Error("unexpected acceptance value: " + keyObj.acceptance);
+ }
+ }
+
+ let status = document.createXULElement("label");
+ document.l10n.setAttributes(status, acceptanceText);
+ status.setAttribute("crop", "end");
+ status.setAttribute("style", "width: var(--statusWidth)");
+ listitem.appendChild(status);
+
+ let issued = document.createXULElement("label");
+ issued.setAttribute("value", keyObj.created);
+ issued.setAttribute("crop", "end");
+ issued.setAttribute("style", "width: var(--issuedWidth)");
+ listitem.appendChild(issued);
+
+ let expire = document.createXULElement("label");
+ expire.setAttribute("value", keyObj.expiry);
+ expire.setAttribute("crop", "end");
+ expire.setAttribute("style", "width: var(--expireWidth)");
+ listitem.appendChild(expire);
+
+ gListBox.appendChild(listitem);
+
+ gRowToKey[index] = keyObj.keyId;
+ index++;
+ }
+}
+
+async function onLoad() {
+ let params = window.arguments[0];
+ if (!params) {
+ return;
+ }
+
+ gListBox = document.getElementById("infolist");
+ gViewButton = document.getElementById("detailsButton");
+
+ gAddr = params.email;
+
+ document.l10n.setAttributes(
+ document.getElementById("intro"),
+ "openpgp-intro",
+ { key: gAddr }
+ );
+
+ await setListEntries(params.keys);
+}
+
+async function reloadAndSelect(selIndex = -1) {
+ while (true) {
+ let child = gListBox.lastChild;
+ // keep first child, which is the header
+ if (child == gListBox.firstChild) {
+ break;
+ }
+ gListBox.removeChild(child);
+ }
+ gRowToKey = [];
+ await setListEntries();
+ gListBox.selectedIndex = selIndex;
+}
+
+function onSelectionChange(event) {
+ let haveSelection = gListBox.selectedItems.length;
+ gViewButton.disabled = !haveSelection;
+}
+
+function viewSelectedKey() {
+ let selIndex = gListBox.selectedIndex;
+ if (gViewButton.disabled || selIndex == -1) {
+ return;
+ }
+ EnigmailWindows.openKeyDetails(window, gRowToKey[selIndex], false);
+ reloadAndSelect(selIndex);
+}
+
+async function discoverKey() {
+ let keyIds = gRowToKey;
+ let foundNewData = await KeyLookupHelper.fullOnlineDiscovery(
+ "interactive-import",
+ window,
+ gAddr,
+ keyIds
+ );
+ if (foundNewData) {
+ reloadAndSelect();
+ } else {
+ let value = await document.l10n.formatValue("no-key-found2");
+ EnigmailDialog.alert(window, value);
+ }
+}
diff --git a/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml
new file mode 100644
index 0000000000..5875224d4c
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/openPgpComposeStatus.css" type="text/css"?>
+
+<window
+ data-l10n-id="openpgp-one-recipient-status-title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="width: 50em; height: 22em"
+ persist="width height"
+ onload="onLoad();"
+>
+ <dialog id="oneRecipientStatus" buttons="accept">
+ <script src="chrome://openpgp/content/ui/oneRecipientStatus.js" />
+ <linkset>
+ <html:link
+ rel="localization"
+ href="messenger/openpgp/oneRecipientStatus.ftl"
+ />
+ <html:link rel="localization" href="messenger/openpgp/openpgp.ftl" />
+ </linkset>
+ <script>
+ <![CDATA[
+ function resizeColumns() {
+ let list = document.getElementById("infolist");
+ let cols = list.getElementsByTagName("treecol");
+ list.style.setProperty("--keyWidth", cols[0].getBoundingClientRect().width + "px");
+ list.style.setProperty("--statusWidth", cols[1].getBoundingClientRect().width + "px");
+ list.style.setProperty("--issuedWidth", cols[2].getBoundingClientRect().width + "px");
+ list.style.setProperty("--expireWidth", cols[3].getBoundingClientRect().width - 5 + "px");
+ }
+ addEventListener("load", resizeColumns, { once: true });
+ addEventListener("resize", resizeColumns);
+ ]]>
+ </script>
+
+ <description data-l10n-id="openpgp-one-recipient-status-instruction1" />
+ <separator class="thin" />
+ <description data-l10n-id="openpgp-one-recipient-status-instruction2" />
+ <separator class="thin" />
+ <label id="intro" control="infolist" />
+
+ <richlistbox
+ id="infolist"
+ class="theme-listbox"
+ flex="1"
+ onselect="onSelectionChange(event);"
+ >
+ <treecols>
+ <treecol
+ id="recipientKeyIdCol"
+ data-l10n-id="openpgp-one-recipient-status-key-id"
+ />
+ <treecol
+ id="recipientStatusCol"
+ data-l10n-id="openpgp-one-recipient-status-status"
+ />
+ <treecol
+ style="flex: 1 auto"
+ data-l10n-id="openpgp-one-recipient-status-created-date"
+ />
+ <treecol
+ style="flex: 1 auto"
+ data-l10n-id="openpgp-one-recipient-status-expires-date"
+ />
+ </treecols>
+ </richlistbox>
+ <hbox pack="start">
+ <button
+ id="detailsButton"
+ disabled="true"
+ data-l10n-id="openpgp-one-recipient-status-open-details"
+ oncommand="viewSelectedKey();"
+ />
+ <button
+ id="discoverButton"
+ data-l10n-id="openpgp-one-recipient-status-discover"
+ oncommand="discoverKey();"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/comm/mail/extensions/openpgp/jar.mn b/comm/mail/extensions/openpgp/jar.mn
new file mode 100644
index 0000000000..fbcff37062
--- /dev/null
+++ b/comm/mail/extensions/openpgp/jar.mn
@@ -0,0 +1,14 @@
+#filter substitution
+# 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/.
+
+openpgp.jar:
+% content openpgp %content/openpgp/
+ content/openpgp/BondOpenPGP.jsm (content/BondOpenPGP.jsm)
+ content/openpgp/modules (content/modules/*.js*)
+ content/openpgp/modules/stdlib (content/modules/stdlib/*.js*)
+ content/openpgp/modules/cryptoAPI (content/modules/cryptoAPI/*.js*)
+ content/openpgp/strings/enigmail.properties (content/strings/enigmail.properties)
+ content/openpgp/ui (content/ui/*.js)
+ content/openpgp/ui (content/ui/*.xhtml)
diff --git a/comm/mail/extensions/openpgp/moz.build b/comm/mail/extensions/openpgp/moz.build
new file mode 100644
index 0000000000..64429f4bf4
--- /dev/null
+++ b/comm/mail/extensions/openpgp/moz.build
@@ -0,0 +1,7 @@
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/rnp/xpcshell.ini"]
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg b/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg
new file mode 100644
index 0000000000..6bc9b4f3af
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg
Binary files differ
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.asc b/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.asc
new file mode 100644
index 0000000000..c67d0dd057
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.asc
@@ -0,0 +1,200 @@
+-----BEGIN PGP MESSAGE-----
+
+hQGMA3wvqk35PDeyAQv/fskZLTo2KonOaZoFSjMcDDtnbI94Ra6urjupaltpoblj
+suA8uQJaqeud54Es8ViCyMTdAcx5S3N0DdcyNVhbY8CMzNxvr65Yeti7vSydX+lE
+jo8bV4ahlGuHRm+57d4LTcgHclhHuEbQ/mg33k7kcRh+GUB0qX/j//ZpumBp/dVP
+a4d7G4BH3IaUBHkMR7zxYN2GytgcHcjHYaihjDejWWvTi/fEmUn8wUBF4kBA4ZBk
+P5VdkMEG5qFPoULbS6x+J+9DEOeoVxCRBupeR/wIB9XaxdjYuzjVnguIAAFhyvj4
+kmODe2mwz788fGeBU8pAXBnacBU2tuCtSQMT1v2hvY5l2R7PBnQRP8Og5lGRQGTN
+IMluCVfKdhAJugVUlBk0fqgmdxnggJtbm/9MdgALkG35cRTiQFA2Tbg/+fzDNj8j
+l1F6WiHe8bK4439rycE3RMpbcPChxMXSxg+tYyxVcbKyYVaV1p2QjBdI5GtLGy7y
+IXwFXIi2+f1G01JiJy1Q0u0BqEc1cUdlXPJJHmQCzx9P2ea87uEiSuUBQ9rvPNEM
+GraVb2AMIGtztVh7DEWdn+QpepGLB2c7wx1ZVT7uSIJzDt0rxMWEPwiUQR5Qd17p
+z4UAw7OsRl44QsXBiqAZj4T4i2Tj34c7uBGRgRCRV+J8qh/9MzFmL/4ZBQw6UhyK
+7GCrkwcwsC1GO7HTOZyZV1i6iVhFykDpDS6iDXypkgCCDMZC16fA4GgD1dhE/vR1
+q4O3YH250sfmRqMfN7M6NTTETK0h78rwuwng6Z7ah9QMX9WESJgfAG6ZvlrhmbCu
+K1kg1hJKvPCcxHRqAs+hkUcN42CNhLiMZTrQnHHyGp5BHq63IwU8qRWOeWVzdGGe
+JsdhGrS9LdWsk/Wh/5zoqj4LREqILgLQM3aMoFdk86T+Td7sZGCO7BIrjif+xktz
+PWEJAcl30mI8NXe9YYtpvm1riXqf52q45kyjjq/2WJA3T7X5CVr1iC60JxrVD9Rv
+TKsRdU6eVVmlECPNzgz8Ft1RDo+tbmtByOIkck9pnlbL9ZGJeTynjVRdHdogs9/i
+PbukExDgkQuW05E+NsAmj2N4/ery4uneRiWoe3VlKczXLpCyMMV6GlYHlIECcxkW
+OJ+nnaz2oT6AB4ooTbZADG9PxQN4sVPhLuCtksVoH1DZjOL63oPpDW047U7yyyFI
+t6JccClKIjgTsDyiLB38qjhdKcrvsd9gsuueowsB67zVCxDylG8IcrY+SHWXjEK5
+m23xwIEyGcmyotawK0GYuWH2a8ZxqoJLCTyCmxrFGjd7Twc89AVqaZ+BGZ3P8KgJ
+ztMg+oB9t3MugiAepMFTHRdOIO5FWUraNrJIq1fwT4ZzLpCLygY9xrCvjekm4sHy
+uKzvkFvjft9C6RKeEWd0PjV8FU1cnPlCDtidPEIrro8dQsgh6hyTuVOZWDwUh4k2
+kPErWW08UM4luFU60VgAeWNW3Z0Ue9ETZwbgl9MWpsxlJxC24E13Jsugj/xOw0By
+BwYFSH/Uy3+vggqKjdDC0mn0TSKD1ay+vTBfw1DUbE4FxmPoZQ5KjKX1949E8P29
+ejdTf9k56pKA5vcResOlHJqo16oBT7g4SRmK/vTXL2r+STS3egCdz/TAvGV7SY7s
+x2aifs6sMoe8dO6OgJUbxFUUgrzWqud/nlmZI2R7BnDdG37/05++7ETxxCdLT2Jb
+uzA9azO942UmcLttGoKsar7dI71Z5cgiwdckk+z4zkJ2W/JRGoWui09g1NzDbGSu
+rsT8SQ3wq3ExSABWYMywbaXj5KmKnLMzJfRmyDY6Nc2bN4KQbP86eLBi6tjNPJSZ
+bf+qqPgMIXsK0deH0KZlt0Gw2ltryuYB2KEc0RIUOKzA0cIGxpC9m6OTMn23+4sE
+e69X5Z+cd+ObrG0WxqUz5w7aE+Q/z0aqccL1yQdzN3VHBo/mmCErGDcfGNPbEBOR
+nJVv8ov6ueBaYiKtb2N1Rw6cTjT1kPbjFRnP9wxxu5FsVkn51jAOfdLlbGZPS9tR
+csvv/6QEdFcV1HAw1YJPsraCDFaK9rKjR38lzxb+kHJQV18HeDjiG6FIH7/0oOD0
+5vdEMI8hebMI8y4y/ly5xJoVJ8IDQURXLk+/uRlN0J7C2AX9egzJDNNrLZGJAcnx
+ITBLhhfj+pB8EsEYWAOsJ33Y8Q4y+RWoNZSly7bphvWQP30dYLa0R3U4xRwlzfLe
+0o8EbzZHTYWtWoGx5FmjopSvPqT4i4OzCWQZlCgedIMRZYITBOzqjj7g2xjKLeeE
+2qho8ra+X+OsuQhp24sMjuu9SfUUQUXoNxu8qCMfM8wjjeLKyG5/1ncMPV3z4t1/
+GBi/Gxd1eQ/whh6rzEyFrWdm0+zPTONAHJOLrO0iS3szK/EFZMQK1IcgUzFfQoKy
+wANnuc8mIsQH9IuglmqUak+bc2J1wmNFAwi3KE/cw+hhqCzpObf1J7OUISRRrx15
+mL5Z/kUGgTERWcEPaO9LU6h9eSx5k0oQxatrrOVen29LPRRxfPJQf/1PqQX8YAy+
+eP4de1RXbKp1XsyZSPyJ1qciPHopv1I1uwn0eBt52VWqX9kGtyYWK1uXGYJdSVB5
+wihOxZDYtBnq872lKS0CQjZb7DyCkMuTUeBEQNz2PK0d8pKA22wGBFyaRd9EIQye
+JtVMWLUBKFRu3bJffmDovwBeGw+CRzYv0qiRr10g2it9M1uMUm7XGBzFqXVMPkGA
+wE2rD5DM/gKt4OaE/ccGtpjdoH5eKYD+4UzEHYzHbyQh0JXbjdPPR5KNdh/oxpAm
+JLwUdPiG6+hcCab3FL4VBWFdJGQAez3okmI8CzgaxnDBgeYDDtN2IwCqJ6VHjv2w
+5aAOCbt8xxgcrDNlF4V6TsUSXTRYgQdxArwGiy54HxKpF2TTM24csLO1alWcdEfX
+mV2fcRo5GrnMeCKboVBoMIcY4x5ClAaoq8LuCRYDH3qT4/FDOgp2O4EqhzvwR/7I
+UQRHY0g/m92WuRb9rVhX+Z9a+ljW2AmxpNuyrwTQ0+KPykJMHQNy+LsKrG/7I9ro
+ZL7V1OyXuKbjE+8MI3PGOQWqH+dL+/hK24cMn3Tz0sIZmtQcf/KuJfe7TMERGB+d
+GugC9xLv0gfUuMcTSN34autdHC8pTDUufJOvDwZfKQzbFYcOXmC8cTFmw84Z4lLh
+Odq7UmaS6/UhE9D9nY+4XDUsKK7CRE6B1RqI/tFKYSO130vHklmCVKkLtrwYpGay
+JY/Nwo3Cw8NUqQOe8DCrhjKgAkE6QTMpf7cmqIwpif6J58kM282ZKnV/EVIaP0mv
+khd2FnJqHGKRrC76i24AtyuH+1jMY4jt0XpLhSE5oPcrtTsyadhcw/YIHMTXbYLA
+b3V+n/yYZkMlhTbKQmks4kVQTCJMOHce2ES69WuDeFkaHgo7uJUp2gkl3TuUQ184
++/Fi/6EhhlRff1O7AHXNMYp2FNJ5O/fbCbMuo4GVVqjBIG5nS2QfmfNUvtSsQQIg
+bpdP6wpkbcN24WHOYieNfiQHlvb85j2yXdaSRX6pv+biTw1JoEzWg4zoWRzWTf2O
+x7l9JD4vnhflxNrlvg1AoCzSWWo1qDVEO73lkwmDGU/Y/T5twzDAEkZfDCVWiSPe
+Ngz+oNsLGNSYC1HxuybVCCqX5ahLusRkX1NaAB2dlefsjnA9UJmKEtVUacDBR0Su
+tVie4FgRTBU5yWX2cMEmCm17+VLINhfBYpMRw0dOxZgH8uKQz6DddVm/5/O6lYYN
+GLtY9ZSbVV3cbBx3bGV6RqOVGuvPKFsZo6xwu3jo6Ij+O/aGaFiAwfFGzYZs+LOU
+BCE4ShXQDOxYUdVuM1f8R+ZabtiVogOE244nWwhqaR0INCxr1T5VsIyO2yWpDUld
+tQR1gCkzMEITIs7ORiUwFEq8MwzlwbI67V4s341HKgoQG4VG20zRfdNZrCElFiJq
+Q1WhIJJFzRZldVTdloQ/kDAHEEDu7ypVdkgmX4P3ak8Cp+FZjxJKYo+We1laioPj
+cn+YHq0aRG8sFJGAyMem4kW6m9TzZ0Nis48hFJdpjpcIXi+HmmmRlB8O30icSeOF
+D+4D5o/J0S5hdnLTL4jK7EfXspruiVjdcKTgZsEfo9xMbUSvkMfkUAq6CEqp5UpU
+8UL91QyK3NFvQ/0hb5IgrTKq5aeK93ZNr4RNlVfLJ7R83KbNbNFNrdxlga5Qr4h1
+7QUfXYVq0fkOVukxA+gYdVeE/oqrFmdpOlOte2sFdcEQm25HsDcpsVaV/GUpRuoh
+PU5HWa2FEEaes9R7Yyva7D6FTOTceEVCIWVPHRz8esujgxlKnvSA3zKqusPzEGqX
+lM1mLMmMlklrZQQ8cFULzmhllY2u+fcj6DGDSeXjm92CXzfoSjntclU1OXHxwZc1
+BQ88odnaMtjf9GkErG7oiiRyv1vuYVXEEh8D34JfV/AIZizLD5MhRuwhTKFdUtdM
+SgUZpYtLVRTmAeYRaTsSWeEKcPvHC3CuqmEwF9OHiArwxBve23PoLkgQufdygnQP
+LmFN8Xv706TOsm8flXmbRZs987llKDAinThrZlfN9VOQVZiKHt8ZM2hFFuwsh1sn
+vs/AF9e0NTI39vA2MtNrKbit2LoB+5ZTl7dgyzgRR/rhD26Nj7R8B68cXHaS3eKG
+cRAMcQ1Pl2AcofbXiKu9tLeXXouin0j1iyySBAt5yzMLw+j15t526zBrGQN1/Aes
+Ukp4ao2+gluz231crZXjgunXJo8AcW40RXmvvyWakL5Iv5hCGnAWih9LCvuaPXWg
+ADexTLNpCMmsR/WAUnB3xQCVXyYoF+OBmyaIuBQ5HzH7fUgQA3k1ARtynuYcPQhH
+CXK752U8Zh1QA/D5tPFy0kR/V1QDO9zXpUeZfUnb2B7dE5o0LBy3UbmzLkpv28LA
+vyEWiKq+8igH4zjNhFTbQ4bMaTDizGwoS4KfHVt0cqDxzBIYgfRdK4v7ybnfnWwA
++Ye2ULsvE6kdgGbQ+12PKdQCZ3++8r2xY9FxXivxQjVhzsA9c6J0VHCOH1b6vv/z
+5D0Db0rjWqd10J8rNW2sP+PB522fIP/BMZPEcMsu/UbMHYt+ULk/tHKEA7znGnLV
+BbIHgjr92DRlAT/rjUvzvfKg612fPYUeXU9ff+MNA+2sMFSsY5l8TcGZIKvk8OGR
++Pq1dJa+BVlB2PNQZp1/6294mqXcZ2LIYU7UAau8rB5ZLebGCnrSILWtmmPi4uIK
+HS6/PE9qIS3o1D3yflI6KKHDhU29OGLFTi89HHe3IUEnOw008fzLC8gh84vZWngA
+IX2HjKQEJJMX7mFJ3EteYSt4tdWCdYXz4yEjtZDfzsTJYKKYQvmu1nLElWDi0VIK
+eOkfQRF+hNXOkoBg5C4jmZk/wTwUV+bqYIxdK9kGOgJw6+tw97piw0hUI9leNIFl
+UMjxCgNHa52tEMgyOKxryKjUuxxj0oxdoNNp0d6CIgiAZEwMjw1aR2waieGZJJwD
+GWwtjRSdHearoKBlGeXclj3pZgSkDNE0TAJbMN9QduQHnDlWXRgV8iBO1A/PRDJs
+rpivUpOjNzhGWfWVGmn5lA/UWsYCtgjPxnbwkV/7BIqCmQvZOUkCWDnq/J+LHKeJ
+rXtMx71C3WfOOXOCFdh1d5NoBCCghPh2sOhTrevtT8oBvbZbqPz9xSwl4WwodAdm
+2EhkioiY+5hYaqzlbXj118BIROZsAjkBWsTy0Eh6ev8a+6Prc/nUEUhmvuya8swb
+b2L/x5kGSGX3BiPfUXi1xdHmCvoqxLTA128VfK4COsalqsfIDsbQ4zMU7b1esH4V
+mIyAiFL9k6UR0WboW/MneIV5zUxxHKiHH5qzhAZRzMvU6jOGZMmWfn2usYK8dsfQ
+L+d2NpapgCmVoM9Btv11ZOVNZhx6E5SpeM2zfxjlc47l72/2pfz/Hz7gRsiw4xuD
+L4YOnW9yjxdMoM54249LwozNpMR5GFaqzqBMF8/nLUwB6OQiXgjp2G65EaJG5T3+
++dVrl7AllIuAd04hpjwb/HR/KK2F73QG0hIQuCnLiqjSwNM6tGD4/BRZV7Z1zwgR
+Ab1dJKP7ZAO7Pnm8kO3AM9ib7KJnou3ZzErqnNwKRSMwvnQDI+E4feu2NTmg0LWB
+PLpgh3ljzl/TMDMHqsdm2gPvd5Ql/lLnivGELVFxtncVIxU29NwfmOoVRBW4LgVl
+roUEvSmoUdLxd+0c4l1nnoq87mVbpx5oQ3KjwK4GgUHyRJshYQs87HyJ5c8whM4M
+QBVvDBU/NvaZ0R/9FOqUFHM2yxWGHgqs4OSzlyHMCybOixxvwSOsDoauppIBO/t6
+cNqWruaqcYbs2FYhTZg3HKmjaRbIoGLl0Ko3BJstWyweqWJQeOijyQ84ejkwAkFL
+cuDYH55yehsQ5O1h0UlWXwM4qTLOJYj9UvH7aVJZY+mhbrIJ74Vo8eNeJ6GxShni
+gVJp2N3aiRc+brkAT7qFDG/+EPp+kK2h41wSqZLpZj24q5oLbiRksv5msS7CIE4w
+BL1xRK8SwpeVAlnWZ0XyBbQ9tccvqOLreY7EcsAjf2ONKsHYl6phciIyT1Cul/1M
+PzeRrYQuRLAnYi7+TOAFkkPqdZxqfbC5cs11tseFOg/Rjqmd6VHZw7jTzlgjw1Pw
+YsEr9TDAMbApLUwtDO/bAmkVPrUhufT9E+U4zBo7ln5oh51Tjiz/5Xp1olXWfa/j
+fv+Xwjbx1Sg7Y49SizVsS5ilzSd6XO51ob0O1dl7JFGT9JTmyTI++PEfSOwNFKRl
+NjoT0C4tIAXyBIRJm9umFooO+7URm6gmjLdggvbbUmJEIQsexhHIwKXMEUucpU1A
+PY6IoglWF44l3bhh/SANk1E9BeiDza00vZwzSNpPfFexRpt0XGhZabS+3MOBJLFi
+pk6ASWI81r0jTrRiz00COnQDPsBVp86NT3B/v4gbXJ6F05gDbA9cW5qOU8EIUtFo
+gblCweTQH8topgxKhcJenWkCuoEwpopfLwY3kJVVsKj7+4kwP+BrAdzreCQ83JCS
+5ySboNlHMx7WXFiRf3f1lSGPBYtzGN/v4X8m9tOW95kmIpuCKSYXNJQlnDiie73w
+2ogqgOsXWp6bI3d7u5VCKAZjpx9sEGLC46v+n1M5pjcoriit0PuEvyX9G5839F4T
+HUFx2wsAMQxT4I+Qhk9BxZ/G/d+u0i4FsgNns+ijvqfOMlURVBcYHYjOZUgXJZtO
+sfILq9jo8dyIjzmAbqj2RY3YvUJT3KTsDXJC7i9WhKv2hUADnScfo2MqfIO67HnN
+tGKb1XRhY+FMgMXOpLCiY86+qkZGrT23gks3W9vY4Ko7LIXj4Ao93+o/bbtcZTTj
+ZIXIpMaigjpEBFpVk5h6Af1ajFFSjzzPRWhywrBOUuXGsqSigzMvNMCsPYFuErp7
+GWVdTah+JE+V9uR0/JS2Tr+mnX6HnjoPltWRQ+MEQT/Jiw1AUi8pdD4LdR/MK5Tl
+307G9hsysMA/bQpPWsj7b994OF6XD2cBNFVt+sTHc4OpbTEnwLIIC/0uyHSE6vIL
+0pApr/+xtkSUmcbiEcLfBtKd1Wibdvkzea1zcSKdSXgtK/QjvK9AidMWI6WOuPiy
+Yk/9o/SnyYEVdHvbJg74JldVIzK8a7Hay4Q2XQ1eU3H63dPLOfeGiGARMQp9btv0
+vGvQJ7PWu6cNLF0gv5vaSvPU+YvDCRB8xd2J1/MScN3JEPHxRy8xyB89D5Z3dHmW
+SpIqiHJ9o3pP5CIoZjjftwPpZkklJGzG0272nFWJbbuScbBNa8JSZ1a8IXrhzUi5
+83fh2/TsCF8bSkyPgI8GPY5z/rj+MEzIrq67K/JWqCALcbrSfHFS59LGE5gJwMng
+7kLnLmI235FKlBwtQESgI8uP/t8ydYCEkNJRvIOAhJcbQR/Zbn7FIqFTFQrgGg3K
+nntK250VWYK2e83ZvaBK7wXc/7TTHBzpbdXpUvLDQxUi/CCJhgHluyISiR7PPn/H
+d3K0jWUGv5LBmLoq2S4OTgEq0C1bE5dTIXQE4SxDSxJoKUXrqO8nIY5SfUI5v5MD
+8VVU5IgUuaSXdcN5UAPRlT88bb1wRUQ4+WoJUYDTWFwhmjOoxeLo5nTCYRoBu8UW
+wmpc0VGU+j3/nxYI48sjn9gTHpoG2f8YBTzpgN9SriM6pM7zE12TgiDBU/fCDxB4
+YW+q/AWxa6hWyURfY9Uy0oa1FeocCDT4kAkaiNM9DqFUaI3Oviz55c7nnF2jS5v7
+cobAi1OxSZJcxH4DHSVYUptmXdXEyTlbYbJN7Wq1+h65SOV6aTQYQKu2YF4C9qvv
+zFtilXkyJ6gskG2209q9i6dA8C+hH9n+lxhkhy6llTZV3CHb/ru5yPUseV4XghwU
+6fH64AgqFmz1F5onEui2JJvoYbwyD4/UQ59/dTe5XIAgaI4WW0LuB3HuJiBu+8rv
+lhcxJEMWP6ZpHpvLD00ENYfFWBl/kZtuOs/M30SlErYO6OtrZfxo+42utFPv+c3f
+hUQqvSuBa7/Ix6adau6CBSL7Rqqmph/HZ4WCFLECCDKZNn2r3PAfVE4zmii/oK2K
+PdylDQoUkARvtZz1fX7PcDcPfUB/yPik3dH2UeqOBt4arcb1XB5+r5HMWc9E8KA/
+m6cPgYoNy4yxqw7+TFSZI5yrUFKCjQA9rpaI3ea2OjWc6CpE3sEo0EC2dcrLEuPe
+YKdQ6t5fe/Zwm3Soxh1GqKzcl2ZOAHZkzrZ0I/cHPHWIbOnEodmIW5WMMN8oStsS
+rBhD8k0ckS0EgvmJeStETuSPX0vJIQXBs9nxSVP9hPTLuDCQ7TJXInj9VZ3XifRH
+GKk4IvmvGj9CHSXros77m2Iw0NGKxhKNKjAVGehTy2GY1iC2kDYcPbM235OO3jTc
+8+XeN9m4XETSFPqeE4ALYcSRvYusFZ54VYloe97aHptYtz7u10r9ooM3rQPfalkZ
+HwR9zj8KfFS+dJhQCISw/pS/1yNU+m9dM69szKuZKfrN2f5Fd2qyZd5N74MdZUdB
+Qhcd5CLfme7wYUvxg+4Vo/hPe2aiX+xF3EgAC4sU3vsVb3gBRXAoWoY85UsZkmS1
+b82IXy/7PZcRAlU1qjelOMxu3DIkpTKsiCcxG5H8POLXpGte1WmFgj4XKU17gSHY
+jhXdxegbPbk4OQDsWPflWh3r6s/eUowPFL5yL1WUccoobesaWK+3mzkCZxmhJml5
+ASZyY2iD0uR/7ns9D1vgAWN3jHbSXNoMrm1pQZJ6Vfl0u5JfH7HLZXnIjQUYI/6k
+WLj70c4E8syK1ZQ7XfZd1zSh2ADATPd+t920YiIyS1i262W5rMkL9QzBDAT8b49c
+2+6wM94b8b2Fztw8zf2XPAurzvCRDs/jJ5mcVC03tP9pULVtVJ6hSshoRtiKQqce
+FJI7P9QGzEVwafB353VkYhpd6MYk2MdOclwhOACl+ri9VIZBKWah2gMbQ9sGbSwZ
+MZSWx8BDTyUq0y9iUoVnSaIoLenXFyAzlYuBql6dz56ZX5nhw2B4+Vj0OrhES2Sx
+9hsJuOMPeGWw8YCBGMigQMZpPRUDSaFmf790BIXhRIussbDTIaJsAXuEFjFg82AI
+thfxBgSKMcEzycbtuxDFDjSAKRb+cWn9zqxNusJlCuDfnf2wxwtLW1rLjkeM3ipz
+9IWle8+fPDOgSMVe9SUbnH38hBKsVNhzByFRtzJ7uAlJ4FFwo3OtMyMSRwMDW6sa
+TXWBoLDFcG0G5oBGeLcXgnScprkRnFNdpC5016bqe8i0bpBpLOJovdGERiCcmhPo
+9C9WzyzaU2C5O3zzkZKJvsiqMYTkDorqMN8gm1Oy2T39p8cLuYpf3/WLagRzjiik
+mcow8j1DU5pjqB+1a04xFysaQFaqDyV1Wdcq3Xps7tXw0v73S966zMtFUI0Yszzy
+qRiIjqoYdh4B11AITokzwqpaaHi7uifu87mDjZUQR1pH1owedtmMmXniF0SOmkX4
+BRq2XLqQC3cS1tp5kpJ/jHiLVMLw5ueadwQLjSBrDpbdmNHSgtD9HA9AaDo0kefD
+u/H3/xF24QOopYYci0X6vomKo0xWhaQTh0wXjem9pEL0I9NMYDZmlk3wAHQ0Qvw6
+zQoL7dbfLCWxfCle1nyuqvEnzzlLvtPgtbSpuXiLGD2PbKoWIJA6W7GnLXhUJnkf
+irMeseXpoTg/5tmOPh9OLELb8Z2hsoSDxkkpPFw6dBJxLTTc9gHBWB94Inz1fB4Y
+wk2xN4fNzJwFknG7NmRv9ieGpsHZ2Yg6seOiT1JfHFFlD1v4SC2NZo2qHVv3QfQ/
+nkh7NgDmq53VXXQGzOA5r8vPrxdj1tX2qW83Xpx1ENOVZQWq8t2AENe2OHR64bKH
+XmysCm6Jc9PtoHpVRd06Kme+aOHa0M3K2g/CWXjMD4nhOBDuRKz8/tKCIteSXsCO
+tu6pAk1w6jB7FnZBWPKXzLK4+4etYdoxTvcrDR1xpTuFQHE58EOZlXv1sISGjif2
+9rKfO+AOv1TKaqrXB5VT4x661IGsSa3ETne1UMdzREq8WGUPkb+oqGtnOCeiPNPl
+qoBLoaK7xZocaQBOJZXwPSSEacjxaB0JjFChI+DFU4AyjocVS7gk4cKA3p1ulvbK
+dgJsc2o0SBsS5tu9l0Y+T9McPIalh1PTkb6vR79BwIUucZ8gL88l6tXYdXHqMryi
+I9DOslqjgigJLoC0lPfyh3MikFoFI1vAn1flRbEj78kwsQ8v4YI/Z9k+fSUHDLVO
+HwRCi6CsZOmCwDk0dXxUH/EqPjbk7OHSrBgtLyZlgd3KZaLuXiQaENJBhh0Jlm0G
+zi1/hN+FgIb2yLnmQ2Ph2qz5wKZ8YDRn6itcYlMJzA/YZNHzY8TEu/pmtI9I964Q
+ZcOVusv+bhhtr9ygOvWkxH99CIDSwvvqrvzoIVeLRAoD/xpD9XxrmVtvQ2De3KrD
+x9VYXiYvXKtI/KIouC1a+aqiWb31+1y/xsuLPaOkWLH1E+QKmms2prW6PZ2dYw1w
+rQXs8BvM629l6RJqihZgXLPrTMTFaZ8U2mtIKUw3LkVCX2LgSdmpUUqvJ7yotQF0
+xFQG3Jwlp3tyL9Y8bbYIHae3lpTyw11sGQC+AJhL/+fks1zYwvopx+mW1qMNjlRp
+gGzKJnORJnYKM75CeGMsianazBcbCFcgPHoVg/T9ttGQOafh/pg/9YkyOEZeoE57
+/vH9EC9Bruzw3FzVZmWvFTsKjE7eRnj1tWt1hBf1KVxlPQotbwIvZpCKc6j5It7m
+RV2Uwv4vQNNnf35IMVpSpxvs17IgH5HcDZgoBpkc7EIrllmGFGSpkYueaxYNxP9+
+C9u1wUHkhNIoMhE/5QTsll9KwygQbqI4sQMEcLvYk4oQOPJjZ97UgemIUAWJDj34
+BNhuz6HHryt2tX7E/lq3YFXFKRDkq2F3q1siP9waW3Hf7b3e8bpK+T0z1PIvAQiC
+T+lqmad+OKSRpH/B8E9fgF60wJMUC/fRQXuaO4ZpSUWlOLwOE+mUv2j9BCsBvjDc
+bVXiSZEViQlQIL34CHk/AVVhxkiUaIjFRGzK7aXl+7Rd0jrwxrvGxgXHlbovfd5w
+Tptcp+i8xL+btFD5OPt2a7WKmd9UBu7FCO9NPLUohgJat+uQulvYDjdmj1wMRAbC
+U/kCxUUqrU3gNtDzqItOQy2wd53NiPTxlzilY+xUG1tbtFF3I5zSghikyezr84P0
+EK63HbyNLEt3vQmaVk/SUNspz/6r5eSeslnzZzgGDtiBdLGiCXG4iymI6C0UQWHT
+8Jzirj+XzeeON0cWO1xmOjwFys1ZB92Zn40BBWWXBR00WBd/5/gCEwm5gkC24z06
+dVaPanNqa2jIt1x9jaHrAz9mTkFsg8Krts9ShFsdrD7Pql7Q0sLXp/7pLf8DdkNX
+SL8KLiOQcwubiWl12EDlX+puEic1D81sxNzjWMIFXKZC0oUT3CSMbJjjk2O6Uaz8
+wFUz/PmnBLKjXOUZXAkfJM+h4s/zYDArkYfDOY28AiyOubwlPl4WE6ek0XgMngk7
+StJovaYlxVAeTyKZPGzjLsuULu8jOIiyQh68Uge2mMEgHEpMdu/46A7Dxb8jnxXF
+w6t5SONEd1QKppXpjtbcfYJ8+ZdCjYWdCPLySpChlf3WTsBXj/9g7hfXw9cnydpC
+K7x3fW3W3V5FPWfO9TjFYJFzh0r4lhuzpOk8xkWn+elexseIdVhciHutusIqj0t0
+PU2Gb/CACaP6akn0NP6aQTCqcu99iABqfbS4gUJAXb9sf/eTblCCaD03J/AqhgAN
+Kz0mcXgB3sHbw6LJ6W0tB0mvd2ogWBUzkTkrmarsDI27ueTjlDF6liVkJy5d4Kls
+2h/3EEFIwIHVFcBS+/2CfUQhMQbGO4ynivRLdkeXu47ahHB5IlKDyiVfDh1/CZRR
+fSacsGl/9JnZZhXHhJEccoReXEKaNQOF5okg/OOMcIk9Ub8FRIidtPu3sMNQd5aw
+OqSc9tGDcmANF3LQEj1htma9yReOKmZdQTDY
+=s6vG
+-----END PGP MESSAGE-----
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.gpg b/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.gpg
new file mode 100644
index 0000000000..1feb515938
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/data/bluebird50.jpg.gpg
Binary files differ
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-key-and-windows-1252-encoded-eml-attachment.eml b/comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-key-and-windows-1252-encoded-eml-attachment.eml
new file mode 100644
index 0000000000..599d5f64f4
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-key-and-windows-1252-encoded-eml-attachment.eml
@@ -0,0 +1,109 @@
+Content-Type: multipart/mixed; boundary=\"xy23ZrTYskosBXu9d5LEB0IV1ZMMCLTf7\";
+ protected-headers=\"v1\"
+Subject: Key And Windows 1252 Encoded Attachment
+From: Bob Babbage <bob@openpgp.example>
+To: alice@openpgp.example
+Message-ID: <ce639032-0823-5c4d-30a3-76a874950908@openpgp.example>
+
+--xy23ZrTYskosBXu9d5LEB0IV1ZMMCLTf7
+Content-Type: multipart/mixed; boundary=\"------------azSRG4BClDM2kDHcC4FYbiRW\"
+
+--------------azSRG4BClDM2kDHcC4FYbiRW
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Please see attached.
+
+--------------azSRG4BClDM2kDHcC4FYbiRW
+Content-Type: message/rfc822; name=\"win1252.eml\"
+Content-Disposition: attachment; filename=\"win1252.eml\"
+Content-Transfer-Encoding: 8bit
+
+To: bob@openpgp.example
+From: carol@openpgp.example
+Subject: Windows 1252
+Date: Wed, 4 Nov 2020 16:32:02 -0400
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101
+ Thunderbird/78.4.0
+MIME-Version: 1.0
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 8bit
+Content-Language: en-US
+
+This message has 6 ü ü ü ü ü ü.
+
+--------------azSRG4BClDM2kDHcC4FYbiRW
+Content-Type: application/pgp-keys; name=\"OpenPGP_0xFBFCC82A015E7330.asc\"
+Content-Disposition: attachment; filename=\"OpenPGP_0xFBFCC82A015E7330.asc\"
+Content-Transfer-Encoding: quoted-printable
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv/seOXpgec=
+TdO
+cVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz/56fW2O0F23qIRd8UUJp5=
+IIl
+N4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/5whqsyroEWDJoSV0yOb25B/iwk/pLUFoy=
+hDG
+9bj0kIzDxrEqW+7Ba8nocQlecMF3X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8=
+Je/
+ECwUWYUiAZxLIlMv9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx=
+5wR
+Z4F0qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7ebSGXrl=
+5ZM
+pbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wbvLIwa3T4CyshfT0AE=
+QEA
+Ac0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1wbGU+wsEOBBMBCgA4AhsDBQsJCAcCB=
+hUK
+CQgLAgQWAgMBAh4BAheAFiEE0aZuGiOxgsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFec=
+zBv
+bAv/VNk90a6hG8Od9xTzXxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9=
+IOh
+Q5Esm6DOZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g9=
+EBU
+Wiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXFDcCZCi+qEbafm=
+TQz
+kAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7cZUzs6Xh4+I55NRWl5smrLq66y=
+OQo
+FPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G16rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC=
+8Ea
+CDfVnUBCPi/Gv+egLjsIbPJZZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDh=
+mUQ
+KiACszNU+RRozAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsDNBF2lnPIBD=
+ADW
+ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvIDEINOQ6A9=
+Qxd
+xoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+Uzula/6k1DogDf28qhCxM=
+wG/
+i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AObaifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2=
+WTY
+Pg/S4k1nMXVDwZXrvIsA0YwIMgIT86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW=
+2+L
+XoPZuVE/wGlQ01rh827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paL=
+NDd
+VPL6vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76UqVC7K=
+idN
+epdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48AEQEAAcLA9gQYAQoAI=
+BYh
+BNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJEPv8yCoBXnMw6f8L/26C34dkjBffT=
+zMj
+5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcSKhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhB=
+AcU
+WSupKnUrdVaZQanYmtSxcVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV=
+9zp
+f3u0k14itcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHVd=
+Trd
+Z2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+wqMJxfpa1lHvJL=
+obz
+OP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6VyjP7SXGLwvfisw34OxuZr3qmx1=
+Suf
+u4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xjzRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGo=
+Bj0
+HCLO3gVaBe4ubVrj5KjhX2PVNEJd3XZRzaXZE2aAMQ=3D=3D
+=3DF9yX
+-----END PGP PUBLIC KEY BLOCK-----
+--------------azSRG4BClDM2kDHcC4FYbiRW--
+
+
+--xy23ZrTYskosBXu9d5LEB0IV1ZMMCLTf7--
+
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-windows-1252-encoded-eml-attachment.eml b/comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-windows-1252-encoded-eml-attachment.eml
new file mode 100644
index 0000000000..b43d9883bf
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/data/plaintext-with-windows-1252-encoded-eml-attachment.eml
@@ -0,0 +1,39 @@
+Content-Type: multipart/mixed; boundary=\"mzik1s6PCI8wO850i5PykshHnKiAGShJ0\";
+ protected-headers=\"v1\"
+Subject: Key And Windows 1252 Encoded Attachment
+From: Bob Babbage <bob@openpgp.example>
+To: alice@openpgp.example
+Message-ID: <26826c45-bac5-f173-9d60-dab5c907156d@openpgp.example>
+
+--mzik1s6PCI8wO850i5PykshHnKiAGShJ0
+Content-Type: multipart/mixed; boundary=\"------------voUvzEdWKr8OR9mQliQ0aHDW\"
+
+--------------voUvzEdWKr8OR9mQliQ0aHDW
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+Please see attached.
+
+--------------voUvzEdWKr8OR9mQliQ0aHDW
+Content-Type: message/rfc822; name=\"win1252.eml\"
+Content-Disposition: attachment; filename=\"win1252.eml\"
+Content-Transfer-Encoding: 8bit
+
+To: bob@openpgp.example
+From: carol@openpgp.example
+Subject: Windows 1252
+Date: Wed, 4 Nov 2020 16:32:02 -0400
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101
+ Thunderbird/78.4.0
+MIME-Version: 1.0
+Content-Type: text/plain; charset=windows-1252; format=flowed
+Content-Transfer-Encoding: 8bit
+Content-Language: en-US
+
+This message has 6 ü ü ü ü ü ü.
+
+--------------voUvzEdWKr8OR9mQliQ0aHDW--
+
+
+--mzik1s6PCI8wO850i5PykshHnKiAGShJ0--
+
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/test_alias.js b/comm/mail/extensions/openpgp/test/unit/rnp/test_alias.js
new file mode 100644
index 0000000000..cc30b52a8a
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/test_alias.js
@@ -0,0 +1,321 @@
+/* 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/. */
+
+/**
+ * Tests for OpenPGP encryption alias rules.
+ */
+
+"use strict";
+
+const { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+const { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+const { EnigmailEncryption } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/encryption.jsm"
+);
+const { OpenPGPAlias } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/OpenPGPAlias.jsm"
+);
+
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+const keyDir = "../../../../../test/browser/openpgp/data/keys";
+const mailNewsDir = "../../../../../../mailnews/test/data";
+
+// Alice's key: EB85BB5FA33A75E15E944E63F231550C4F47E38E
+// Bob's key: D1A66E1A23B182C9980F788CFBFCC82A015E7330
+// Carol's key: B8F2F6F4BD3AD3F82DC446833099FF1238852B9F
+
+const tests = [
+ {
+ info: "Should find Alice's key directly",
+ filename: undefined,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "Key absent, no alias defined for address",
+ filename: `${mailNewsDir}/alias-1.json`,
+ to: "nobody@openpgp.example",
+ expectedMissing: true,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "File maps Alice's address to Bob's (id) and Carol's (fingerprint) keys",
+ filename: `${mailNewsDir}/alias-1.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: [
+ "D1A66E1A23B182C9980F788CFBFCC82A015E7330",
+ "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F",
+ ],
+ },
+ {
+ info: "File maps Alice's address to an absent key",
+ filename: `${mailNewsDir}/alias-2.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: true,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "File maps Alice's address to Alice's key (unnecessary alias)",
+ filename: `${mailNewsDir}/alias-3.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: ["EB85BB5FA33A75E15E944E63F231550C4F47E38E"],
+ },
+ {
+ info: "File maps an address to several keys, all available",
+ filename: `${mailNewsDir}/alias-4.json`,
+ to: "nobody@example.com",
+ expectedMissing: false,
+ expectedAliasKeys: [
+ "EB85BB5FA33A75E15E944E63F231550C4F47E38E",
+ "D1A66E1A23B182C9980F788CFBFCC82A015E7330",
+ "B8F2F6F4BD3AD3F82DC446833099FF1238852B9F",
+ ],
+ },
+ {
+ info: "File maps an address to several keys, one not available",
+ filename: `${mailNewsDir}/alias-5.json`,
+ to: "nobody@example.com",
+ expectedMissing: true,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "File maps the domain to Carol's key",
+ filename: `${mailNewsDir}/alias-6.json`,
+ to: "someone@example.com",
+ expectedMissing: false,
+ expectedAliasKeys: ["B8F2F6F4BD3AD3F82DC446833099FF1238852B9F"],
+ },
+ {
+ info: "Multiple rules, should match domain1 rule",
+ filename: `${mailNewsDir}/alias-7.json`,
+ to: "someone@domain1.example.com",
+ expectedMissing: false,
+ expectedAliasKeys: ["EB85BB5FA33A75E15E944E63F231550C4F47E38E"],
+ },
+ {
+ info: "Multiple rules, should match domain2 rule",
+ filename: `${mailNewsDir}/alias-7.json`,
+ to: "contact@domain2.example.com",
+ expectedMissing: false,
+ expectedAliasKeys: ["D1A66E1A23B182C9980F788CFBFCC82A015E7330"],
+ },
+ {
+ info: "Multiple rules, should match email contact@domain1 rule",
+ filename: `${mailNewsDir}/alias-7.json`,
+ to: "contact@domain1.example.com",
+ expectedMissing: false,
+ expectedAliasKeys: [
+ "D1A66E1A23B182C9980F788CFBFCC82A015E7330",
+ "EB85BB5FA33A75E15E944E63F231550C4F47E38E",
+ ],
+ },
+ {
+ info: "Multiple rules, shouldn't match",
+ filename: `${mailNewsDir}/alias-7.json`,
+ to: "contact@domain2.example",
+ expectedMissing: true,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "Mixed case test a",
+ filename: `${mailNewsDir}/alias-8.json`,
+ to: "a@UPPERDOM.EXAMPLE",
+ expectedMissing: false,
+ expectedAliasKeys: ["EB85BB5FA33A75E15E944E63F231550C4F47E38E"],
+ },
+ {
+ info: "Mixed case test b",
+ filename: `${mailNewsDir}/alias-8.json`,
+ to: "b@lowerdom.example",
+ expectedMissing: false,
+ expectedAliasKeys: ["D1A66E1A23B182C9980F788CFBFCC82A015E7330"],
+ },
+ {
+ info: "Mixed case test c",
+ filename: `${mailNewsDir}/alias-8.json`,
+ to: "C@MIXed.EXample",
+ expectedMissing: false,
+ expectedAliasKeys: ["B8F2F6F4BD3AD3F82DC446833099FF1238852B9F"],
+ },
+ {
+ info: "Mixed case test d",
+ filename: `${mailNewsDir}/alias-13.json`,
+ to: "NAME@DOMAIN.NET",
+ expectedMissing: false,
+ expectedAliasKeys: ["D1A66E1A23B182C9980F788CFBFCC82A015E7330"],
+ },
+ {
+ info: "Mixed case test e",
+ filename: `${mailNewsDir}/alias-14.json`,
+ to: "name@domain.net",
+ expectedMissing: false,
+ expectedAliasKeys: ["D1A66E1A23B182C9980F788CFBFCC82A015E7330"],
+ },
+ {
+ info: "Mixed case test f",
+ filename: `${mailNewsDir}/alias-15.json`,
+ to: "name@domain.net",
+ expectedMissing: false,
+ expectedAliasKeys: ["D1A66E1A23B182C9980F788CFBFCC82A015E7330"],
+ },
+ {
+ info: "JSON with bad syntax, should find Alice's key directly",
+ filename: `${mailNewsDir}/alias-9.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: null,
+ expectException: true,
+ },
+ {
+ info: "JSON with missing keys entry, should find Alice's key directly",
+ filename: `${mailNewsDir}/alias-10.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "JSON with empty keys entry, should find Alice's key directly",
+ filename: `${mailNewsDir}/alias-11.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: null,
+ },
+ {
+ info: "JSON with bad type keys entry, should find Alice's key directly",
+ filename: `${mailNewsDir}/alias-12.json`,
+ to: "alice@openpgp.example",
+ expectedMissing: false,
+ expectedAliasKeys: null,
+ },
+];
+
+/**
+ * Initialize OpenPGP add testing keys.
+ */
+add_setup(async function () {
+ do_get_profile();
+
+ await OpenPGPTestUtils.initOpenPGP();
+
+ await OpenPGPTestUtils.importPublicKey(
+ null,
+ do_get_file(`${keyDir}/alice@openpgp.example-0xf231550c4f47e38e-pub.asc`)
+ );
+
+ await OpenPGPTestUtils.importPublicKey(
+ null,
+ do_get_file(`${keyDir}/bob@openpgp.example-0xfbfcc82a015e7330-pub.asc`)
+ );
+
+ await OpenPGPTestUtils.importPublicKey(
+ null,
+ do_get_file(`${keyDir}/carol@example.com-0x3099ff1238852b9f-pub.asc`)
+ );
+});
+
+add_task(async function testAlias() {
+ let aliasFilename = "openpgp-alias-rules.json";
+ let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+
+ for (let test of tests) {
+ if (test.filename) {
+ info(`Running alias test with rules from: ${test.filename}`);
+
+ // Copy test file to profile directory (which is a relative path),
+ // because load function only works with simple filenames
+ // or absolute file URLs.
+
+ let inFile = do_get_file(test.filename);
+ inFile.copyTo(profileDir, aliasFilename);
+
+ try {
+ await OpenPGPAlias._loadFromFile(aliasFilename);
+ Assert.ok(
+ !("expectException" in test) || !test.expectException,
+ "expected no load exception"
+ );
+ } catch (ex) {
+ console.log(
+ "exception when loading alias file " + aliasFilename + " : " + ex
+ );
+ Assert.ok(
+ "expectException" in test && test.expectException,
+ "expected load exception"
+ );
+ }
+ } else {
+ info(`Running alias test without rules`);
+ OpenPGPAlias._clear();
+ }
+ info(test.info);
+
+ let addresses = [test.to];
+ let resultDetails = {};
+
+ let isMissing = await EnigmailKeyRing.getValidKeysForAllRecipients(
+ addresses,
+ resultDetails
+ );
+
+ Assert.ok(
+ (isMissing && test.expectedMissing) ||
+ (!isMissing && !test.expectedMissing),
+ "Should have the expected result from getValidKeysForAllRecipients"
+ );
+
+ if (isMissing || test.expectedMissing) {
+ continue;
+ }
+
+ let errorMsgObj = { value: "" };
+ let logFileObj = {};
+ let encryptArgs = EnigmailEncryption.getCryptParams(
+ "",
+ test.to,
+ "",
+ "SHA256",
+ EnigmailConstants.SEND_ENCRYPTED,
+ 0,
+ errorMsgObj,
+ logFileObj
+ );
+
+ let foundAliasKeys = encryptArgs.aliasKeys.get(test.to.toLowerCase());
+
+ if (!test.expectedAliasKeys) {
+ Assert.ok(!foundAliasKeys, "foundAliasKeys should be empty");
+ } else {
+ Assert.equal(foundAliasKeys.length, test.expectedAliasKeys.length);
+
+ test.expectedAliasKeys.forEach((val, i) => {
+ Assert.ok(foundAliasKeys.includes(val));
+ });
+
+ let encryptResult = {};
+ let encrypted = await RNP.encryptAndOrSign(
+ "plaintext",
+ encryptArgs,
+ encryptResult
+ );
+
+ Assert.ok(
+ !encryptResult.exitCode,
+ "RNP.encryptAndOrSign() should exit ok"
+ );
+
+ Assert.ok(encrypted.includes("END PGP MESSAGE"));
+ }
+ }
+});
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/test_badKeys.js b/comm/mail/extensions/openpgp/test/unit/rnp/test_badKeys.js
new file mode 100644
index 0000000000..3ca7709dc6
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/test_badKeys.js
@@ -0,0 +1,69 @@
+/* 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/. */
+
+/**
+ * Tests for bad OpenPGP keys.
+ */
+
+"use strict";
+
+const { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+const { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+const { EnigmailEncryption } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/encryption.jsm"
+);
+const { OpenPGPAlias } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/OpenPGPAlias.jsm"
+);
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+const KEY_DIR = "../../../../../test/browser/openpgp/data/keys";
+
+add_setup(async function () {
+ do_get_profile();
+
+ await OpenPGPTestUtils.initOpenPGP();
+});
+
+// Attempt to import a key with a single user ID, which is invalid,
+// because it doesn't have a valid signature.
+// Our code should reject the attempt to import the key.
+add_task(async function testFailToImport() {
+ let ids = await OpenPGPTestUtils.importKey(
+ null,
+ do_get_file(`${KEY_DIR}/invalid-pubkey-nosigs.pgp`),
+ true
+ );
+ Assert.ok(!ids.length, "importKey should return empty list of imported keys");
+});
+
+// Import a key with two encryption subkeys. One is good, the other one
+// has an invalid signature. When attempting to encrypt, our code should
+// skip the bad subkey, and should use the expected good subkey.
+add_task(async function testAvoidBadSubkey() {
+ let ids = await OpenPGPTestUtils.importKey(
+ null,
+ do_get_file(`${KEY_DIR}/encryption-subkey-bad.pgp`),
+ true
+ );
+ await OpenPGPTestUtils.updateKeyIdAcceptance(
+ ids,
+ OpenPGPTestUtils.ACCEPTANCE_VERIFIED
+ );
+
+ let primaryKey = await RNP.findKeyByEmail(
+ "<encryption-subkey@example.org>",
+ true
+ );
+ let encSubKey = RNP.getSuitableSubkey(primaryKey, "encrypt");
+ let keyId = RNP.getKeyIDFromHandle(encSubKey);
+ Assert.ok(keyId == "BC63472A109D5859", "should obtain key ID of good subkey");
+});
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/test_encryptAndOrSign.js b/comm/mail/extensions/openpgp/test/unit/rnp/test_encryptAndOrSign.js
new file mode 100644
index 0000000000..a52911d288
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/test_encryptAndOrSign.js
@@ -0,0 +1,278 @@
+/* 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/. */
+
+/**
+ * Tests for RNP.encryptAndOrSign().
+ */
+
+"use strict";
+
+const { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+const keyDir = "../../../../../test/browser/openpgp/data/keys";
+const mailNewsDir = "../../../../../../mailnews/test/data";
+
+const tests = [
+ // Base64 encoded bodies.
+ {
+ filename: `${mailNewsDir}/01-plaintext.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/02-plaintext+attachment.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/03-HTML.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/04-HTML+attachment.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/05-HTML+embedded-image.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/06-plaintext+HMTL.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/07-plaintext+(HTML+embedded-image).eml`,
+ },
+ {
+ filename: `${mailNewsDir}/08-plaintext+HTML+attachment.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/09-(HTML+embedded-image)+attachment.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/10-plaintext+(HTML+embedded-image)+attachment.eml`,
+ },
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets.
+ {
+ filename: `${mailNewsDir}/11-plaintext.eml`,
+ skip: true,
+ },
+ // using ISO-8859-7 (Greek)
+ {
+ filename: `${mailNewsDir}/12-plaintext+attachment.eml`,
+ encoding: "iso-8859-7",
+ skip: true,
+ },
+ {
+ filename: `${mailNewsDir}/13-HTML.eml`,
+ skip: true,
+ },
+ {
+ filename: `${mailNewsDir}/14-HTML+attachment.eml`,
+ skip: true,
+ },
+ {
+ filename: `${mailNewsDir}/15-HTML+embedded-image.eml`,
+ skip: true,
+ },
+ // text part is base64 encoded
+ {
+ filename: `${mailNewsDir}/16-plaintext+HMTL.eml`,
+ skip: true,
+ },
+ // HTML part is base64 encoded
+ {
+ filename: `${mailNewsDir}/17-plaintext+(HTML+embedded-image).eml`,
+ skip: true,
+ },
+ {
+ filename: `${mailNewsDir}/18-plaintext+HTML+attachment.eml`,
+ skip: true,
+ },
+ {
+ filename: `${mailNewsDir}/19-(HTML+embedded-image)+attachment.eml`,
+ skip: true,
+ },
+ // using windows-1252
+ {
+ filename: `${mailNewsDir}/20-plaintext+(HTML+embedded-image)+attachment.eml`,
+ encoding: "windows-1252",
+ skip: true,
+ },
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets, all encoded
+ // with quoted printable.
+ {
+ filename: `${mailNewsDir}/21-plaintext.eml`,
+ },
+ // using ISO-8859-7 (Greek)
+ {
+ filename: `${mailNewsDir}/22-plaintext+attachment.eml`,
+ encoding: "iso-8859-7",
+ },
+ {
+ filename: `${mailNewsDir}/23-HTML.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/24-HTML+attachment.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/25-HTML+embedded-image.eml`,
+ },
+ // text part is base64 encoded
+ {
+ filename: `${mailNewsDir}/26-plaintext+HMTL.eml`,
+ },
+ // HTML part is base64 encoded
+ {
+ filename: `${mailNewsDir}/27-plaintext+(HTML+embedded-image).eml`,
+ },
+ {
+ filename: `${mailNewsDir}/28-plaintext+HTML+attachment.eml`,
+ },
+ {
+ filename: `${mailNewsDir}/29-(HTML+embedded-image)+attachment.eml`,
+ },
+ // using windows-1252
+ {
+ filename: `${mailNewsDir}/30-plaintext+(HTML+embedded-image)+attachment.eml`,
+ encoding: "windows-1252",
+ },
+
+ // Bug 1669107
+ {
+ filename:
+ "data/plaintext-with-key-and-windows-1252-encoded-eml-attachment.eml",
+ encoding: "windows-1252",
+ skip: true,
+ },
+ {
+ filename: "data/plaintext-with-windows-1252-encoded-eml-attachment.eml",
+ encoding: "windows-1252",
+ skip: true,
+ },
+];
+
+/**
+ * Initialize OpenPGP add testing keys.
+ */
+add_setup(async function () {
+ do_get_profile();
+
+ await OpenPGPTestUtils.initOpenPGP();
+
+ await OpenPGPTestUtils.importPrivateKey(
+ null,
+ do_get_file(`${keyDir}/bob@openpgp.example-0xfbfcc82a015e7330-secret.asc`)
+ );
+
+ await OpenPGPTestUtils.importPublicKey(
+ null,
+ do_get_file(`${keyDir}/alice@openpgp.example-0xf231550c4f47e38e-pub.asc`)
+ );
+});
+
+/**
+ * Test the decrypted output of RNP.encryptOrSign() against its source text
+ * with various inputs.
+ */
+add_task(async function testEncryptAndOrSignResults() {
+ for (let test of tests) {
+ let chunks = test.filename.split("/");
+ let filename = chunks[chunks.length - 1];
+ if (test.skip) {
+ info(`Skipped input from: ${filename}`);
+ continue;
+ }
+
+ info(`Running test with input from: ${filename}`);
+
+ let buffer = await IOUtils.read(do_get_file(test.filename).path);
+ const textDecoder = new TextDecoder(test.encoding || "utf-8");
+
+ let sourceText = textDecoder.decode(buffer);
+ let encryptResult = {};
+
+ let encryptArgs = {
+ aliasKeys: new Map(),
+ armor: true,
+ bcc: [],
+ encrypt: true,
+ encryptToSender: true,
+ sender: "0xFBFCC82A015E7330",
+ senderKeyIsExternal: false,
+ sigTypeClear: false,
+ sigTypeDetached: false,
+ sign: false,
+ signatureHash: "SHA256",
+ to: ["<alice@openpgp.example>"],
+ };
+
+ let encrypted = await RNP.encryptAndOrSign(
+ sourceText,
+ encryptArgs,
+ encryptResult
+ );
+
+ Assert.ok(
+ !encryptResult.exitCode,
+ `${filename}: RNP.encryptAndOrSign() exited ok`
+ );
+
+ let decryptOptions = {
+ fromAddr: "bob@openpgp.example",
+ maxOutputLength: encrypted.length * 100,
+ noOutput: false,
+ uiFlags: EnigmailConstants.UI_PGP_MIME,
+ verifyOnly: false,
+ msgDate: null,
+ };
+
+ let { exitCode, decryptedData } = await RNP.decrypt(
+ encrypted,
+ decryptOptions
+ );
+
+ Assert.ok(!exitCode, `${filename}: RNP.decrypt() exited ok`);
+
+ Assert.equal(
+ sourceText,
+ decryptedData,
+ `${filename}: source text and decrypted text should be the same`
+ );
+ }
+});
+
+/**
+ * Test that we correctly produce binary files when decrypting,
+ * for both binary OpenPGP input and ASCII armored OpenPGP input.
+ *
+ * Image source: openclipart.org (public domain)
+ * https://openclipart.org/detail/191741/blue-bird
+ */
+add_task(async function testDecryptAttachment() {
+ let expected = String.fromCharCode(
+ ...(await IOUtils.read(do_get_file("data/bluebird50.jpg").path))
+ );
+
+ for (let filename of ["data/bluebird50.jpg.asc", "data/bluebird50.jpg.gpg"]) {
+ let encrypted = String.fromCharCode(
+ ...(await IOUtils.read(do_get_file(filename).path))
+ );
+ let options = {};
+ options.fromAddr = "";
+ options.msgDate = null;
+ let result = await RNP.decrypt(encrypted, options);
+
+ Assert.ok(!result.exitCode, `${filename}: RNP.decrypt() exited ok`);
+
+ // Don't use Assert.equal to avoid logging the raw binary data
+ let isEqual = expected === result.decryptedData;
+
+ Assert.ok(
+ isEqual,
+ `${filename}: decrypted data should match the expected binary file`
+ );
+ }
+});
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/test_secretKeys.js b/comm/mail/extensions/openpgp/test/unit/rnp/test_secretKeys.js
new file mode 100644
index 0000000000..772c5caae4
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/test_secretKeys.js
@@ -0,0 +1,384 @@
+/* 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/. */
+
+/**
+ * Tests for secret keys.
+ */
+
+"use strict";
+
+const { RNP, RnpPrivateKeyUnlockTracker } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/RNP.jsm"
+);
+const { OpenPGPMasterpass } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/masterpass.jsm"
+);
+const { EnigmailConstants } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/constants.jsm"
+);
+const { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+
+const keyDir = "../../../../../test/browser/openpgp/data/keys";
+
+/**
+ * Initialize OpenPGP add testing keys.
+ */
+add_setup(async function () {
+ do_get_profile();
+
+ await OpenPGPTestUtils.initOpenPGP();
+});
+
+add_task(async function testSecretKeys() {
+ let pass = await OpenPGPMasterpass.retrieveOpenPGPPassword();
+ let newKeyId = await RNP.genKey(
+ "Erin <erin@example.com>",
+ "ECC",
+ 0,
+ 30,
+ pass
+ );
+
+ Assert.ok(
+ newKeyId != null && typeof newKeyId == "string",
+ "RNP.genKey() should return a non null string with a key ID"
+ );
+
+ let keyObj = EnigmailKeyRing.getKeyById(newKeyId);
+ Assert.ok(
+ keyObj && keyObj.secretAvailable,
+ "EnigmailKeyRing.getKeyById should return an object with a secret key"
+ );
+
+ let fpr = keyObj.fpr;
+
+ Assert.ok(
+ keyObj.iSimpleOneSubkeySameExpiry(),
+ "check iSimpleOneSubkeySameExpiry should succeed"
+ );
+
+ let allFingerprints = [fpr, keyObj.subKeys[0].fpr];
+
+ let keyTrackers = [];
+ for (let fp of allFingerprints) {
+ let tracker = RnpPrivateKeyUnlockTracker.constructFromFingerprint(fp);
+ await tracker.unlock();
+ keyTrackers.push(tracker);
+ }
+
+ let expiryChanged = await RNP.changeExpirationDate(
+ allFingerprints,
+ 100 * 24 * 60 * 60
+ );
+ Assert.ok(expiryChanged, "changeExpirationDate should return success");
+
+ for (let t of keyTrackers) {
+ t.release();
+ }
+
+ let backupPassword = "new-password-1234";
+
+ let backupKeyBlock = await RNP.backupSecretKeys([fpr], backupPassword);
+
+ let expectedString = "END PGP PRIVATE KEY BLOCK";
+
+ Assert.ok(
+ backupKeyBlock.includes(expectedString),
+ "backup of secret key should contain the string: " + expectedString
+ );
+
+ await RNP.deleteKey(fpr, true);
+
+ EnigmailKeyRing.clearCache();
+
+ keyObj = EnigmailKeyRing.getKeyById(newKeyId);
+ Assert.ok(
+ !keyObj,
+ "after deleting the key we should be unable to find it in the keyring"
+ );
+
+ let alreadyProvidedWrongPassword = false;
+
+ let getWrongPassword = function (win, keyId, resultFlags) {
+ if (alreadyProvidedWrongPassword) {
+ resultFlags.canceled = true;
+ return "";
+ }
+
+ alreadyProvidedWrongPassword = true;
+ return "wrong-password";
+ };
+
+ let importResult = await RNP.importSecKeyBlockImpl(
+ null,
+ getWrongPassword,
+ false,
+ backupKeyBlock
+ );
+
+ Assert.ok(importResult.exitCode != 0, "import should have failed");
+
+ let getGoodPassword = function (win, keyId, resultFlags) {
+ return backupPassword;
+ };
+
+ importResult = await RNP.importSecKeyBlockImpl(
+ null,
+ getGoodPassword,
+ false,
+ backupKeyBlock
+ );
+
+ Assert.ok(importResult.exitCode == 0, "import result code should be 0");
+
+ keyObj = EnigmailKeyRing.getKeyById(newKeyId);
+
+ Assert.ok(
+ keyObj && keyObj.secretAvailable,
+ "after import, EnigmailKeyRing.getKeyById should return an object with a secret key"
+ );
+});
+
+add_task(async function testImportSecretKeyIsProtected() {
+ let carolFile = do_get_file(
+ `${keyDir}/carol@example.com-0x3099ff1238852b9f-secret.asc`
+ );
+ let carolSec = await IOUtils.readUTF8(carolFile.path);
+
+ // Carol's secret key is protected with password "x".
+ let getCarolPassword = function (win, keyId, resultFlags) {
+ return "x";
+ };
+
+ let importResult = await RNP.importSecKeyBlockImpl(
+ null,
+ getCarolPassword,
+ false,
+ carolSec
+ );
+
+ Assert.equal(
+ importResult.exitCode,
+ 0,
+ "Should be able to import Carol's secret key"
+ );
+
+ let aliceFile = do_get_file(
+ `${keyDir}/alice@openpgp.example-0xf231550c4f47e38e-secret.asc`
+ );
+ let aliceSec = await IOUtils.readUTF8(aliceFile.path);
+
+ // Alice's secret key is unprotected.
+ importResult = await RNP.importSecKeyBlockImpl(null, null, false, aliceSec);
+
+ Assert.equal(
+ importResult.exitCode,
+ 0,
+ "Should be able to import Alice's secret key"
+ );
+
+ let [prot, unprot] = OpenPGPTestUtils.getProtectedKeysCount();
+ Assert.notEqual(prot, 0, "Should have protected secret keys");
+ Assert.equal(unprot, 0, "Should not have any unprotected secret keys");
+});
+
+add_task(async function testImportOfflinePrimaryKey() {
+ let importResult = await OpenPGPTestUtils.importPrivateKey(
+ null,
+ do_get_file(`${keyDir}/ofelia-secret-subkeys.asc`)
+ );
+
+ Assert.equal(
+ importResult[0],
+ "0x97DCDA5E56EBB822",
+ "expected key id should have been reported"
+ );
+
+ let primaryKey = await RNP.findKeyByEmail("<ofelia@openpgp.example>", false);
+
+ let encSubKey = RNP.getSuitableSubkey(primaryKey, "encrypt");
+ let keyId = RNP.getKeyIDFromHandle(encSubKey);
+ Assert.equal(
+ keyId,
+ "31C31DF1DFB67601",
+ "should obtain key ID of encryption subkey"
+ );
+
+ let sigSubKey = RNP.getSuitableSubkey(primaryKey, "sign");
+ let keyIdSig = RNP.getKeyIDFromHandle(sigSubKey);
+ Assert.equal(
+ keyIdSig,
+ "1BC8F5764D348FE1",
+ "should obtain key ID of signing subkey"
+ );
+
+ // Test that we can sign with a signing subkey
+ // (this ensures that our code can unlock the secret subkey).
+ // Ofelia's key has no secret key for the primary key available,
+ // which further ensures that signing used the subkey.
+
+ let sourceText = "we-sign-this-text";
+ let signResult = {};
+
+ let signArgs = {
+ aliasKeys: new Map(),
+ armor: true,
+ bcc: [],
+ encrypt: false,
+ encryptToSender: false,
+ sender: "0x97DCDA5E56EBB822",
+ senderKeyIsExternal: false,
+ sigTypeClear: true,
+ sigTypeDetached: false,
+ sign: true,
+ signatureHash: "SHA256",
+ to: ["<alice@openpgp.example>"],
+ };
+
+ await RNP.encryptAndOrSign(sourceText, signArgs, signResult);
+
+ Assert.ok(!signResult.exitCode, "signing with subkey should work");
+});
+
+add_task(async function testSecretForPreferredSignSubkeyIsMissing() {
+ let secBlock = await IOUtils.readUTF8(
+ do_get_file(
+ `${keyDir}/secret-for-preferred-sign-subkey-is-missing--a-without-second-sub--sec.asc`
+ ).path
+ );
+
+ let cancelPassword = function (win, keyId, resultFlags) {
+ resultFlags.canceled = true;
+ return "";
+ };
+
+ let importResult = await RNP.importSecKeyBlockImpl(
+ null,
+ cancelPassword,
+ false,
+ secBlock
+ );
+
+ Assert.ok(importResult.exitCode == 0);
+
+ let pubBlock = await IOUtils.readUTF8(
+ do_get_file(
+ `${keyDir}/secret-for-preferred-sign-subkey-is-missing--b-with-second-sub--pub.asc`
+ ).path
+ );
+
+ importResult = await RNP.importPubkeyBlockAutoAcceptImpl(
+ null,
+ pubBlock,
+ null // acceptance
+ );
+
+ Assert.ok(importResult.exitCode == 0);
+
+ let primaryKey = await RNP.findKeyByEmail(
+ "<secret-for-preferred-sign-subkey-is-missing@example.com>",
+ false
+ );
+
+ let signSubKey = RNP.getSuitableSubkey(primaryKey, "sign");
+ let keyId = RNP.getKeyIDFromHandle(signSubKey);
+ Assert.equal(
+ keyId,
+ "625D4819F02EE727",
+ "should obtain key ID of older, non-preferred subkey that has the secret key available"
+ );
+});
+
+// If we an existing public key, with multiple subkeys, and then we
+// import the secret key, but one of the existing public subkeys is
+// missing, test that we don't fail to import (bug 1795698).
+add_task(async function testNoSecretForExistingPublicSubkey() {
+ let pubBlock = await IOUtils.readUTF8(
+ do_get_file(`${keyDir}/two-enc-subkeys-still-both.pub.asc`).path
+ );
+
+ let importResult = await RNP.importPubkeyBlockAutoAcceptImpl(
+ null,
+ pubBlock,
+ null // acceptance
+ );
+
+ Assert.ok(importResult.exitCode == 0);
+
+ let secBlock = await IOUtils.readUTF8(
+ do_get_file(`${keyDir}/two-enc-subkeys-one-deleted.sec.asc`).path
+ );
+
+ let cancelPassword = function (win, keyId, resultFlags) {
+ resultFlags.canceled = true;
+ return "";
+ };
+
+ importResult = await RNP.importSecKeyBlockImpl(
+ null,
+ cancelPassword,
+ false,
+ secBlock
+ );
+
+ Assert.ok(importResult.exitCode == 0);
+});
+
+add_task(async function testImportAndBackupUntweakedECCKey() {
+ const untweakedFile = do_get_file(`${keyDir}/untweaked-secret.asc`);
+ const untweakedSecKey = await IOUtils.readUTF8(untweakedFile.path);
+
+ const getGoodPasswordForTweaked = function (win, keyId, resultFlags) {
+ return "pass112233";
+ };
+
+ const importResult = await RNP.importSecKeyBlockImpl(
+ null,
+ getGoodPasswordForTweaked,
+ false,
+ untweakedSecKey
+ );
+
+ Assert.ok(importResult.exitCode == 0);
+ const fpr = "492965A6F56DAD2423B3506E849F29B0020707F7";
+
+ const backupPassword = "new-password-1234";
+ const backupKeyBlock = await RNP.backupSecretKeys([fpr], backupPassword);
+ const expectedString = "END PGP PRIVATE KEY BLOCK";
+
+ Assert.ok(
+ backupKeyBlock.includes(expectedString),
+ "backup of secret key should contain the string: " + expectedString
+ );
+
+ await RNP.deleteKey(fpr, true);
+
+ EnigmailKeyRing.clearCache();
+});
+
+// Sanity check for bug 1790610 and bug 1792450, test that our passphrase
+// reading code, which can run through repair code for corrupted profiles,
+// will not replace our existing and good data.
+// Ideally this test should restart the application, but is is difficult.
+// We simulate a restart by erasing the cache and forcing it to read
+// data again from disk (which will run the consistency checks and
+// could potentially execute the repair code).
+add_task(async function testRereadingPassphrase() {
+ let pass1 = await OpenPGPMasterpass.retrieveOpenPGPPassword();
+ OpenPGPMasterpass.cachedPassword = null;
+ let pass2 = await OpenPGPMasterpass.retrieveOpenPGPPassword();
+ Assert.equal(
+ pass1,
+ pass2,
+ "openpgp passphrase should remain the same after cache invalidation"
+ );
+});
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/test_strip.js b/comm/mail/extensions/openpgp/test/unit/rnp/test_strip.js
new file mode 100644
index 0000000000..4260921ef2
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/test_strip.js
@@ -0,0 +1,137 @@
+/* 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/. */
+
+/**
+ * Tests stripping keys.
+ */
+
+"use strict";
+
+const { RNP } = ChromeUtils.import("chrome://openpgp/content/modules/RNP.jsm");
+const { EnigmailKeyRing } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/keyRing.jsm"
+);
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+const { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+const keyDir = "../../../../../test/browser/openpgp/data/keys";
+
+/**
+ * Initialize OpenPGP add testing keys.
+ */
+add_setup(async function () {
+ do_get_profile();
+
+ await OpenPGPTestUtils.initOpenPGP();
+});
+
+add_task(async function testStripSignatures() {
+ await OpenPGPTestUtils.importPublicKey(
+ null,
+ do_get_file(`${keyDir}/heisenberg-signed-by-pinkman.asc`)
+ );
+
+ let heisenbergFpr = "8E3D32E652A254F05BEA9F66CF3EB4AFCAC29340";
+ let foundKeys = await RNP.getKeys(["0x" + heisenbergFpr]);
+
+ Assert.equal(foundKeys.length, 1);
+
+ let sigs = RNP.getKeyObjSignatures(foundKeys[0]);
+
+ // Signatures for one user ID
+ Assert.equal(sigs.length, 1);
+
+ // The key in the file has two signatures: one self signature,
+ // plus one foreign certification signature.
+ Assert.equal(sigs[0].sigList.length, 2);
+
+ let reducedKey = RNP.getMultiplePublicKeys([], ["0x" + heisenbergFpr], []);
+
+ // Delete the key we have previously imported
+ await RNP.deleteKey(heisenbergFpr);
+ foundKeys = await RNP.getKeys(["0x" + heisenbergFpr]);
+ Assert.equal(foundKeys.length, 0);
+
+ // Import the reduced key
+ let errorObj = {};
+ let fingerPrintObj = {};
+ let result = await EnigmailKeyRing.importKeyAsync(
+ null,
+ false,
+ reducedKey,
+ false,
+ null,
+ errorObj,
+ fingerPrintObj,
+ false,
+ [],
+ false
+ );
+ Assert.equal(result, 0);
+
+ foundKeys = await RNP.getKeys(["0x" + heisenbergFpr]);
+ Assert.equal(foundKeys.length, 1);
+
+ sigs = RNP.getKeyObjSignatures(foundKeys[0]);
+
+ // The imported stripped key should have only the self signature.
+ Assert.equal(sigs[0].sigList.length, 1);
+});
+
+add_task(async function testKeyWithUnicodeComment() {
+ let keyFile = do_get_file(`${keyDir}/key-with-utf8-comment.asc`);
+ let keyBlock = await IOUtils.readUTF8(keyFile.path);
+
+ let errorObj = {};
+ let fingerPrintObj = {};
+ let result = await EnigmailKeyRing.importKeyAsync(
+ null,
+ false,
+ keyBlock,
+ false,
+ null,
+ errorObj,
+ fingerPrintObj,
+ false,
+ [],
+ false
+ );
+ Assert.equal(result, 0);
+
+ let fpr = "72514F43D0060FC588E80238852C55E6D2AFD7EF";
+ let foundKeys = await RNP.getKeys(["0x" + fpr]);
+
+ Assert.equal(foundKeys.length, 1);
+});
+
+add_task(async function testBinaryKey() {
+ let keyFile = do_get_file(`${keyDir}/key-binary.gpg`);
+ let keyData = await IOUtils.read(keyFile.path);
+ let keyBlock = MailStringUtils.uint8ArrayToByteString(keyData);
+
+ let errorObj = {};
+ let fingerPrintObj = {};
+ let result = await EnigmailKeyRing.importKeyAsync(
+ null,
+ false,
+ keyBlock,
+ true,
+ null,
+ errorObj,
+ fingerPrintObj,
+ false,
+ [],
+ false
+ );
+ Assert.equal(result, 0);
+
+ let fpr = "683F775BA2E5F0ADEBB29697A2D1B914F722004E";
+ let foundKeys = await RNP.getKeys(["0x" + fpr]);
+
+ Assert.equal(foundKeys.length, 1);
+});
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/test_uid.js b/comm/mail/extensions/openpgp/test/unit/rnp/test_uid.js
new file mode 100644
index 0000000000..b87e068e0d
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/test_uid.js
@@ -0,0 +1,132 @@
+/* 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/. */
+
+/**
+ * Tests for OpenPGP encryption alias rules.
+ */
+
+"use strict";
+
+const { OpenPGPTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mozmill/OpenPGPTestUtils.jsm"
+);
+const { EnigmailFuncs } = ChromeUtils.import(
+ "chrome://openpgp/content/modules/funcs.jsm"
+);
+
+const tests = [
+ {
+ input: "Cherry Blossom (桜の花) (description) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input:
+ "Cherry Blossom (桜の花) (description) (more information) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "Last, First <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "email@example.com",
+ email: "email@example.com",
+ },
+ {
+ input: "<email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last email@example.com>",
+ email: "",
+ },
+ {
+ input: "First Last (comment) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last (a) (b) (c) (comment) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last (comment <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last )comment) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "",
+ email: "",
+ },
+ {
+ input: "First Last () <>",
+ email: "",
+ },
+ {
+ input: "First Last () <> <> <>",
+ email: "",
+ },
+ {
+ input: "First Last () <> <email1@example.com>",
+ email: "",
+ },
+ {
+ input: "First <Last> (comment) <email1@example.com>",
+ email: "",
+ },
+ {
+ input: "First Last <email@example.com> (bad comment)",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last <email@example.com> extra text",
+ email: "email@example.com",
+ },
+ {
+ input: "First Last <not-an-email> extra text",
+ email: "",
+ },
+ {
+ input: "First Last (comment (nested)) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input:
+ "First Last (comment (no second closing bracket) <email@example.com>",
+ email: "email@example.com",
+ },
+ {
+ input: "<a@example.org b@example.org>",
+ email: "",
+ },
+ {
+ input: "<a@@example.org>",
+ email: "",
+ },
+];
+
+/**
+ * Initialize OpenPGP add testing keys.
+ */
+add_setup(async function () {
+ do_get_profile();
+
+ await OpenPGPTestUtils.initOpenPGP();
+});
+
+add_task(async function testAlias() {
+ for (let test of tests) {
+ console.debug("testing input: " + test.input);
+
+ let email = EnigmailFuncs.getEmailFromUserID(test.input);
+
+ Assert.equal(test.email, email);
+ }
+});
diff --git a/comm/mail/extensions/openpgp/test/unit/rnp/xpcshell.ini b/comm/mail/extensions/openpgp/test/unit/rnp/xpcshell.ini
new file mode 100644
index 0000000000..f0601aefae
--- /dev/null
+++ b/comm/mail/extensions/openpgp/test/unit/rnp/xpcshell.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+head =
+tail =
+support-files =
+ ../../../../../test/browser/openpgp/data/keys/*
+ ../../../../../../mailnews/test/data/*
+ data/*
+
+[test_encryptAndOrSign.js]
+[test_secretKeys.js]
+[test_alias.js]
+[test_badKeys.js]
+[test_uid.js]
+[test_strip.js]