/* 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, }); }, };