From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../extensions/openpgp/content/BondOpenPGP.jsm | 86 + .../openpgp/content/modules/CollectedKeysDB.jsm | 355 ++ .../extensions/openpgp/content/modules/GPGME.jsm | 338 ++ .../openpgp/content/modules/GPGMELib.jsm | 584 +++ .../openpgp/content/modules/OpenPGPAlias.jsm | 173 + .../extensions/openpgp/content/modules/RNP.jsm | 4787 ++++++++++++++++++++ .../extensions/openpgp/content/modules/RNPLib.jsm | 2109 +++++++++ .../extensions/openpgp/content/modules/armor.jsm | 367 ++ .../openpgp/content/modules/constants.jsm | 183 + .../extensions/openpgp/content/modules/core.jsm | 189 + .../openpgp/content/modules/cryptoAPI.jsm | 32 + .../content/modules/cryptoAPI/GnuPGCryptoAPI.jsm | 238 + .../content/modules/cryptoAPI/RNPCryptoAPI.jsm | 282 ++ .../openpgp/content/modules/cryptoAPI/interface.js | 288 ++ .../extensions/openpgp/content/modules/data.jsm | 156 + .../openpgp/content/modules/decryption.jsm | 639 +++ .../extensions/openpgp/content/modules/dialog.jsm | 481 ++ .../openpgp/content/modules/encryption.jsm | 564 +++ .../extensions/openpgp/content/modules/filters.jsm | 598 +++ .../openpgp/content/modules/filtersWrapper.jsm | 186 + .../openpgp/content/modules/fixExchangeMsg.jsm | 433 ++ .../extensions/openpgp/content/modules/funcs.jsm | 561 +++ .../extensions/openpgp/content/modules/key.jsm | 285 ++ .../openpgp/content/modules/keyLookupHelper.jsm | 380 ++ .../extensions/openpgp/content/modules/keyObj.jsm | 679 +++ .../extensions/openpgp/content/modules/keyRing.jsm | 2202 +++++++++ .../openpgp/content/modules/keyserver.jsm | 1549 +++++++ .../openpgp/content/modules/keyserverUris.jsm | 43 + .../extensions/openpgp/content/modules/log.jsm | 151 + .../openpgp/content/modules/masterpass.jsm | 332 ++ .../extensions/openpgp/content/modules/mime.jsm | 571 +++ .../openpgp/content/modules/mimeDecrypt.jsm | 933 ++++ .../openpgp/content/modules/mimeEncrypt.jsm | 760 ++++ .../openpgp/content/modules/mimeVerify.jsm | 716 +++ .../extensions/openpgp/content/modules/msgRead.jsm | 289 ++ .../openpgp/content/modules/persistentCrypto.jsm | 1338 ++++++ .../openpgp/content/modules/pgpmimeHandler.jsm | 299 ++ .../openpgp/content/modules/singletons.jsm | 54 + .../openpgp/content/modules/sqliteDb.jsm | 477 ++ .../extensions/openpgp/content/modules/streams.jsm | 155 + .../extensions/openpgp/content/modules/trust.jsm | 94 + .../extensions/openpgp/content/modules/uris.jsm | 124 + .../extensions/openpgp/content/modules/webKey.jsm | 293 ++ .../extensions/openpgp/content/modules/windows.jsm | 518 +++ .../openpgp/content/modules/wkdLookup.jsm | 363 ++ .../openpgp/content/modules/wksMimeHandler.jsm | 262 ++ .../extensions/openpgp/content/modules/zbase32.jsm | 108 + .../openpgp/content/strings/enigmail.properties | 348 ++ .../content/ui/attachmentItemContext.inc.xhtml | 17 + .../openpgp/content/ui/backupKeyPassword.js | 123 + .../openpgp/content/ui/backupKeyPassword.xhtml | 67 + .../openpgp/content/ui/changeExpiryDlg.js | 152 + .../openpgp/content/ui/changeExpiryDlg.xhtml | 73 + .../openpgp/content/ui/commonWorkflows.js | 194 + .../openpgp/content/ui/composeKeyStatus.js | 222 + .../openpgp/content/ui/composeKeyStatus.xhtml | 94 + .../openpgp/content/ui/confirmPubkeyImport.js | 102 + .../openpgp/content/ui/confirmPubkeyImport.xhtml | 55 + .../openpgp/content/ui/enigmailCommon.js | 69 + .../openpgp/content/ui/enigmailKeyImportInfo.js | 172 + .../openpgp/content/ui/enigmailKeyImportInfo.xhtml | 42 + .../openpgp/content/ui/enigmailKeyManager.js | 1442 ++++++ .../openpgp/content/ui/enigmailKeyManager.xhtml | 406 ++ .../openpgp/content/ui/enigmailMessengerOverlay.js | 3460 ++++++++++++++ .../openpgp/content/ui/enigmailMsgBox.js | 181 + .../openpgp/content/ui/enigmailMsgBox.xhtml | 71 + .../content/ui/enigmailMsgComposeOverlay.js | 3034 +++++++++++++ .../content/ui/enigmailMsgHdrViewOverlay.js | 1214 +++++ .../openpgp/content/ui/keyAssistant.inc.xhtml | 119 + .../extensions/openpgp/content/ui/keyAssistant.js | 956 ++++ .../extensions/openpgp/content/ui/keyDetailsDlg.js | 1119 +++++ .../openpgp/content/ui/keyDetailsDlg.xhtml | 405 ++ .../extensions/openpgp/content/ui/keyWizard.js | 1195 +++++ .../extensions/openpgp/content/ui/keyWizard.xhtml | 506 +++ .../openpgp/content/ui/oneRecipientStatus.js | 177 + .../openpgp/content/ui/oneRecipientStatus.xhtml | 86 + 76 files changed, 42705 insertions(+) create mode 100644 comm/mail/extensions/openpgp/content/BondOpenPGP.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/CollectedKeysDB.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/GPGME.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/GPGMELib.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/OpenPGPAlias.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/RNP.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/RNPLib.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/armor.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/constants.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/core.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/cryptoAPI.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/cryptoAPI/GnuPGCryptoAPI.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/cryptoAPI/RNPCryptoAPI.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/cryptoAPI/interface.js create mode 100644 comm/mail/extensions/openpgp/content/modules/data.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/decryption.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/dialog.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/encryption.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/filters.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/filtersWrapper.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/fixExchangeMsg.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/funcs.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/key.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/keyLookupHelper.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/keyObj.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/keyRing.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/keyserver.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/keyserverUris.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/log.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/masterpass.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/mime.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/mimeDecrypt.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/mimeEncrypt.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/mimeVerify.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/msgRead.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/persistentCrypto.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/pgpmimeHandler.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/singletons.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/sqliteDb.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/streams.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/trust.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/uris.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/webKey.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/windows.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/wkdLookup.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/wksMimeHandler.jsm create mode 100644 comm/mail/extensions/openpgp/content/modules/zbase32.jsm create mode 100644 comm/mail/extensions/openpgp/content/strings/enigmail.properties create mode 100644 comm/mail/extensions/openpgp/content/ui/attachmentItemContext.inc.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/backupKeyPassword.js create mode 100644 comm/mail/extensions/openpgp/content/ui/backupKeyPassword.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.js create mode 100644 comm/mail/extensions/openpgp/content/ui/changeExpiryDlg.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/commonWorkflows.js create mode 100644 comm/mail/extensions/openpgp/content/ui/composeKeyStatus.js create mode 100644 comm/mail/extensions/openpgp/content/ui/composeKeyStatus.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.js create mode 100644 comm/mail/extensions/openpgp/content/ui/confirmPubkeyImport.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailCommon.js create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.js create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailKeyImportInfo.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.js create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailKeyManager.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailMessengerOverlay.js create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.js create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailMsgBox.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js create mode 100644 comm/mail/extensions/openpgp/content/ui/enigmailMsgHdrViewOverlay.js create mode 100644 comm/mail/extensions/openpgp/content/ui/keyAssistant.inc.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/keyAssistant.js create mode 100644 comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.js create mode 100644 comm/mail/extensions/openpgp/content/ui/keyDetailsDlg.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/keyWizard.js create mode 100644 comm/mail/extensions/openpgp/content/ui/keyWizard.xhtml create mode 100644 comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.js create mode 100644 comm/mail/extensions/openpgp/content/ui/oneRecipientStatus.xhtml (limited to 'comm/mail/extensions/openpgp/content') 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} + */ + 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} + */ + 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} - 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} - 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} subkeyDates: [optional] remove subkeys with specific creation Dates + * + * @returns {Promise}: + * - 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} - 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} - 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} - 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} - 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} - 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} + */ + 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} - 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} - 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} subkeyDates: [optional] remove subkeys with specific creation Dates + * + * @returns {Promise}: + * - 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} - 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} - 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} - 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} - 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} - 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} - 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} - 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}: + * - 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} + */ + 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} - 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} - 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} - 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} - 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} - 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 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} - 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 ) + 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 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 = ""; + for (let j = 0; j < citeLevel - oldCiteLevel; j++) { + preface += '
'; + } + preface += '
\n';
+      } else if (citeLevel < oldCiteLevel) {
+        preface = "
"; + for (let j = 0; j < oldCiteLevel - citeLevel; j++) { + preface += "
"; + } + + preface += '
\n';
+      }
+
+      if (logLineStart.value > 0) {
+        preface +=
+          '' +
+          gTxtConverter.scanTXT(
+            lines[i].substr(0, logLineStart.value),
+            convFlags
+          ) +
+          "";
+      } else if (lines[i] == "-- ") {
+        preface += '
'; + isSignature = true; + } + lines[i] = + preface + + gTxtConverter.scanTXT(lines[i].substr(logLineStart.value), convFlags); + } + + var r = + '
' +
+      lines.join("\n") +
+      (isSignature ? "
" : "") + + "
"; + //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 - [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 - [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} 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 + */ + 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 + * - 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 + */ + 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 + * - 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 + */ + 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 + * - 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: - 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 + * - 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 (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, "&"); + } + + if (text.indexOf("<") > -1) { + text = text.replace(/") > -1) { + text = text.replace(/>/g, ">"); + } + + if (text.indexOf('"') > -1) { + text = text.replace(/"/g, """); + } + + 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 += '' + addr + ""; + + 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(/([),.']|>|")$/, ""); + + if (!url.length) { + continue; + } + + newText += '' + url + ""; + + 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: 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, "
\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}: 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}: 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}: 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= + +### 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. (What is Autocrypt) + +# 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 \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/. + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + +
+ +

+
+ +

+ +

+ +
+
+ +
+ +
+ +
+ +
+ +
+
+
+ + 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 @@ + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3