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 --- .../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 + 46 files changed, 26518 insertions(+) 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 (limited to 'comm/mail/extensions/openpgp/content/modules') 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 + ); + }, +}; -- cgit v1.2.3