summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/modules/RNP.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/RNP.jsm')
-rw-r--r--comm/mail/extensions/openpgp/content/modules/RNP.jsm4787
1 files changed, 4787 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/RNP.jsm b/comm/mail/extensions/openpgp/content/modules/RNP.jsm
new file mode 100644
index 0000000000..c6969842c8
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/RNP.jsm
@@ -0,0 +1,4787 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["RNP", "RnpPrivateKeyUnlockTracker"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailConstants: "chrome://openpgp/content/modules/constants.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ GPGME: "chrome://openpgp/content/modules/GPGME.jsm",
+ OpenPGPMasterpass: "chrome://openpgp/content/modules/masterpass.jsm",
+ PgpSqliteDb2: "chrome://openpgp/content/modules/sqliteDb.jsm",
+ RNPLibLoader: "chrome://openpgp/content/modules/RNPLib.jsm",
+});
+
+var l10n = new Localization(["messenger/openpgp/openpgp.ftl"]);
+
+const str_encrypt = "encrypt";
+const str_sign = "sign";
+const str_certify = "certify";
+const str_authenticate = "authenticate";
+const RNP_PHOTO_USERID_ID = "(photo)"; // string is hardcoded inside RNP
+
+var RNPLib;
+
+/**
+ * Opens a prompt, asking the user to enter passphrase for given key id.
+
+ * @param {?nsIWindow} win - Parent window, may be null
+ * @param {string} promptString - This message will be shown to the user
+ * @param {object} resultFlags - Attribute .canceled is set to true
+ * if the user clicked cancel, other it's set to false.
+ * @returns {string} - The passphrase the user entered
+ */
+function passphrasePromptCallback(win, promptString, resultFlags) {
+ let password = { value: "" };
+ if (!Services.prompt.promptPassword(win, "", promptString, password)) {
+ resultFlags.canceled = true;
+ return "";
+ }
+
+ resultFlags.canceled = false;
+ return password.value;
+}
+
+/**
+ * Helper class to track resources related to a private/secret key,
+ * holding the key handle obtained from RNP, and offering services
+ * related to that key and its handle, including releasing the handle
+ * when done. Tracking a null handle is allowed.
+ */
+class RnpPrivateKeyUnlockTracker {
+ #rnpKeyHandle = null;
+ #wasUnlocked = false;
+ #allowPromptingUserForPassword = false;
+ #allowAutoUnlockWithCachedPasswords = false;
+ #passwordCache = null;
+ #fingerprint = "";
+ #passphraseCallback = null;
+ #rememberUnlockPasswordForUnprotect = false;
+ #unlockPassword = null;
+ #isLocked = true;
+
+ /**
+ * Initialize this object as a tracker for the private key identified
+ * by the given fingerprint. The fingerprint will be looked up in an
+ * RNP space (FFI) and the resulting handle will be tracked. The
+ * default FFI is used for performing the lookup, unless a specific
+ * FFI is given. If no key can be found, the object is initialized
+ * with a null handle. If a handle was found, the handle and any
+ * additional resources can be freed by calling the object's release()
+ * method.
+ *
+ * @param {string} fingerprint - the fingerprint of a key to look up.
+ * @param {rnp_ffi_t} ffi - An optional specific FFI.
+ * @returns {RnpPrivateKeyUnlockTracker} - a new instance, which was
+ * either initialized with a found key handle, or with null-
+ */
+ static constructFromFingerprint(fingerprint, ffi = RNPLib.ffi) {
+ if (fingerprint.startsWith("0x")) {
+ throw new Error("fingerprint must not start with 0x");
+ }
+
+ let handle = RNP.getKeyHandleByKeyIdOrFingerprint(ffi, `0x${fingerprint}`);
+
+ return new RnpPrivateKeyUnlockTracker(handle);
+ }
+
+ /**
+ * Construct this object as a tracker for the private key referenced
+ * by the given handle. The object may also be initialized
+ * with null, if no key was found. A valid handle and any additional
+ * resources can be freed by calling the object's release() method.
+ *
+ * @param {?rnp_key_handle_t} handle - the handle of a RNP key, or null
+ */
+ constructor(handle) {
+ if (this.#rnpKeyHandle) {
+ throw new Error("Instance already initialized");
+ }
+ if (!handle) {
+ return;
+ }
+ this.#rnpKeyHandle = handle;
+
+ if (!this.available()) {
+ // Not a private key. We tolerate this use to enable automatic
+ // handle releasing, for code that sometimes needs to track a
+ // secret key, and sometimes only a public key.
+ // The only functionality that is allowed on such a key is to
+ // call the .available() and the .release() methods.
+ this.#isLocked = false;
+ } else {
+ let is_locked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_locked(this.#rnpKeyHandle, is_locked.address())) {
+ throw new Error("rnp_key_is_locked failed");
+ }
+ this.#isLocked = is_locked.value;
+ }
+
+ if (!this.#fingerprint) {
+ let fingerprint = new lazy.ctypes.char.ptr();
+ if (
+ RNPLib.rnp_key_get_fprint(this.#rnpKeyHandle, fingerprint.address())
+ ) {
+ throw new Error("rnp_key_get_fprint failed");
+ }
+ this.#fingerprint = fingerprint.readString();
+ RNPLib.rnp_buffer_destroy(fingerprint);
+ }
+ }
+
+ /**
+ * @param {Function} cb - Override the callback function that this
+ * object will call to obtain the passphrase to unlock the private
+ * key for tracked key handle, if the object needs to unlock
+ * the key and prompting the user is allowed.
+ * If no alternative callback is set, the global
+ * passphrasePromptCallback function will be used.
+ */
+ setPassphraseCallback(cb) {
+ this.#passphraseCallback = cb;
+ }
+
+ /**
+ * Allow or forbid prompting the user for a passphrase.
+ *
+ * @param {boolean} isAllowed - True if allowed, false if forbidden
+ */
+ setAllowPromptingUserForPassword(isAllowed) {
+ this.#allowPromptingUserForPassword = isAllowed;
+ }
+
+ /**
+ * Allow or forbid automatically using passphrases from a configured
+ * cache of passphrase, if it's necessary to obtain a passphrase
+ * for unlocking.
+ *
+ * @param {boolean} isAllowed - True if allowed, false if forbidden
+ */
+ setAllowAutoUnlockWithCachedPasswords(isAllowed) {
+ this.#allowAutoUnlockWithCachedPasswords = isAllowed;
+ }
+
+ /**
+ * Allow or forbid this object to remember the passphrase that was
+ * successfully used to to unlock it. This is necessary when intending
+ * to subsequently call the unprotect() function to remove the key's
+ * passphrase protection. Care should be taken that a tracker object
+ * with a remembered passphrase is held in memory only for a short
+ * amount of time, and should be released as soon as a task has
+ * completed.
+ *
+ * @param {boolean} isAllowed - True if allowed, false if forbidden
+ */
+ setRememberUnlockPassword(isAllowed) {
+ this.#rememberUnlockPasswordForUnprotect = isAllowed;
+ }
+
+ /**
+ * Registers a reference to shared object that implements an optional
+ * password cache. Will be used to look up passwords if
+ * #allowAutoUnlockWithCachedPasswords is set to true. Will be used
+ * to store additional passwords that are found to unlock a key.
+ */
+ setPasswordCache(cacheObj) {
+ this.#passwordCache = cacheObj;
+ }
+
+ /**
+ * Completely remove the encryption layer that protects the private
+ * key. Requires that setRememberUnlockPassword(true) was already
+ * called on this object, prior to unlocking the key, because this
+ * code requires that the unlock/unprotect passphrase has been cached
+ * in this object already, and that the tracked key has already been
+ * unlocked.
+ */
+ unprotect() {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ const is_protected = new lazy.ctypes.bool();
+ if (
+ RNPLib.rnp_key_is_protected(this.#rnpKeyHandle, is_protected.address())
+ ) {
+ throw new Error("rnp_key_is_protected failed");
+ }
+ if (!is_protected.value) {
+ return;
+ }
+
+ if (!this.#wasUnlocked || !this.#rememberUnlockPasswordForUnprotect) {
+ // This precondition ensures we have the correct password cached.
+ throw new Error("Key should have been unlocked already.");
+ }
+
+ if (RNPLib.rnp_key_unprotect(this.#rnpKeyHandle, this.#unlockPassword)) {
+ throw new Error(`Failed to unprotect private key ${this.#fingerprint}`);
+ }
+ }
+
+ /**
+ * Attempt to unlock the tracked key with the given passphrase,
+ * can also be used with the empty string, which will unlock the key
+ * if no passphrase is set.
+ *
+ * @param {string} pass - try to unlock the key using this passphrase
+ */
+ unlockWithPassword(pass) {
+ if (!this.#rnpKeyHandle || !this.#isLocked) {
+ return;
+ }
+ this.#wasUnlocked = false;
+
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, pass)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = pass;
+ }
+ }
+ }
+
+ /**
+ * Attempt to unlock the tracked key, using the allowed unlock
+ * mechanisms that have been configured/allowed for this tracker,
+ * which must been configured as desired prior to calling this function.
+ * Attempts will potentially be made to unlock using the automatic
+ * passphrase, or using password available in the password cache,
+ * or by prompting the user for a password, repeatedly prompting
+ * until the user enters the correct password or cancels.
+ * When prompting the user for a passphrase, and the key is a subkey,
+ * it might be necessary to lookup the primary key. A RNP FFI handle
+ * is necessary for that potential lookup.
+ * Unless a ffi parameter is provided, the default ffi is used.
+ *
+ * @param {rnp_ffi_t} ffi - An optional specific FFI.
+ */
+ async unlock(ffi = RNPLib.ffi) {
+ if (!this.#rnpKeyHandle || !this.#isLocked) {
+ return;
+ }
+ this.#wasUnlocked = false;
+ let autoPassword = await lazy.OpenPGPMasterpass.retrieveOpenPGPPassword();
+
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, autoPassword)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = autoPassword;
+ }
+ return;
+ }
+
+ if (this.#allowAutoUnlockWithCachedPasswords && this.#passwordCache) {
+ for (let pw of this.#passwordCache.passwords) {
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, pw)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = pw;
+ }
+ return;
+ }
+ }
+ }
+
+ if (!this.#allowPromptingUserForPassword) {
+ return;
+ }
+
+ let promptString = await RNP.getPassphrasePrompt(this.#rnpKeyHandle, ffi);
+
+ while (true) {
+ let userFlags = { canceled: false };
+ let pass;
+ if (this.#passphraseCallback) {
+ pass = this.#passphraseCallback(null, promptString, userFlags);
+ } else {
+ pass = passphrasePromptCallback(null, promptString, userFlags);
+ }
+ if (userFlags.canceled) {
+ return;
+ }
+
+ if (!RNPLib.rnp_key_unlock(this.#rnpKeyHandle, pass)) {
+ this.#isLocked = false;
+ this.#wasUnlocked = true;
+ if (this.#rememberUnlockPasswordForUnprotect) {
+ this.#unlockPassword = pass;
+ }
+
+ if (this.#passwordCache) {
+ this.#passwordCache.passwords.push(pass);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Check that this tracker has a reference to a private key.
+ *
+ * @returns {boolean} - true if the tracked key is a secret/private
+ */
+ isSecret() {
+ return (
+ this.#rnpKeyHandle &&
+ RNPLib.getSecretAvailableFromHandle(this.#rnpKeyHandle)
+ );
+ }
+
+ /**
+ * Check that this tracker has a reference to a valid private key.
+ * The check will fail e.g. for offline secret keys, where a
+ * primary key is marked as being a secret key, but not having
+ * the raw key data available. (In that scenario, the raw key data
+ * for subkeys is usually available.)
+ *
+ * @returns {boolean} - true if the tracked key is a secret/private
+ * key with its key material available.
+ */
+ available() {
+ return (
+ this.#rnpKeyHandle &&
+ RNPLib.getSecretAvailableFromHandle(this.#rnpKeyHandle) &&
+ RNPLib.isSecretKeyMaterialAvailable(this.#rnpKeyHandle)
+ );
+ }
+
+ /**
+ * Obtain the raw RNP key handle managed by this tracker.
+ * The returned handle may be temporarily used by the caller,
+ * but the caller must not destroy the handle. The returned handle
+ * will become invalid as soon as the release() function is called
+ * on this tracker object.
+ *
+ * @returns {rnp_key_handle_t} - the handle of the tracked private key
+ * or null, if no key is tracked by this tracker.
+ */
+ getHandle() {
+ return this.#rnpKeyHandle;
+ }
+
+ /**
+ * @returns {string} - key fingerprint of the tracked key, or the
+ * empty string.
+ */
+ getFingerprint() {
+ return this.#fingerprint;
+ }
+
+ /**
+ * @returns {boolean} - true if the tracked key is currently unlocked.
+ */
+ isUnlocked() {
+ return !this.#isLocked;
+ }
+
+ /**
+ * Protect the key with the automatic passphrase mechanism, that is,
+ * using the classic mechanism that uses an automatically generated
+ * passphrase, which is either unprotected, or protected by the
+ * primary password.
+ * Requires that the key is unlocked already.
+ */
+ async setAutoPassphrase() {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ let autoPassword = await lazy.OpenPGPMasterpass.retrieveOpenPGPPassword();
+ if (
+ RNPLib.rnp_key_protect(
+ this.#rnpKeyHandle,
+ autoPassword,
+ null,
+ null,
+ null,
+ 0
+ )
+ ) {
+ throw new Error(`rnp_key_protect failed for ${this.#fingerprint}`);
+ }
+ }
+
+ /**
+ * Protect the key with the given passphrase.
+ * Requires that the key is unlocked already.
+ *
+ * @param {string} pass - protect the key with this passphrase
+ */
+ setPassphrase(pass) {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ if (RNPLib.rnp_key_protect(this.#rnpKeyHandle, pass, null, null, null, 0)) {
+ throw new Error(`rnp_key_protect failed for ${this.#fingerprint}`);
+ }
+ }
+
+ /**
+ * Release all data managed by this tracker, if necessary locking the
+ * tracked private key, forgetting the remembered unlock password,
+ * and destroying the handle.
+ * Note that data passed on to a password cache isn't released.
+ */
+ release() {
+ if (!this.#rnpKeyHandle) {
+ return;
+ }
+
+ this.#unlockPassword = null;
+ if (!this.#isLocked && this.#wasUnlocked) {
+ RNPLib.rnp_key_lock(this.#rnpKeyHandle);
+ this.#isLocked = true;
+ }
+
+ RNPLib.rnp_key_handle_destroy(this.#rnpKeyHandle);
+ this.#rnpKeyHandle = null;
+ }
+}
+
+var RNP = {
+ hasRan: false,
+ libLoaded: false,
+ async once() {
+ this.hasRan = true;
+ try {
+ RNPLib = lazy.RNPLibLoader.init();
+ if (!RNPLib || !RNPLib.loaded) {
+ return;
+ }
+ if (await RNPLib.init()) {
+ //this.initUiOps();
+ RNP.libLoaded = true;
+ }
+ await lazy.OpenPGPMasterpass.ensurePasswordIsCached();
+ } catch (e) {
+ console.log(e);
+ }
+ },
+
+ getRNPLibStatus() {
+ return RNPLib.getRNPLibStatus();
+ },
+
+ async init(opts) {
+ opts = opts || {};
+
+ if (!this.hasRan) {
+ await this.once();
+ }
+
+ return RNP.libLoaded;
+ },
+
+ isAllowedPublicKeyAlgo(algo) {
+ // see rnp/src/lib/rnp.cpp pubkey_alg_map
+ switch (algo) {
+ case "SM2":
+ return false;
+
+ default:
+ return true;
+ }
+ },
+
+ /**
+ * returns {integer} - the raw value of the key's creation date
+ */
+ getKeyCreatedValueFromHandle(handle) {
+ let key_creation = new lazy.ctypes.uint32_t();
+ if (RNPLib.rnp_key_get_creation(handle, key_creation.address())) {
+ throw new Error("rnp_key_get_creation failed");
+ }
+ return key_creation.value;
+ },
+
+ addKeyAttributes(handle, meta, keyObj, is_subkey, forListing) {
+ let algo = new lazy.ctypes.char.ptr();
+ let bits = new lazy.ctypes.uint32_t();
+ let key_expiration = new lazy.ctypes.uint32_t();
+ let allowed = new lazy.ctypes.bool();
+
+ keyObj.secretAvailable = this.getSecretAvailableFromHandle(handle);
+
+ if (keyObj.secretAvailable) {
+ keyObj.secretMaterial = RNPLib.isSecretKeyMaterialAvailable(handle);
+ } else {
+ keyObj.secretMaterial = false;
+ }
+
+ if (is_subkey) {
+ keyObj.type = "sub";
+ } else {
+ keyObj.type = "pub";
+ }
+
+ keyObj.keyId = this.getKeyIDFromHandle(handle);
+ if (forListing) {
+ keyObj.id = keyObj.keyId;
+ }
+
+ keyObj.fpr = this.getFingerprintFromHandle(handle);
+
+ if (RNPLib.rnp_key_get_alg(handle, algo.address())) {
+ throw new Error("rnp_key_get_alg failed");
+ }
+ keyObj.algoSym = algo.readString();
+ RNPLib.rnp_buffer_destroy(algo);
+
+ if (RNPLib.rnp_key_get_bits(handle, bits.address())) {
+ throw new Error("rnp_key_get_bits failed");
+ }
+ keyObj.keySize = bits.value;
+
+ keyObj.keyCreated = this.getKeyCreatedValueFromHandle(handle);
+ keyObj.created = new Services.intl.DateTimeFormat().format(
+ new Date(keyObj.keyCreated * 1000)
+ );
+
+ if (RNPLib.rnp_key_get_expiration(handle, key_expiration.address())) {
+ throw new Error("rnp_key_get_expiration failed");
+ }
+ if (key_expiration.value > 0) {
+ keyObj.expiryTime = keyObj.keyCreated + key_expiration.value;
+ } else {
+ keyObj.expiryTime = 0;
+ }
+ keyObj.expiry = keyObj.expiryTime
+ ? new Services.intl.DateTimeFormat().format(
+ new Date(keyObj.expiryTime * 1000)
+ )
+ : "";
+ keyObj.keyUseFor = "";
+
+ if (!this.isAllowedPublicKeyAlgo(keyObj.algoSym)) {
+ return;
+ }
+
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+
+ if (key_revoked.value) {
+ keyObj.keyTrust = "r";
+ if (forListing) {
+ keyObj.revoke = true;
+ }
+ } else if (this.isExpiredTime(keyObj.expiryTime)) {
+ keyObj.keyTrust = "e";
+ } else if (keyObj.secretAvailable) {
+ keyObj.keyTrust = "u";
+ } else {
+ keyObj.keyTrust = "o";
+ }
+
+ if (RNPLib.rnp_key_allows_usage(handle, str_encrypt, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "e";
+ meta.e = true;
+ }
+ if (RNPLib.rnp_key_allows_usage(handle, str_sign, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "s";
+ meta.s = true;
+ }
+ if (RNPLib.rnp_key_allows_usage(handle, str_certify, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "c";
+ meta.c = true;
+ }
+ if (
+ RNPLib.rnp_key_allows_usage(handle, str_authenticate, allowed.address())
+ ) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (allowed.value) {
+ keyObj.keyUseFor += "a";
+ meta.a = true;
+ }
+ },
+
+ async getKeys(onlyKeys = null) {
+ return this.getKeysFromFFI(RNPLib.ffi, false, onlyKeys, false);
+ },
+
+ async getSecretKeys(onlyKeys = null) {
+ return this.getKeysFromFFI(RNPLib.ffi, false, onlyKeys, true);
+ },
+
+ getProtectedKeysCount() {
+ return RNPLib.getProtectedKeysCount();
+ },
+
+ async protectUnprotectedKeys() {
+ return RNPLib.protectUnprotectedKeys();
+ },
+
+ /**
+ * This function inspects the keys contained in the RNP space "ffi",
+ * and returns objects of type KeyObj that describe the keys.
+ *
+ * Some consumers want a different listing of keys, and expect
+ * slightly different attribute names.
+ * If forListing is true, we'll set those additional attributes.
+ * If onlyKeys is given: only returns keys in that array.
+ *
+ * @param {rnp_ffi_t} ffi - RNP library handle to key storage area
+ * @param {boolean} forListing - Request additional attributes
+ * in the returned objects, for backwards compatibility.
+ * @param {string[]} onlyKeys - An array of key IDs or fingerprints.
+ * If non-null, only the given elements will be returned.
+ * If null, all elements are returned.
+ * @param {boolean} onlySecret - If true, only information for
+ * available secret keys is returned.
+ * @param {boolean} withPubKey - If true, an additional attribute
+ * "pubKey" will be added to each returned KeyObj, which will
+ * contain an ascii armor copy of the public key.
+ * @returns {KeyObj[]} - An array of KeyObj objects that describe the
+ * available keys.
+ */
+ async getKeysFromFFI(
+ ffi,
+ forListing,
+ onlyKeys = null,
+ onlySecret = false,
+ withPubKey = false
+ ) {
+ if (!!onlyKeys && onlySecret) {
+ throw new Error(
+ "filtering by both white list and only secret keys isn't supported"
+ );
+ }
+
+ let keys = [];
+
+ if (onlyKeys) {
+ for (let ki = 0; ki < onlyKeys.length; ki++) {
+ let handle = await this.getKeyHandleByIdentifier(ffi, onlyKeys[ki]);
+ if (!handle || handle.isNull()) {
+ continue;
+ }
+
+ let keyObj = {};
+ try {
+ // Skip if it is a sub key, it will be processed together with primary key later.
+ let ok = this.getKeyInfoFromHandle(
+ ffi,
+ handle,
+ keyObj,
+ false,
+ forListing,
+ false
+ );
+ if (!ok) {
+ continue;
+ }
+ } catch (ex) {
+ console.log(ex);
+ } finally {
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ if (keyObj) {
+ if (withPubKey) {
+ let pubKey = await this.getPublicKey("0x" + keyObj.id, ffi);
+ if (pubKey) {
+ keyObj.pubKey = pubKey;
+ }
+ }
+ keys.push(keyObj);
+ }
+ }
+ } else {
+ let rv;
+
+ let iter = new RNPLib.rnp_identifier_iterator_t();
+ let grip = new lazy.ctypes.char.ptr();
+
+ rv = RNPLib.rnp_identifier_iterator_create(ffi, iter.address(), "grip");
+ if (rv) {
+ return null;
+ }
+
+ while (!RNPLib.rnp_identifier_iterator_next(iter, grip.address())) {
+ if (grip.isNull()) {
+ break;
+ }
+
+ let handle = new RNPLib.rnp_key_handle_t();
+
+ if (RNPLib.rnp_locate_key(ffi, "grip", grip, handle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+
+ let keyObj = {};
+ try {
+ if (RNP.isBadKey(handle, null, ffi)) {
+ continue;
+ }
+
+ // Skip if it is a sub key, it will be processed together with primary key later.
+ if (
+ !this.getKeyInfoFromHandle(
+ ffi,
+ handle,
+ keyObj,
+ false,
+ forListing,
+ onlySecret
+ )
+ ) {
+ continue;
+ }
+ } catch (ex) {
+ console.log(ex);
+ } finally {
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ if (keyObj) {
+ if (withPubKey) {
+ let pubKey = await this.getPublicKey("0x" + keyObj.id, ffi);
+ if (pubKey) {
+ keyObj.pubKey = pubKey;
+ }
+ }
+ keys.push(keyObj);
+ }
+ }
+ RNPLib.rnp_identifier_iterator_destroy(iter);
+ }
+ return keys;
+ },
+
+ getFingerprintFromHandle(handle) {
+ let fingerprint = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_fprint(handle, fingerprint.address())) {
+ throw new Error("rnp_key_get_fprint failed");
+ }
+ let result = fingerprint.readString();
+ RNPLib.rnp_buffer_destroy(fingerprint);
+ return result;
+ },
+
+ getKeyIDFromHandle(handle) {
+ let ctypes_key_id = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_keyid(handle, ctypes_key_id.address())) {
+ throw new Error("rnp_key_get_keyid failed");
+ }
+ let result = ctypes_key_id.readString();
+ RNPLib.rnp_buffer_destroy(ctypes_key_id);
+ return result;
+ },
+
+ getSecretAvailableFromHandle(handle) {
+ return RNPLib.getSecretAvailableFromHandle(handle);
+ },
+
+ // We already know sub_handle is a subkey
+ getPrimaryKeyHandleFromSub(ffi, sub_handle) {
+ let newHandle = new RNPLib.rnp_key_handle_t();
+ // test my expectation is correct
+ if (!newHandle.isNull()) {
+ throw new Error("unexpected, new handle isn't null");
+ }
+ let primary_grip = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_primary_grip(sub_handle, primary_grip.address())) {
+ throw new Error("rnp_key_get_primary_grip failed");
+ }
+ if (primary_grip.isNull()) {
+ return newHandle;
+ }
+ if (RNPLib.rnp_locate_key(ffi, "grip", primary_grip, newHandle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+ return newHandle;
+ },
+
+ // We don't know if handle is a subkey. If it's not, return null handle
+ getPrimaryKeyHandleIfSub(ffi, handle) {
+ let is_subkey = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_sub(handle, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (!is_subkey.value) {
+ let nullHandle = new RNPLib.rnp_key_handle_t();
+ // test my expectation is correct
+ if (!nullHandle.isNull()) {
+ throw new Error("unexpected, new handle isn't null");
+ }
+ return nullHandle;
+ }
+ return this.getPrimaryKeyHandleFromSub(ffi, handle);
+ },
+
+ hasKeyWeakSelfSignature(selfId, handle) {
+ let sig_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_signature_count(handle, sig_count.address())) {
+ throw new Error("rnp_key_get_signature_count failed");
+ }
+
+ let hasWeak = false;
+ for (let i = 0; !hasWeak && i < sig_count.value; i++) {
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+
+ if (RNPLib.rnp_key_get_signature_at(handle, i, sig_handle.address())) {
+ throw new Error("rnp_key_get_signature_at failed");
+ }
+
+ hasWeak = RNP.isWeakSelfSignature(selfId, sig_handle);
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+ }
+ return hasWeak;
+ },
+
+ isWeakSelfSignature(selfId, sig_handle) {
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+
+ let sigId = sig_id_str.readString();
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+
+ // Is it a self-signature?
+ if (sigId != selfId) {
+ return false;
+ }
+
+ let creation = new lazy.ctypes.uint32_t();
+ if (RNPLib.rnp_signature_get_creation(sig_handle, creation.address())) {
+ throw new Error("rnp_signature_get_creation failed");
+ }
+
+ let hash_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_hash_alg(sig_handle, hash_str.address())) {
+ throw new Error("rnp_signature_get_hash_alg failed");
+ }
+
+ let creation64 = new lazy.ctypes.uint64_t();
+ creation64.value = creation.value;
+
+ let level = new lazy.ctypes.uint32_t();
+
+ if (
+ RNPLib.rnp_get_security_rule(
+ RNPLib.ffi,
+ RNPLib.RNP_FEATURE_HASH_ALG,
+ hash_str,
+ creation64,
+ null,
+ null,
+ level.address()
+ )
+ ) {
+ throw new Error("rnp_get_security_rule failed");
+ }
+
+ RNPLib.rnp_buffer_destroy(hash_str);
+ return level.value < RNPLib.RNP_SECURITY_DEFAULT;
+ },
+
+ // return false if handle refers to subkey and should be ignored
+ getKeyInfoFromHandle(
+ ffi,
+ handle,
+ keyObj,
+ usePrimaryIfSubkey,
+ forListing,
+ onlyIfSecret
+ ) {
+ keyObj.ownerTrust = null;
+ keyObj.userId = null;
+ keyObj.userIds = [];
+ keyObj.subKeys = [];
+ keyObj.photoAvailable = false;
+ keyObj.hasIgnoredAttributes = false;
+
+ let is_subkey = new lazy.ctypes.bool();
+ let sub_count = new lazy.ctypes.size_t();
+ let uid_count = new lazy.ctypes.size_t();
+
+ if (RNPLib.rnp_key_is_sub(handle, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (is_subkey.value) {
+ if (!usePrimaryIfSubkey) {
+ return false;
+ }
+ let rv = false;
+ let newHandle = this.getPrimaryKeyHandleFromSub(ffi, handle);
+ if (!newHandle.isNull()) {
+ // recursively call ourselves to get primary key info
+ rv = this.getKeyInfoFromHandle(
+ ffi,
+ newHandle,
+ keyObj,
+ false,
+ forListing,
+ onlyIfSecret
+ );
+ RNPLib.rnp_key_handle_destroy(newHandle);
+ }
+ return rv;
+ }
+
+ if (onlyIfSecret) {
+ let have_secret = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_have_secret(handle, have_secret.address())) {
+ throw new Error("rnp_key_have_secret failed");
+ }
+ if (!have_secret.value) {
+ return false;
+ }
+ }
+
+ let meta = {
+ a: false,
+ s: false,
+ c: false,
+ e: false,
+ };
+ this.addKeyAttributes(handle, meta, keyObj, false, forListing);
+
+ let hasAnySecretKey = keyObj.secretAvailable;
+
+ /* The remaining actions are done for primary keys, only. */
+ if (!is_subkey.value) {
+ if (RNPLib.rnp_key_get_uid_count(handle, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+ let firstValidUid = null;
+ for (let i = 0; i < uid_count.value; i++) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+
+ if (RNPLib.rnp_key_get_uid_handle_at(handle, i, uid_handle.address())) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+
+ // Never allow revoked user IDs
+ let uidOkToUse = !this.isRevokedUid(uid_handle);
+ if (uidOkToUse) {
+ // Usually, we don't allow user IDs reported as not valid
+ uidOkToUse = !this.isBadUid(uid_handle);
+
+ let { hasGoodSignature, hasWeakSignature } =
+ this.getUidSignatureQuality(keyObj.keyId, uid_handle);
+
+ if (hasWeakSignature) {
+ keyObj.hasIgnoredAttributes = true;
+ }
+
+ if (!uidOkToUse && keyObj.keyTrust == "e") {
+ // However, a user might be not valid, because it has
+ // expired. If the primary key has expired, we should show
+ // some user ID, even if all user IDs have expired,
+ // otherwise the user cannot see any text description.
+ // We allow showing user IDs with a good self-signature.
+ uidOkToUse = hasGoodSignature;
+ }
+ }
+
+ if (uidOkToUse) {
+ let uid_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_uid_at(handle, i, uid_str.address())) {
+ throw new Error("rnp_key_get_uid_at failed");
+ }
+ let userIdStr = uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(uid_str);
+
+ if (userIdStr !== RNP_PHOTO_USERID_ID) {
+ if (!firstValidUid) {
+ firstValidUid = userIdStr;
+ }
+
+ if (!keyObj.userId && this.isPrimaryUid(uid_handle)) {
+ keyObj.userId = userIdStr;
+ }
+
+ let uidObj = {};
+ uidObj.userId = userIdStr;
+ uidObj.type = "uid";
+ uidObj.keyTrust = keyObj.keyTrust;
+ uidObj.uidFpr = "??fpr??";
+
+ keyObj.userIds.push(uidObj);
+ }
+ }
+
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+
+ if (!keyObj.userId && firstValidUid) {
+ // No user ID marked as primary, so let's use the first valid.
+ keyObj.userId = firstValidUid;
+ }
+
+ if (!keyObj.userId) {
+ keyObj.userId = "?";
+ }
+
+ if (forListing) {
+ keyObj.name = keyObj.userId;
+ }
+
+ if (RNPLib.rnp_key_get_subkey_count(handle, sub_count.address())) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_key_get_subkey_at(handle, i, sub_handle.address())) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ if (RNP.hasKeyWeakSelfSignature(keyObj.keyId, sub_handle)) {
+ keyObj.hasIgnoredAttributes = true;
+ }
+
+ if (!RNP.isBadKey(sub_handle, handle, null)) {
+ let subKeyObj = {};
+ this.addKeyAttributes(sub_handle, meta, subKeyObj, true, forListing);
+ keyObj.subKeys.push(subKeyObj);
+ hasAnySecretKey = hasAnySecretKey || subKeyObj.secretAvailable;
+ }
+
+ RNPLib.rnp_key_handle_destroy(sub_handle);
+ }
+
+ let haveNonExpiringEncryptionKey = false;
+ let haveNonExpiringSigningKey = false;
+
+ let effectiveEncryptionExpiry = keyObj.expiry;
+ let effectiveSigningExpiry = keyObj.expiry;
+ let effectiveEncryptionExpiryTime = keyObj.expiryTime;
+ let effectiveSigningExpiryTime = keyObj.expiryTime;
+
+ if (keyObj.keyUseFor.match(/e/) && !keyObj.expiryTime) {
+ haveNonExpiringEncryptionKey = true;
+ }
+
+ if (keyObj.keyUseFor.match(/s/) && !keyObj.expiryTime) {
+ haveNonExpiringSigningKey = true;
+ }
+
+ let mostFutureEncExpiryTime = 0;
+ let mostFutureSigExpiryTime = 0;
+ let mostFutureEncExpiry = "";
+ let mostFutureSigExpiry = "";
+
+ for (let aSub of keyObj.subKeys) {
+ if (aSub.keyTrust == "r") {
+ continue;
+ }
+
+ // Expiring subkeys may shorten the effective expiry,
+ // unless the primary key is non-expiring and can be used
+ // for a purpose.
+ // Subkeys cannot extend the expiry beyond the primary key's.
+
+ // Strategy: If we don't have a non-expiring usable primary key,
+ // then find the usable subkey that has the most future
+ // expiration date. Stop searching is a non-expiring subkey
+ // is found. Then compare with primary key expiry.
+
+ if (!haveNonExpiringEncryptionKey && aSub.keyUseFor.match(/e/)) {
+ if (!aSub.expiryTime) {
+ haveNonExpiringEncryptionKey = true;
+ } else if (!mostFutureEncExpiryTime) {
+ mostFutureEncExpiryTime = aSub.expiryTime;
+ mostFutureEncExpiry = aSub.expiry;
+ } else if (aSub.expiryTime > mostFutureEncExpiryTime) {
+ mostFutureEncExpiryTime = aSub.expiryTime;
+ mostFutureEncExpiry = aSub.expiry;
+ }
+ }
+
+ // We only need to calculate the effective signing expiration
+ // if it's about a personal key (we require both signing and
+ // encryption capability).
+ if (
+ hasAnySecretKey &&
+ !haveNonExpiringSigningKey &&
+ aSub.keyUseFor.match(/s/)
+ ) {
+ if (!aSub.expiryTime) {
+ haveNonExpiringSigningKey = true;
+ } else if (!mostFutureSigExpiryTime) {
+ mostFutureSigExpiryTime = aSub.expiryTime;
+ mostFutureSigExpiry = aSub.expiry;
+ } else if (aSub.expiryTime > mostFutureEncExpiryTime) {
+ mostFutureSigExpiryTime = aSub.expiryTime;
+ mostFutureSigExpiry = aSub.expiry;
+ }
+ }
+ }
+
+ if (
+ !haveNonExpiringEncryptionKey &&
+ mostFutureEncExpiryTime &&
+ (!keyObj.expiryTime || mostFutureEncExpiryTime < keyObj.expiryTime)
+ ) {
+ effectiveEncryptionExpiryTime = mostFutureEncExpiryTime;
+ effectiveEncryptionExpiry = mostFutureEncExpiry;
+ }
+
+ if (
+ !haveNonExpiringSigningKey &&
+ mostFutureSigExpiryTime &&
+ (!keyObj.expiryTime || mostFutureSigExpiryTime < keyObj.expiryTime)
+ ) {
+ effectiveSigningExpiryTime = mostFutureSigExpiryTime;
+ effectiveSigningExpiry = mostFutureSigExpiry;
+ }
+
+ if (!hasAnySecretKey) {
+ keyObj.effectiveExpiryTime = effectiveEncryptionExpiryTime;
+ keyObj.effectiveExpiry = effectiveEncryptionExpiry;
+ } else {
+ let effectiveSignOrEncExpiry = "";
+ let effectiveSignOrEncExpiryTime = 0;
+
+ if (!effectiveEncryptionExpiryTime) {
+ if (effectiveSigningExpiryTime) {
+ effectiveSignOrEncExpiryTime = effectiveSigningExpiryTime;
+ effectiveSignOrEncExpiry = effectiveSigningExpiry;
+ }
+ } else if (!effectiveSigningExpiryTime) {
+ effectiveSignOrEncExpiryTime = effectiveEncryptionExpiryTime;
+ effectiveSignOrEncExpiry = effectiveEncryptionExpiry;
+ } else if (effectiveSigningExpiryTime < effectiveEncryptionExpiryTime) {
+ effectiveSignOrEncExpiryTime = effectiveSigningExpiryTime;
+ effectiveSignOrEncExpiry = effectiveSigningExpiry;
+ } else {
+ effectiveSignOrEncExpiryTime = effectiveEncryptionExpiryTime;
+ effectiveSignOrEncExpiry = effectiveEncryptionExpiry;
+ }
+
+ keyObj.effectiveExpiryTime = effectiveSignOrEncExpiryTime;
+ keyObj.effectiveExpiry = effectiveSignOrEncExpiry;
+ }
+
+ if (meta.s) {
+ keyObj.keyUseFor += "S";
+ }
+ if (meta.a) {
+ keyObj.keyUseFor += "A";
+ }
+ if (meta.c) {
+ keyObj.keyUseFor += "C";
+ }
+ if (meta.e) {
+ keyObj.keyUseFor += "E";
+ }
+
+ if (RNP.hasKeyWeakSelfSignature(keyObj.keyId, handle)) {
+ keyObj.hasIgnoredAttributes = true;
+ }
+ }
+
+ return true;
+ },
+
+ /*
+ // We don't need these functions currently, but it's helpful
+ // information that I'd like to keep around as documentation.
+
+ isUInt64WithinBounds(val) {
+ // JS integers are limited to 53 bits precision.
+ // Numbers smaller than 2^53 -1 are safe to use.
+ // (For comparison, that's 8192 TB or 8388608 GB).
+ const num53BitsMinus1 = ctypes.UInt64("0x1fffffffffffff");
+ return ctypes.UInt64.compare(val, num53BitsMinus1) < 0;
+ },
+
+ isUInt64Max(val) {
+ // 2^64-1, 18446744073709551615
+ const max = ctypes.UInt64("0xffffffffffffffff");
+ return ctypes.UInt64.compare(val, max) == 0;
+ },
+ */
+
+ isBadKey(handle, knownPrimaryKey, knownContextFFI) {
+ let validTill64 = new lazy.ctypes.uint64_t();
+ if (RNPLib.rnp_key_valid_till64(handle, validTill64.address())) {
+ throw new Error("rnp_key_valid_till64 failed");
+ }
+
+ // For the purpose of this function, we define bad as: there isn't
+ // any valid self-signature on the key, and thus the key should
+ // be completely avoided.
+ // In this scenario, zero is returned. In other words,
+ // if a non-zero value is returned, we know the key isn't completely
+ // bad according to our definition.
+
+ // ctypes.uint64_t().value is of type ctypes.UInt64
+
+ if (
+ lazy.ctypes.UInt64.compare(validTill64.value, lazy.ctypes.UInt64("0")) > 0
+ ) {
+ return false;
+ }
+
+ // If zero was returned, it could potentially have been revoked.
+ // If it was revoked, we don't treat is as generally bad,
+ // to allow importing it and to consume the revocation information.
+ // If the key was not revoked, then treat it as a bad key.
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+
+ if (!key_revoked.value) {
+ // Also check if the primary key was revoked. If the primary key
+ // is revoked, the subkey is considered revoked, too.
+ if (knownPrimaryKey) {
+ if (RNPLib.rnp_key_is_revoked(knownPrimaryKey, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+ } else if (knownContextFFI) {
+ let primaryHandle = this.getPrimaryKeyHandleIfSub(
+ knownContextFFI,
+ handle
+ );
+ if (!primaryHandle.isNull()) {
+ if (RNPLib.rnp_key_is_revoked(primaryHandle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+ RNPLib.rnp_key_handle_destroy(primaryHandle);
+ }
+ }
+ }
+
+ return !key_revoked.value;
+ },
+
+ isPrimaryUid(uid_handle) {
+ let is_primary = new lazy.ctypes.bool();
+
+ if (RNPLib.rnp_uid_is_primary(uid_handle, is_primary.address())) {
+ throw new Error("rnp_uid_is_primary failed");
+ }
+
+ return is_primary.value;
+ },
+
+ getUidSignatureQuality(self_key_id, uid_handle) {
+ let result = {
+ hasGoodSignature: false,
+ hasWeakSignature: false,
+ };
+
+ let sig_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_uid_get_signature_count(uid_handle, sig_count.address())) {
+ throw new Error("rnp_uid_get_signature_count failed");
+ }
+
+ for (let i = 0; i < sig_count.value; i++) {
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+
+ if (
+ RNPLib.rnp_uid_get_signature_at(uid_handle, i, sig_handle.address())
+ ) {
+ throw new Error("rnp_uid_get_signature_at failed");
+ }
+
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+
+ if (sig_id_str.readString() == self_key_id) {
+ if (!result.hasGoodSignature) {
+ let sig_validity = RNPLib.rnp_signature_is_valid(sig_handle, 0);
+ result.hasGoodSignature =
+ sig_validity == RNPLib.RNP_SUCCESS ||
+ sig_validity == RNPLib.RNP_ERROR_SIGNATURE_EXPIRED;
+ }
+
+ if (!result.hasWeakSignature) {
+ result.hasWeakSignature = RNP.isWeakSelfSignature(
+ self_key_id,
+ sig_handle
+ );
+ }
+ }
+
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+ }
+
+ return result;
+ },
+
+ isBadUid(uid_handle) {
+ let is_valid = new lazy.ctypes.bool();
+
+ if (RNPLib.rnp_uid_is_valid(uid_handle, is_valid.address())) {
+ throw new Error("rnp_uid_is_valid failed");
+ }
+
+ return !is_valid.value;
+ },
+
+ isRevokedUid(uid_handle) {
+ let is_revoked = new lazy.ctypes.bool();
+
+ if (RNPLib.rnp_uid_is_revoked(uid_handle, is_revoked.address())) {
+ throw new Error("rnp_uid_is_revoked failed");
+ }
+
+ return is_revoked.value;
+ },
+
+ getKeySignatures(keyId, ignoreUnknownUid) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + keyId
+ );
+ if (handle.isNull()) {
+ return null;
+ }
+
+ let mainKeyObj = {};
+ this.getKeyInfoFromHandle(
+ RNPLib.ffi,
+ handle,
+ mainKeyObj,
+ false,
+ true,
+ false
+ );
+
+ let result = RNP._getSignatures(mainKeyObj, handle, ignoreUnknownUid);
+ RNPLib.rnp_key_handle_destroy(handle);
+ return result;
+ },
+
+ getKeyObjSignatures(keyObj, ignoreUnknownUid) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + keyObj.keyId
+ );
+ if (handle.isNull()) {
+ return null;
+ }
+
+ let result = RNP._getSignatures(keyObj, handle, ignoreUnknownUid);
+ RNPLib.rnp_key_handle_destroy(handle);
+ return result;
+ },
+
+ _getSignatures(keyObj, handle, ignoreUnknownUid) {
+ let rList = [];
+
+ try {
+ let uid_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_uid_count(handle, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+ let outputIndex = 0;
+ for (let i = 0; i < uid_count.value; i++) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+
+ if (RNPLib.rnp_key_get_uid_handle_at(handle, i, uid_handle.address())) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+
+ if (!this.isBadUid(uid_handle) && !this.isRevokedUid(uid_handle)) {
+ let uid_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_uid_at(handle, i, uid_str.address())) {
+ throw new Error("rnp_key_get_uid_at failed");
+ }
+ let userIdStr = uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(uid_str);
+
+ if (userIdStr !== RNP_PHOTO_USERID_ID) {
+ let id = outputIndex;
+ ++outputIndex;
+
+ let subList = {};
+
+ subList = {};
+ subList.keyCreated = keyObj.keyCreated;
+ subList.created = keyObj.created;
+ subList.fpr = keyObj.fpr;
+ subList.keyId = keyObj.keyId;
+
+ subList.userId = userIdStr;
+ subList.sigList = [];
+
+ let sig_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_uid_get_signature_count(
+ uid_handle,
+ sig_count.address()
+ )
+ ) {
+ throw new Error("rnp_uid_get_signature_count failed");
+ }
+ for (let j = 0; j < sig_count.value; j++) {
+ let sigObj = {};
+
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+ if (
+ RNPLib.rnp_uid_get_signature_at(
+ uid_handle,
+ j,
+ sig_handle.address()
+ )
+ ) {
+ throw new Error("rnp_uid_get_signature_at failed");
+ }
+
+ let creation = new lazy.ctypes.uint32_t();
+ if (
+ RNPLib.rnp_signature_get_creation(
+ sig_handle,
+ creation.address()
+ )
+ ) {
+ throw new Error("rnp_signature_get_creation failed");
+ }
+ sigObj.keyCreated = creation.value;
+ sigObj.created = new Services.intl.DateTimeFormat().format(
+ new Date(sigObj.keyCreated * 1000)
+ );
+ sigObj.sigType = "?";
+
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (
+ RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())
+ ) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+
+ let sigIdStr = sig_id_str.readString();
+ sigObj.signerKeyId = sigIdStr;
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+
+ let signerHandle = new RNPLib.rnp_key_handle_t();
+
+ if (
+ RNPLib.rnp_signature_get_signer(
+ sig_handle,
+ signerHandle.address()
+ )
+ ) {
+ throw new Error("rnp_signature_get_signer failed");
+ }
+
+ if (
+ signerHandle.isNull() ||
+ this.isBadKey(signerHandle, null, RNPLib.ffi)
+ ) {
+ if (!ignoreUnknownUid) {
+ sigObj.userId = "?";
+ sigObj.sigKnown = false;
+ subList.sigList.push(sigObj);
+ }
+ } else {
+ let signer_uid_str = new lazy.ctypes.char.ptr();
+ if (
+ RNPLib.rnp_key_get_primary_uid(
+ signerHandle,
+ signer_uid_str.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_primary_uid failed");
+ }
+ sigObj.userId = signer_uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(signer_uid_str);
+ sigObj.sigKnown = true;
+ subList.sigList.push(sigObj);
+ RNPLib.rnp_key_handle_destroy(signerHandle);
+ }
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+ }
+ rList[id] = subList;
+ }
+ }
+
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+ } catch (ex) {
+ console.log(ex);
+ }
+ return rList;
+ },
+
+ policyForbidsAlg(alg) {
+ // TODO: implement policy
+ // Currently, all algorithms are allowed
+ return false;
+ },
+
+ getKeyIdsFromRecipHandle(recip_handle, resultRecipAndPrimary) {
+ resultRecipAndPrimary.keyId = "";
+ resultRecipAndPrimary.primaryKeyId = "";
+
+ let c_key_id = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_recipient_get_keyid(recip_handle, c_key_id.address())) {
+ throw new Error("rnp_recipient_get_keyid failed");
+ }
+ let recip_key_id = c_key_id.readString();
+ resultRecipAndPrimary.keyId = recip_key_id;
+ RNPLib.rnp_buffer_destroy(c_key_id);
+
+ let recip_key_handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + recip_key_id
+ );
+ if (!recip_key_handle.isNull()) {
+ let primary_signer_handle = this.getPrimaryKeyHandleIfSub(
+ RNPLib.ffi,
+ recip_key_handle
+ );
+ if (!primary_signer_handle.isNull()) {
+ resultRecipAndPrimary.primaryKeyId = this.getKeyIDFromHandle(
+ primary_signer_handle
+ );
+ RNPLib.rnp_key_handle_destroy(primary_signer_handle);
+ }
+ RNPLib.rnp_key_handle_destroy(recip_key_handle);
+ }
+ },
+
+ getCharCodeArray(pgpData) {
+ return pgpData.split("").map(e => e.charCodeAt());
+ },
+
+ is8Bit(charCodeArray) {
+ for (let i = 0; i < charCodeArray.length; i++) {
+ if (charCodeArray[i] > 255) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ removeCommentLines(str) {
+ const commentLine = /^Comment:.*(\r?\n|\r)/gm;
+ return str.replace(commentLine, "");
+ },
+
+ /**
+ * This function analyzes an encrypted message. It will check if one
+ * of the available secret keys can be used to decrypt a message,
+ * without actually performing the decryption.
+ * This is done by performing a decryption attempt in an empty
+ * environment, which doesn't have any keys available. The decryption
+ * attempt allows us to use the RNP APIs that list the key IDs of
+ * keys that would be able to decrypt the object.
+ * If a matching available secret ID is found, then the handle to that
+ * available secret key is returned.
+ *
+ * @param {rnp_input_t} - A prepared RNP input object that contains
+ * the encrypted message that should be analyzed.
+ * @returns {rnp_key_handle_t} - the handle of a private key that can
+ * be used to decrypt the message, or null, if no usable key was
+ * found.
+ */
+ getFirstAvailableDecryptionKeyHandle(encrypted_rnp_input_from_memory) {
+ let resultKey = null;
+
+ let dummyFfi = RNPLib.prepare_ffi();
+ if (!dummyFfi) {
+ return null;
+ }
+
+ const dummy_max_output_size = 1;
+ let dummy_output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(
+ dummy_output_to_memory.address(),
+ dummy_max_output_size
+ );
+
+ let dummy_verify_op = new RNPLib.rnp_op_verify_t();
+ RNPLib.rnp_op_verify_create(
+ dummy_verify_op.address(),
+ dummyFfi,
+ encrypted_rnp_input_from_memory,
+ dummy_output_to_memory
+ );
+
+ // It's expected and ok that this function returns an error,
+ // e.r. RNP_ERROR_NO_SUITABLE_KEY, we'll query detailed results.
+ RNPLib.rnp_op_verify_execute(dummy_verify_op);
+
+ let all_recip_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_count(
+ dummy_verify_op,
+ all_recip_count.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_count failed");
+ }
+
+ // Loop is skipped if all_recip_count is zero.
+ for (
+ let recip_i = 0;
+ recip_i < all_recip_count.value && !resultKey;
+ recip_i++
+ ) {
+ let recip_handle = new RNPLib.rnp_recipient_handle_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_at(
+ dummy_verify_op,
+ recip_i,
+ recip_handle.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_at failed");
+ }
+
+ let c_key_id = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_recipient_get_keyid(recip_handle, c_key_id.address())) {
+ throw new Error("rnp_recipient_get_keyid failed");
+ }
+ let recip_key_id = c_key_id.readString();
+ RNPLib.rnp_buffer_destroy(c_key_id);
+
+ let recip_key_handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + recip_key_id
+ );
+ if (!recip_key_handle.isNull()) {
+ if (
+ RNPLib.getSecretAvailableFromHandle(recip_key_handle) &&
+ RNPLib.isSecretKeyMaterialAvailable(recip_key_handle)
+ ) {
+ resultKey = recip_key_handle;
+ } else {
+ RNPLib.rnp_key_handle_destroy(recip_key_handle);
+ }
+ }
+ }
+
+ RNPLib.rnp_output_destroy(dummy_output_to_memory);
+ RNPLib.rnp_op_verify_destroy(dummy_verify_op);
+ RNPLib.rnp_ffi_destroy(dummyFfi);
+
+ return resultKey;
+ },
+
+ async decrypt(encrypted, options, alreadyDecrypted = false) {
+ let arr = encrypted.split("").map(e => e.charCodeAt());
+ var encrypted_array = lazy.ctypes.uint8_t.array()(arr);
+
+ let result = {};
+ result.decryptedData = "";
+ result.statusFlags = 0;
+ result.extStatusFlags = 0;
+
+ result.userId = "";
+ result.keyId = "";
+ result.encToDetails = {};
+ result.encToDetails.myRecipKey = {};
+ result.encToDetails.allRecipKeys = [];
+ result.sigDetails = {};
+ result.sigDetails.sigDate = null;
+
+ if (alreadyDecrypted) {
+ result.encToDetails = options.encToDetails;
+ }
+
+ // We cannot reuse the same rnp_input_t for both the dummy operation
+ // and the real decryption operation, as the rnp_input_t object
+ // apparently becomes unusable after operating on it.
+ // That's why we produce a separate rnp_input_t based on the same
+ // data for the dummy operation.
+ let dummy_input_from_memory = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ dummy_input_from_memory.address(),
+ encrypted_array,
+ encrypted_array.length,
+ false
+ );
+
+ let rnpCannotDecrypt = true;
+
+ let decryptKey = new RnpPrivateKeyUnlockTracker(
+ this.getFirstAvailableDecryptionKeyHandle(dummy_input_from_memory)
+ );
+
+ decryptKey.setAllowPromptingUserForPassword(true);
+ decryptKey.setAllowAutoUnlockWithCachedPasswords(true);
+
+ if (decryptKey.available()) {
+ // If the key cannot be automatically unlocked, we'll rely on
+ // the password prompt callback from RNP, and on the user to unlock.
+ await decryptKey.unlock();
+ }
+
+ // Even if we don't have a matching decryption key, run
+ // through full processing, to obtain all the various status flags,
+ // and because decryption might not be necessary.
+ try {
+ let input_from_memory = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ encrypted_array,
+ encrypted_array.length,
+ false
+ );
+
+ // Allow compressed encrypted messages, max factor 1200, up to 100 MiB.
+ const max_decrypted_message_size = 100 * 1024 * 1024;
+ let max_out = Math.min(
+ encrypted.length * 1200,
+ max_decrypted_message_size
+ );
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(output_to_memory.address(), max_out);
+
+ let verify_op = new RNPLib.rnp_op_verify_t();
+ // Apparently the exit code here is ignored (replaced below)
+ result.exitCode = RNPLib.rnp_op_verify_create(
+ verify_op.address(),
+ RNPLib.ffi,
+ input_from_memory,
+ output_to_memory
+ );
+
+ result.exitCode = RNPLib.rnp_op_verify_execute(verify_op);
+
+ rnpCannotDecrypt = false;
+ let queryAllEncryptionRecipients = false;
+ let stillUndecidedIfSignatureIsBad = false;
+
+ let useDecodedData;
+ let processSignature;
+ switch (result.exitCode) {
+ case RNPLib.RNP_SUCCESS:
+ useDecodedData = true;
+ processSignature = true;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_INVALID:
+ // Either the signing key is unavailable, or the signature is
+ // indeed bad. Must check signature status below.
+ stillUndecidedIfSignatureIsBad = true;
+ useDecodedData = true;
+ processSignature = true;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_EXPIRED:
+ useDecodedData = true;
+ processSignature = false;
+ result.statusFlags |= lazy.EnigmailConstants.EXPIRED_SIGNATURE;
+ break;
+ case RNPLib.RNP_ERROR_DECRYPT_FAILED:
+ rnpCannotDecrypt = true;
+ useDecodedData = false;
+ processSignature = false;
+ queryAllEncryptionRecipients = true;
+ result.statusFlags |= lazy.EnigmailConstants.DECRYPTION_FAILED;
+ break;
+ case RNPLib.RNP_ERROR_NO_SUITABLE_KEY:
+ rnpCannotDecrypt = true;
+ useDecodedData = false;
+ processSignature = false;
+ queryAllEncryptionRecipients = true;
+ result.statusFlags |=
+ lazy.EnigmailConstants.DECRYPTION_FAILED |
+ lazy.EnigmailConstants.NO_SECKEY;
+ break;
+ default:
+ useDecodedData = false;
+ processSignature = false;
+ console.debug(
+ "rnp_op_verify_execute returned unexpected: " + result.exitCode
+ );
+ break;
+ }
+
+ if (useDecodedData && alreadyDecrypted) {
+ result.statusFlags |= lazy.EnigmailConstants.DECRYPTION_OKAY;
+ } else if (useDecodedData && !alreadyDecrypted) {
+ let prot_mode_str = new lazy.ctypes.char.ptr();
+ let prot_cipher_str = new lazy.ctypes.char.ptr();
+ let prot_is_valid = new lazy.ctypes.bool();
+
+ if (
+ RNPLib.rnp_op_verify_get_protection_info(
+ verify_op,
+ prot_mode_str.address(),
+ prot_cipher_str.address(),
+ prot_is_valid.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_protection_info failed");
+ }
+ let mode = prot_mode_str.readString();
+ let cipher = prot_cipher_str.readString();
+ let validIntegrityProtection = prot_is_valid.value;
+
+ if (mode != "none") {
+ if (!validIntegrityProtection) {
+ useDecodedData = false;
+ result.statusFlags |=
+ lazy.EnigmailConstants.MISSING_MDC |
+ lazy.EnigmailConstants.DECRYPTION_FAILED;
+ } else if (mode == "null" || this.policyForbidsAlg(cipher)) {
+ // don't indicate decryption, because a non-protecting or insecure cipher was used
+ result.statusFlags |= lazy.EnigmailConstants.UNKNOWN_ALGO;
+ } else {
+ queryAllEncryptionRecipients = true;
+
+ let recip_handle = new RNPLib.rnp_recipient_handle_t();
+ let rv = RNPLib.rnp_op_verify_get_used_recipient(
+ verify_op,
+ recip_handle.address()
+ );
+ if (rv) {
+ throw new Error("rnp_op_verify_get_used_recipient failed");
+ }
+
+ let c_alg = new lazy.ctypes.char.ptr();
+ rv = RNPLib.rnp_recipient_get_alg(recip_handle, c_alg.address());
+ if (rv) {
+ throw new Error("rnp_recipient_get_alg failed");
+ }
+
+ if (this.policyForbidsAlg(c_alg.readString())) {
+ result.statusFlags |= lazy.EnigmailConstants.UNKNOWN_ALGO;
+ } else {
+ this.getKeyIdsFromRecipHandle(
+ recip_handle,
+ result.encToDetails.myRecipKey
+ );
+ result.statusFlags |= lazy.EnigmailConstants.DECRYPTION_OKAY;
+ }
+ }
+ }
+ }
+
+ if (queryAllEncryptionRecipients) {
+ let all_recip_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_count(
+ verify_op,
+ all_recip_count.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_count failed");
+ }
+ if (all_recip_count.value > 1) {
+ for (let recip_i = 0; recip_i < all_recip_count.value; recip_i++) {
+ let other_recip_handle = new RNPLib.rnp_recipient_handle_t();
+ if (
+ RNPLib.rnp_op_verify_get_recipient_at(
+ verify_op,
+ recip_i,
+ other_recip_handle.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_get_recipient_at failed");
+ }
+ let encTo = {};
+ this.getKeyIdsFromRecipHandle(other_recip_handle, encTo);
+ result.encToDetails.allRecipKeys.push(encTo);
+ }
+ }
+ }
+
+ if (useDecodedData) {
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let rv = RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ // result_len is of type UInt64, I don't know of a better way
+ // to convert it to an integer.
+ let b_len = parseInt(result_len.value.toString());
+
+ if (!rv) {
+ // type casting the pointer type to an array type allows us to
+ // access the elements by index.
+ let uint8_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.uint8_t.array(result_len.value).ptr
+ ).contents;
+
+ let str = "";
+ for (let i = 0; i < b_len; i++) {
+ str += String.fromCharCode(uint8_array[i]);
+ }
+
+ result.decryptedData = str;
+ }
+
+ if (processSignature) {
+ // ignore "no signature" result, that's ok
+ await this.getVerifyDetails(
+ RNPLib.ffi,
+ options.fromAddr,
+ options.msgDate,
+ verify_op,
+ result
+ );
+
+ if (
+ (result.statusFlags &
+ (lazy.EnigmailConstants.GOOD_SIGNATURE |
+ lazy.EnigmailConstants.UNCERTAIN_SIGNATURE |
+ lazy.EnigmailConstants.EXPIRED_SIGNATURE |
+ lazy.EnigmailConstants.BAD_SIGNATURE)) !=
+ 0
+ ) {
+ // A decision was already made.
+ stillUndecidedIfSignatureIsBad = false;
+ }
+ }
+ }
+
+ if (stillUndecidedIfSignatureIsBad) {
+ // We didn't find more details above, so conclude it's bad.
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+ RNPLib.rnp_op_verify_destroy(verify_op);
+ } finally {
+ decryptKey.release();
+ RNPLib.rnp_input_destroy(dummy_input_from_memory);
+ }
+
+ if (
+ rnpCannotDecrypt &&
+ !alreadyDecrypted &&
+ Services.prefs.getBoolPref("mail.openpgp.allow_external_gnupg") &&
+ lazy.GPGME.allDependenciesLoaded()
+ ) {
+ // failure processing with RNP, attempt decryption with GPGME
+ let r2 = await lazy.GPGME.decrypt(
+ encrypted,
+ this.enArmorCDataMessage.bind(this)
+ );
+ if (!r2.exitCode && r2.decryptedData) {
+ // TODO: obtain info which key ID was used for decryption
+ // and set result.decryptKey*
+ // It isn't obvious how to do that with GPGME, because
+ // gpgme_op_decrypt_result provides the list of all the
+ // encryption keys, only.
+
+ // The result may still contain wrapping like compression,
+ // and optional signature data. Recursively call ourselves
+ // to perform the remaining processing.
+ options.encToDetails = result.encToDetails;
+ return RNP.decrypt(r2.decryptedData, options, true);
+ }
+ }
+
+ return result;
+ },
+
+ async getVerifyDetails(ffi, fromAddr, msgDate, verify_op, result) {
+ if (!fromAddr) {
+ // We cannot correctly verify without knowing the fromAddr.
+ // This scenario is reached when quoting an encrypted MIME part.
+ return false;
+ }
+
+ let sig_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_op_verify_get_signature_count(verify_op, sig_count.address())
+ ) {
+ throw new Error("rnp_op_verify_get_signature_count failed");
+ }
+
+ // TODO: How should handle (sig_count.value > 1) ?
+ if (sig_count.value == 0) {
+ // !sig_count.value didn't work, === also doesn't work
+ return false;
+ }
+
+ let sig = new RNPLib.rnp_op_verify_signature_t();
+ if (RNPLib.rnp_op_verify_get_signature_at(verify_op, 0, sig.address())) {
+ throw new Error("rnp_op_verify_get_signature_at failed");
+ }
+
+ let sig_handle = new RNPLib.rnp_signature_handle_t();
+ if (RNPLib.rnp_op_verify_signature_get_handle(sig, sig_handle.address())) {
+ throw new Error("rnp_op_verify_signature_get_handle failed");
+ }
+
+ let sig_id_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_signature_get_keyid(sig_handle, sig_id_str.address())) {
+ throw new Error("rnp_signature_get_keyid failed");
+ }
+ result.keyId = sig_id_str.readString();
+ RNPLib.rnp_buffer_destroy(sig_id_str);
+ RNPLib.rnp_signature_handle_destroy(sig_handle);
+
+ let sig_status = RNPLib.rnp_op_verify_signature_get_status(sig);
+ if (sig_status != RNPLib.RNP_SUCCESS && !result.exitCode) {
+ /* Don't allow a good exit code. Keep existing bad code. */
+ result.exitCode = -1;
+ }
+
+ let query_signer = true;
+
+ switch (sig_status) {
+ case RNPLib.RNP_SUCCESS:
+ result.statusFlags |= lazy.EnigmailConstants.GOOD_SIGNATURE;
+ break;
+ case RNPLib.RNP_ERROR_KEY_NOT_FOUND:
+ result.statusFlags |=
+ lazy.EnigmailConstants.UNCERTAIN_SIGNATURE |
+ lazy.EnigmailConstants.NO_PUBKEY;
+ query_signer = false;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_EXPIRED:
+ result.statusFlags |= lazy.EnigmailConstants.EXPIRED_SIGNATURE;
+ break;
+ case RNPLib.RNP_ERROR_SIGNATURE_INVALID:
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ break;
+ default:
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ query_signer = false;
+ break;
+ }
+
+ if (msgDate && result.statusFlags & lazy.EnigmailConstants.GOOD_SIGNATURE) {
+ let created = new lazy.ctypes.uint32_t();
+ let expires = new lazy.ctypes.uint32_t(); //relative
+
+ if (
+ RNPLib.rnp_op_verify_signature_get_times(
+ sig,
+ created.address(),
+ expires.address()
+ )
+ ) {
+ throw new Error("rnp_op_verify_signature_get_times failed");
+ }
+
+ result.sigDetails.sigDate = new Date(created.value * 1000);
+
+ let timeDelta;
+ if (result.sigDetails.sigDate > msgDate) {
+ timeDelta = result.sigDetails.sigDate - msgDate;
+ } else {
+ timeDelta = msgDate - result.sigDetails.sigDate;
+ }
+
+ if (timeDelta > 1000 * 60 * 60 * 1) {
+ result.statusFlags &= ~lazy.EnigmailConstants.GOOD_SIGNATURE;
+ result.statusFlags |= lazy.EnigmailConstants.MSG_SIG_INVALID;
+ }
+ }
+
+ let signer_key = new RNPLib.rnp_key_handle_t();
+ let have_signer_key = false;
+ let use_signer_key = false;
+
+ if (query_signer) {
+ if (RNPLib.rnp_op_verify_signature_get_key(sig, signer_key.address())) {
+ // If sig_status isn't RNP_ERROR_KEY_NOT_FOUND then we must
+ // be able to obtain the signer key.
+ throw new Error("rnp_op_verify_signature_get_key");
+ }
+
+ have_signer_key = true;
+ use_signer_key = !this.isBadKey(signer_key, null, RNPLib.ffi);
+ }
+
+ if (use_signer_key) {
+ let keyInfo = {};
+ let ok = this.getKeyInfoFromHandle(
+ ffi,
+ signer_key,
+ keyInfo,
+ true,
+ false,
+ false
+ );
+ if (!ok) {
+ throw new Error("getKeyInfoFromHandle failed");
+ }
+
+ let fromMatchesAnyUid = false;
+ let fromLower = fromAddr ? fromAddr.toLowerCase() : "";
+
+ for (let uid of keyInfo.userIds) {
+ if (uid.type !== "uid") {
+ continue;
+ }
+
+ if (
+ lazy.EnigmailFuncs.getEmailFromUserID(uid.userId).toLowerCase() ===
+ fromLower
+ ) {
+ fromMatchesAnyUid = true;
+ break;
+ }
+ }
+
+ let useUndecided = true;
+
+ if (keyInfo.secretAvailable) {
+ let isPersonal = await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(
+ keyInfo.fpr
+ );
+ if (isPersonal && fromMatchesAnyUid) {
+ result.extStatusFlags |= lazy.EnigmailConstants.EXT_SELF_IDENTITY;
+ useUndecided = false;
+ } else {
+ result.statusFlags |= lazy.EnigmailConstants.INVALID_RECIPIENT;
+ useUndecided = true;
+ }
+ } else if (result.statusFlags & lazy.EnigmailConstants.GOOD_SIGNATURE) {
+ if (!fromMatchesAnyUid) {
+ /* At the time the user had accepted the key,
+ * a different set of email addresses might have been
+ * contained inside the key. In the meantime, we might
+ * have refreshed the key, a email addresses
+ * might have been removed or revoked.
+ * If the current from was removed/revoked, we'd still
+ * get an acceptance match, but the from is no longer found
+ * in the key's UID list. That should get "undecided".
+ */
+ result.statusFlags |= lazy.EnigmailConstants.INVALID_RECIPIENT;
+ useUndecided = true;
+ } else {
+ let acceptanceResult = {};
+ try {
+ await lazy.PgpSqliteDb2.getAcceptance(
+ keyInfo.fpr,
+ fromLower,
+ acceptanceResult
+ );
+ } catch (ex) {
+ console.debug("getAcceptance failed: " + ex);
+ }
+
+ // unverified key acceptance means, we consider the signature OK,
+ // but it's not a trusted identity.
+ // unverified signature means, we cannot decide if the signature
+ // is ok.
+
+ if (
+ "emailDecided" in acceptanceResult &&
+ acceptanceResult.emailDecided &&
+ "fingerprintAcceptance" in acceptanceResult &&
+ acceptanceResult.fingerprintAcceptance.length &&
+ acceptanceResult.fingerprintAcceptance != "undecided"
+ ) {
+ if (acceptanceResult.fingerprintAcceptance == "rejected") {
+ result.statusFlags &= ~lazy.EnigmailConstants.GOOD_SIGNATURE;
+ result.statusFlags |=
+ lazy.EnigmailConstants.BAD_SIGNATURE |
+ lazy.EnigmailConstants.INVALID_RECIPIENT;
+ useUndecided = false;
+ } else if (acceptanceResult.fingerprintAcceptance == "verified") {
+ result.statusFlags |= lazy.EnigmailConstants.TRUSTED_IDENTITY;
+ useUndecided = false;
+ } else if (acceptanceResult.fingerprintAcceptance == "unverified") {
+ useUndecided = false;
+ }
+ }
+ }
+ }
+
+ if (useUndecided) {
+ result.statusFlags &= ~lazy.EnigmailConstants.GOOD_SIGNATURE;
+ result.statusFlags |= lazy.EnigmailConstants.UNCERTAIN_SIGNATURE;
+ }
+ }
+
+ if (have_signer_key) {
+ RNPLib.rnp_key_handle_destroy(signer_key);
+ }
+
+ return true;
+ },
+
+ async verifyDetached(data, options) {
+ let result = {};
+ result.decryptedData = "";
+ result.statusFlags = 0;
+ result.exitCode = -1;
+ result.extStatusFlags = 0;
+ result.userId = "";
+ result.keyId = "";
+ result.sigDetails = {};
+ result.sigDetails.sigDate = null;
+
+ let sig_arr = options.mimeSignatureData.split("").map(e => e.charCodeAt());
+ var sig_array = lazy.ctypes.uint8_t.array()(sig_arr);
+
+ let input_sig = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ input_sig.address(),
+ sig_array,
+ sig_array.length,
+ false
+ );
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ let arr = data.split("").map(e => e.charCodeAt());
+ var data_array = lazy.ctypes.uint8_t.array()(arr);
+
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ data_array,
+ data_array.length,
+ false
+ );
+
+ let verify_op = new RNPLib.rnp_op_verify_t();
+ if (
+ RNPLib.rnp_op_verify_detached_create(
+ verify_op.address(),
+ RNPLib.ffi,
+ input_from_memory,
+ input_sig
+ )
+ ) {
+ throw new Error("rnp_op_verify_detached_create failed");
+ }
+
+ result.exitCode = RNPLib.rnp_op_verify_execute(verify_op);
+
+ let haveSignature = await this.getVerifyDetails(
+ RNPLib.ffi,
+ options.fromAddr,
+ options.msgDate,
+ verify_op,
+ result
+ );
+ if (!haveSignature) {
+ if (!result.exitCode) {
+ /* Don't allow a good exit code. Keep existing bad code. */
+ result.exitCode = -1;
+ }
+ result.statusFlags |= lazy.EnigmailConstants.BAD_SIGNATURE;
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_input_destroy(input_sig);
+ RNPLib.rnp_op_verify_destroy(verify_op);
+
+ return result;
+ },
+
+ async genKey(userId, keyType, keyBits, expiryDays, passphrase) {
+ let newKeyId = "";
+ let newKeyFingerprint = "";
+
+ let primaryKeyType;
+ let primaryKeyBits = 0;
+ let subKeyType;
+ let subKeyBits = 0;
+ let primaryKeyCurve = null;
+ let subKeyCurve = null;
+ let expireSeconds = 0;
+
+ if (keyType == "RSA") {
+ primaryKeyType = subKeyType = "rsa";
+ primaryKeyBits = subKeyBits = keyBits;
+ } else if (keyType == "ECC") {
+ primaryKeyType = "eddsa";
+ subKeyType = "ecdh";
+ subKeyCurve = "Curve25519";
+ } else {
+ return null;
+ }
+
+ if (expiryDays != 0) {
+ expireSeconds = expiryDays * 24 * 60 * 60;
+ }
+
+ let genOp = new RNPLib.rnp_op_generate_t();
+ if (
+ RNPLib.rnp_op_generate_create(genOp.address(), RNPLib.ffi, primaryKeyType)
+ ) {
+ throw new Error("rnp_op_generate_create primary failed");
+ }
+
+ if (RNPLib.rnp_op_generate_set_userid(genOp, userId)) {
+ throw new Error("rnp_op_generate_set_userid failed");
+ }
+
+ if (passphrase != null && passphrase.length != 0) {
+ if (RNPLib.rnp_op_generate_set_protection_password(genOp, passphrase)) {
+ throw new Error("rnp_op_generate_set_protection_password failed");
+ }
+ }
+
+ if (primaryKeyBits != 0) {
+ if (RNPLib.rnp_op_generate_set_bits(genOp, primaryKeyBits)) {
+ throw new Error("rnp_op_generate_set_bits primary failed");
+ }
+ }
+
+ if (primaryKeyCurve != null) {
+ if (RNPLib.rnp_op_generate_set_curve(genOp, primaryKeyCurve)) {
+ throw new Error("rnp_op_generate_set_curve primary failed");
+ }
+ }
+
+ if (RNPLib.rnp_op_generate_set_expiration(genOp, expireSeconds)) {
+ throw new Error("rnp_op_generate_set_expiration primary failed");
+ }
+
+ if (RNPLib.rnp_op_generate_execute(genOp)) {
+ throw new Error("rnp_op_generate_execute primary failed");
+ }
+
+ let primaryKey = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_op_generate_get_key(genOp, primaryKey.address())) {
+ throw new Error("rnp_op_generate_get_key primary failed");
+ }
+
+ RNPLib.rnp_op_generate_destroy(genOp);
+
+ newKeyFingerprint = this.getFingerprintFromHandle(primaryKey);
+ newKeyId = this.getKeyIDFromHandle(primaryKey);
+
+ if (
+ RNPLib.rnp_op_generate_subkey_create(
+ genOp.address(),
+ RNPLib.ffi,
+ primaryKey,
+ subKeyType
+ )
+ ) {
+ throw new Error("rnp_op_generate_subkey_create primary failed");
+ }
+
+ if (passphrase != null && passphrase.length != 0) {
+ if (RNPLib.rnp_op_generate_set_protection_password(genOp, passphrase)) {
+ throw new Error("rnp_op_generate_set_protection_password failed");
+ }
+ }
+
+ if (subKeyBits != 0) {
+ if (RNPLib.rnp_op_generate_set_bits(genOp, subKeyBits)) {
+ throw new Error("rnp_op_generate_set_bits sub failed");
+ }
+ }
+
+ if (subKeyCurve != null) {
+ if (RNPLib.rnp_op_generate_set_curve(genOp, subKeyCurve)) {
+ throw new Error("rnp_op_generate_set_curve sub failed");
+ }
+ }
+
+ if (RNPLib.rnp_op_generate_set_expiration(genOp, expireSeconds)) {
+ throw new Error("rnp_op_generate_set_expiration sub failed");
+ }
+
+ let unlocked = false;
+ try {
+ if (passphrase != null && passphrase.length != 0) {
+ if (RNPLib.rnp_key_unlock(primaryKey, passphrase)) {
+ throw new Error("rnp_key_unlock failed");
+ }
+ unlocked = true;
+ }
+
+ if (RNPLib.rnp_op_generate_execute(genOp)) {
+ throw new Error("rnp_op_generate_execute sub failed");
+ }
+ } finally {
+ if (unlocked) {
+ RNPLib.rnp_key_lock(primaryKey);
+ }
+ }
+
+ RNPLib.rnp_op_generate_destroy(genOp);
+ RNPLib.rnp_key_handle_destroy(primaryKey);
+
+ await lazy.PgpSqliteDb2.acceptAsPersonalKey(newKeyFingerprint);
+
+ return newKeyId;
+ },
+
+ async saveKeyRings() {
+ RNPLib.saveKeys();
+ Services.obs.notifyObservers(null, "openpgp-key-change");
+ },
+
+ importToFFI(ffi, keyBlockStr, usePublic, useSecret, permissive) {
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ if (!keyBlockStr) {
+ throw new Error("no keyBlockStr parameter in importToFFI");
+ }
+
+ if (typeof keyBlockStr != "string") {
+ throw new Error(
+ "keyBlockStr of unepected type importToFFI: %o",
+ keyBlockStr
+ );
+ }
+
+ // Input might be either plain text or binary data.
+ // If the input is binary, do not modify it.
+ // If the input contains characters with a multi-byte char code value,
+ // we know the input doesn't consist of binary 8-bit values. Rather,
+ // it contains text with multi-byte characters. The only scenario
+ // in which we can tolerate those are comment lines, which we can
+ // filter out.
+
+ let arr = this.getCharCodeArray(keyBlockStr);
+ if (!this.is8Bit(arr)) {
+ let trimmed = this.removeCommentLines(keyBlockStr);
+ arr = this.getCharCodeArray(trimmed);
+ if (!this.is8Bit(arr)) {
+ throw new Error(`Non-ascii key block: ${keyBlockStr}`);
+ }
+ }
+ var key_array = lazy.ctypes.uint8_t.array()(arr);
+
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ key_array,
+ key_array.length,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let jsonInfo = new lazy.ctypes.char.ptr();
+
+ let flags = 0;
+ if (usePublic) {
+ flags |= RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS;
+ }
+ if (useSecret) {
+ flags |= RNPLib.RNP_LOAD_SAVE_SECRET_KEYS;
+ }
+
+ if (permissive) {
+ flags |= RNPLib.RNP_LOAD_SAVE_PERMISSIVE;
+ }
+
+ let rv = RNPLib.rnp_import_keys(
+ ffi,
+ input_from_memory,
+ flags,
+ jsonInfo.address()
+ );
+
+ // TODO: parse jsonInfo and return a list of keys,
+ // as seen in keyRing.importKeyAsync.
+ // (should prevent the incorrect popup "no keys imported".)
+
+ if (rv) {
+ console.debug("rnp_import_keys failed with rv: " + rv);
+ }
+
+ RNPLib.rnp_buffer_destroy(jsonInfo);
+ RNPLib.rnp_input_destroy(input_from_memory);
+
+ return rv;
+ },
+
+ maxImportKeyBlockSize: 5000000,
+
+ async getOnePubKeyFromKeyBlock(keyBlockStr, fpr, permissive = true) {
+ if (!keyBlockStr) {
+ throw new Error(`Invalid parameter; keyblock: ${keyBlockStr}`);
+ }
+
+ if (keyBlockStr.length > RNP.maxImportKeyBlockSize) {
+ throw new Error("rejecting big keyblock");
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let pubKey;
+ if (!this.importToFFI(tempFFI, keyBlockStr, true, false, permissive)) {
+ pubKey = await this.getPublicKey("0x" + fpr, tempFFI);
+ }
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return pubKey;
+ },
+
+ async getKeyListFromKeyBlockImpl(
+ keyBlockStr,
+ pubkey = true,
+ seckey = false,
+ permissive = true,
+ withPubKey = false
+ ) {
+ if (!keyBlockStr) {
+ throw new Error(`Invalid parameter; keyblock: ${keyBlockStr}`);
+ }
+
+ if (keyBlockStr.length > RNP.maxImportKeyBlockSize) {
+ throw new Error("rejecting big keyblock");
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let keyList = null;
+ if (!this.importToFFI(tempFFI, keyBlockStr, pubkey, seckey, permissive)) {
+ keyList = await this.getKeysFromFFI(
+ tempFFI,
+ true,
+ null,
+ false,
+ withPubKey
+ );
+ }
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return keyList;
+ },
+
+ /**
+ * Take two or more ASCII armored key blocks and import them into memory,
+ * and return the merged public key for the given fingerprint.
+ * (Other keys included in the key blocks are ignored.)
+ * The intention is to use it to combine keys obtained from different places,
+ * possibly with updated/different expiration date and userIds etc. to
+ * a canonical representation of them.
+ *
+ * @param {string} fingerprint - Key fingerprint.
+ * @param {...string} - Key blocks.
+ * @returns {string} the resulting public key of the blocks
+ */
+ async mergePublicKeyBlocks(fingerprint, ...keyBlocks) {
+ if (keyBlocks.some(b => b.length > RNP.maxImportKeyBlockSize)) {
+ throw new Error("keyBlock too big");
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ const pubkey = true;
+ const seckey = false;
+ const permissive = false;
+ for (let block of new Set(keyBlocks)) {
+ if (this.importToFFI(tempFFI, block, pubkey, seckey, permissive)) {
+ throw new Error("Merging public keys failed");
+ }
+ }
+ let pubKey = await this.getPublicKey(`0x${fingerprint}`, tempFFI);
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return pubKey;
+ },
+
+ async importRevImpl(data) {
+ if (!data || typeof data != "string") {
+ throw new Error("invalid data parameter");
+ }
+
+ let arr = data.split("").map(e => e.charCodeAt());
+ var key_array = lazy.ctypes.uint8_t.array()(arr);
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ key_array,
+ key_array.length,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let jsonInfo = new lazy.ctypes.char.ptr();
+
+ let flags = 0;
+ let rv = RNPLib.rnp_import_signatures(
+ RNPLib.ffi,
+ input_from_memory,
+ flags,
+ jsonInfo.address()
+ );
+
+ // TODO: parse jsonInfo
+
+ if (rv) {
+ console.debug("rnp_import_signatures failed with rv: " + rv);
+ }
+
+ RNPLib.rnp_buffer_destroy(jsonInfo);
+ RNPLib.rnp_input_destroy(input_from_memory);
+ await this.saveKeyRings();
+
+ return rv;
+ },
+
+ async importSecKeyBlockImpl(
+ win,
+ passCB,
+ keepPassphrases,
+ keyBlockStr,
+ permissive = false,
+ limitedFPRs = []
+ ) {
+ return this._importKeyBlockWithAutoAccept(
+ win,
+ passCB,
+ keepPassphrases,
+ keyBlockStr,
+ false,
+ true,
+ null,
+ permissive,
+ limitedFPRs
+ );
+ },
+
+ async importPubkeyBlockAutoAcceptImpl(
+ win,
+ keyBlockStr,
+ acceptance,
+ permissive = false,
+ limitedFPRs = []
+ ) {
+ return this._importKeyBlockWithAutoAccept(
+ win,
+ null,
+ false,
+ keyBlockStr,
+ true,
+ false,
+ acceptance,
+ permissive,
+ limitedFPRs
+ );
+ },
+
+ /**
+ * Import either a public key or a secret key.
+ * Importing both at the same time isn't supported by this API.
+ *
+ * @param {?nsIWindow} win - Parent window, may be null
+ * @param {Function} passCB - a callback function that will be called if the user needs
+ * to enter a passphrase to unlock a secret key. See passphrasePromptCallback
+ * for the function signature.
+ * @param {boolean} keepPassphrases - controls which passphrase will
+ * be used to protect imported secret keys. If true, the existing
+ * passphrase will be kept. If false, (of if currently there's no
+ * passphrase set), passphrase protection will be changed to use
+ * our automatic passphrase (to allow automatic protection by
+ * primary password, whether's it's currently enabled or not).
+ * @param {string} keyBlockStr - An block of OpenPGP key data. See
+ * implementation of function importToFFI for allowed contents.
+ * TODO: Write better documentation for this parameter.
+ * @param {boolean} pubkey - If true, import the public keys found in
+ * keyBlockStr.
+ * @param {boolean} seckey - If true, import the secret keys found in
+ * keyBlockStr.
+ * @param {string} acceptance - The key acceptance level that should
+ * be assigned to imported public keys.
+ * TODO: Write better documentation for the allowed values.
+ * @param {boolean} permissive - Whether it's allowed to fall back
+ * to a permissive import, if strict import fails.
+ * (See RNP documentation for RNP_LOAD_SAVE_PERMISSIVE.)
+ * @param {string[]} limitedFPRs - This is a filtering parameter.
+ * If the array is empty, all keys will be imported.
+ * If the array contains at least one entry, a key will be imported
+ * only if its fingerprint (of the primary key) is listed in this
+ * array.
+ */
+ async _importKeyBlockWithAutoAccept(
+ win,
+ passCB,
+ keepPassphrases,
+ keyBlockStr,
+ pubkey,
+ seckey,
+ acceptance,
+ permissive = false,
+ limitedFPRs = []
+ ) {
+ if (keyBlockStr.length > RNP.maxImportKeyBlockSize) {
+ throw new Error("rejecting big keyblock");
+ }
+ if (pubkey && seckey) {
+ // Currently no caller needs to import both at the save time,
+ // and the implementation hasn't been reviewed, whether it
+ // supports it or not, so we refuse this request.
+ throw new Error("Cannot import public and secret keys at the same time");
+ }
+
+ /*
+ * Import strategy:
+ * - import file into a temporary space, in-memory only (ffi)
+ * - if we failed to decrypt the secret keys, return null
+ * - set the password of secret keys that don't have one yet
+ * - get the key listing of all keys from the temporary space,
+ * which is want we want to return as the import report
+ * - export all keys from the temporary space, and import them
+ * into our permanent space.
+ */
+ let userFlags = { canceled: false };
+
+ let result = {};
+ result.exitCode = -1;
+ result.importedKeys = [];
+ result.errorMsg = "";
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ // TODO: check result
+ if (this.importToFFI(tempFFI, keyBlockStr, pubkey, seckey, permissive)) {
+ result.errorMsg = "RNP.importToFFI failed";
+ return result;
+ }
+
+ let keys = await this.getKeysFromFFI(tempFFI, true);
+ let pwCache = {
+ passwords: [],
+ };
+
+ // Prior to importing, ensure the user is able to unlock all keys
+
+ // If anything goes wrong during our attempt to unlock keys,
+ // we don't want to keep key material remain unprotected in memory,
+ // that's why we remember the trackers, including the respective
+ // unlock passphrase, temporarily in memory, and we'll minimize
+ // the period of time during which the key remains unprotected.
+ let secretKeyTrackers = new Map();
+
+ let unableToUnlockId = null;
+
+ for (let k of keys) {
+ let fprStr = "0x" + k.fpr;
+ if (limitedFPRs.length && !limitedFPRs.includes(fprStr)) {
+ continue;
+ }
+
+ let impKey = await this.getKeyHandleByIdentifier(tempFFI, fprStr);
+ if (impKey.isNull()) {
+ throw new Error("cannot get key handle for imported key: " + k.fpr);
+ }
+
+ if (!k.secretAvailable) {
+ RNPLib.rnp_key_handle_destroy(impKey);
+ impKey = null;
+ } else {
+ let primaryKey = new RnpPrivateKeyUnlockTracker(impKey);
+ impKey = null;
+
+ // Don't attempt to unlock secret keys that are unavailable.
+ if (primaryKey.available()) {
+ // Is it unprotected?
+ primaryKey.unlockWithPassword("");
+ if (primaryKey.isUnlocked()) {
+ // yes, it's unprotected (empty passphrase)
+ await primaryKey.setAutoPassphrase();
+ } else {
+ // try to unlock with the recently entered passwords,
+ // or ask the user, if allowed
+ primaryKey.setPasswordCache(pwCache);
+ primaryKey.setAllowAutoUnlockWithCachedPasswords(true);
+ primaryKey.setAllowPromptingUserForPassword(!!passCB);
+ primaryKey.setPassphraseCallback(passCB);
+ primaryKey.setRememberUnlockPassword(true);
+ await primaryKey.unlock(tempFFI);
+ if (!primaryKey.isUnlocked()) {
+ userFlags.canceled = true;
+ unableToUnlockId = RNP.getKeyIDFromHandle(primaryKey.getHandle());
+ } else {
+ secretKeyTrackers.set(fprStr, primaryKey);
+ }
+ }
+ }
+
+ if (!userFlags.canceled) {
+ let sub_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_key_get_subkey_count(
+ primaryKey.getHandle(),
+ sub_count.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ for (let i = 0; i < sub_count.value && !userFlags.canceled; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_key_get_subkey_at(
+ primaryKey.getHandle(),
+ i,
+ sub_handle.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ let subTracker = new RnpPrivateKeyUnlockTracker(sub_handle);
+ sub_handle = null;
+
+ if (subTracker.available()) {
+ // Is it unprotected?
+ subTracker.unlockWithPassword("");
+ if (subTracker.isUnlocked()) {
+ // yes, it's unprotected (empty passphrase)
+ await subTracker.setAutoPassphrase();
+ } else {
+ // try to unlock with the recently entered passwords,
+ // or ask the user, if allowed
+ subTracker.setPasswordCache(pwCache);
+ subTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ subTracker.setAllowPromptingUserForPassword(!!passCB);
+ subTracker.setPassphraseCallback(passCB);
+ subTracker.setRememberUnlockPassword(true);
+ await subTracker.unlock(tempFFI);
+ if (!subTracker.isUnlocked()) {
+ userFlags.canceled = true;
+ unableToUnlockId = RNP.getKeyIDFromHandle(
+ subTracker.getHandle()
+ );
+ break;
+ } else {
+ secretKeyTrackers.set(
+ this.getFingerprintFromHandle(subTracker.getHandle()),
+ subTracker
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (userFlags.canceled) {
+ break;
+ }
+ }
+
+ if (unableToUnlockId) {
+ result.errorMsg = "Cannot unlock key " + unableToUnlockId;
+ }
+
+ if (!userFlags.canceled) {
+ for (let k of keys) {
+ let fprStr = "0x" + k.fpr;
+ if (limitedFPRs.length && !limitedFPRs.includes(fprStr)) {
+ continue;
+ }
+
+ // We allow importing, if any of the following is true
+ // - it contains a secret key
+ // - it contains at least one user ID
+ // - it is an update for an existing key (possibly new validity/revocation)
+
+ if (k.userIds.length == 0 && !k.secretAvailable) {
+ let existingKey = await this.getKeyHandleByIdentifier(
+ RNPLib.ffi,
+ "0x" + k.fpr
+ );
+ if (existingKey.isNull()) {
+ continue;
+ }
+ RNPLib.rnp_key_handle_destroy(existingKey);
+ }
+
+ let impKeyPub;
+ let impKeySecTracker = secretKeyTrackers.get(fprStr);
+ if (!impKeySecTracker) {
+ impKeyPub = await this.getKeyHandleByIdentifier(tempFFI, fprStr);
+ }
+
+ if (!keepPassphrases) {
+ // It's possible that the primary key doesn't come with a
+ // secret key (only public key of primary key was imported).
+ // In that scenario, we must still process subkeys that come
+ // with a secret key.
+
+ if (impKeySecTracker) {
+ impKeySecTracker.unprotect();
+ await impKeySecTracker.setAutoPassphrase();
+ }
+
+ let sub_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_key_get_subkey_count(
+ impKeySecTracker ? impKeySecTracker.getHandle() : impKeyPub,
+ sub_count.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_key_get_subkey_at(
+ impKeySecTracker ? impKeySecTracker.getHandle() : impKeyPub,
+ i,
+ sub_handle.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ let subTracker = secretKeyTrackers.get(
+ this.getFingerprintFromHandle(sub_handle)
+ );
+ if (!subTracker) {
+ // There is no secret key material for this subkey available,
+ // that's why no tracker was created, we can skip it.
+ continue;
+ }
+ subTracker.unprotect();
+ await subTracker.setAutoPassphrase();
+ }
+ }
+
+ let exportFlags =
+ RNPLib.RNP_KEY_EXPORT_ARMORED | RNPLib.RNP_KEY_EXPORT_SUBKEYS;
+
+ if (pubkey) {
+ exportFlags |= RNPLib.RNP_KEY_EXPORT_PUBLIC;
+ }
+ if (seckey) {
+ exportFlags |= RNPLib.RNP_KEY_EXPORT_SECRET;
+ }
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ if (
+ RNPLib.rnp_key_export(
+ impKeySecTracker ? impKeySecTracker.getHandle() : impKeyPub,
+ output_to_memory,
+ exportFlags
+ )
+ ) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ if (impKeyPub) {
+ RNPLib.rnp_key_handle_destroy(impKeyPub);
+ impKeyPub = null;
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ }
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ result_buf,
+ result_len,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let importFlags = 0;
+ if (pubkey) {
+ importFlags |= RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS;
+ }
+ if (seckey) {
+ importFlags |= RNPLib.RNP_LOAD_SAVE_SECRET_KEYS;
+ }
+ if (permissive) {
+ importFlags |= RNPLib.RNP_LOAD_SAVE_PERMISSIVE;
+ }
+
+ if (
+ RNPLib.rnp_import_keys(
+ RNPLib.ffi,
+ input_from_memory,
+ importFlags,
+ null
+ )
+ ) {
+ throw new Error("rnp_import_keys failed");
+ }
+
+ result.importedKeys.push("0x" + k.id);
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+
+ // For acceptance "undecided", we don't store it, because that's
+ // the default if no value is stored.
+ let actionableAcceptances = ["rejected", "unverified", "verified"];
+
+ if (
+ pubkey &&
+ !k.secretAvailable &&
+ actionableAcceptances.includes(acceptance)
+ ) {
+ // For each imported public key and associated email address,
+ // update the acceptance to unverified, but only if it's only
+ // currently undecided. In other words, we keep the acceptance
+ // if it's rejected or verified.
+
+ let currentAcceptance =
+ await lazy.PgpSqliteDb2.getFingerprintAcceptance(null, k.fpr);
+
+ if (!currentAcceptance || currentAcceptance == "undecided") {
+ // Currently undecided, allowed to change.
+ let allEmails = [];
+
+ for (let uid of k.userIds) {
+ if (uid.type != "uid") {
+ continue;
+ }
+
+ let uidEmail = lazy.EnigmailFuncs.getEmailFromUserID(uid.userId);
+ if (uidEmail) {
+ allEmails.push(uidEmail);
+ }
+ }
+ await lazy.PgpSqliteDb2.updateAcceptance(
+ k.fpr,
+ allEmails,
+ acceptance
+ );
+ }
+ }
+ }
+
+ result.exitCode = 0;
+ await this.saveKeyRings();
+ }
+
+ for (let valTracker of secretKeyTrackers.values()) {
+ valTracker.release();
+ }
+
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ return result;
+ },
+
+ async deleteKey(keyFingerprint, deleteSecret) {
+ let handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_locate_key(
+ RNPLib.ffi,
+ "fingerprint",
+ keyFingerprint,
+ handle.address()
+ )
+ ) {
+ throw new Error("rnp_locate_key failed");
+ }
+
+ let flags = RNPLib.RNP_KEY_REMOVE_PUBLIC | RNPLib.RNP_KEY_REMOVE_SUBKEYS;
+ if (deleteSecret) {
+ flags |= RNPLib.RNP_KEY_REMOVE_SECRET;
+ }
+
+ if (RNPLib.rnp_key_remove(handle, flags)) {
+ throw new Error("rnp_key_remove failed");
+ }
+
+ RNPLib.rnp_key_handle_destroy(handle);
+ await this.saveKeyRings();
+ },
+
+ async revokeKey(keyFingerprint) {
+ let tracker =
+ RnpPrivateKeyUnlockTracker.constructFromFingerprint(keyFingerprint);
+ if (!tracker.available()) {
+ return;
+ }
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ await tracker.unlock();
+ if (!tracker.isUnlocked()) {
+ return;
+ }
+
+ let flags = 0;
+ let revokeResult = RNPLib.rnp_key_revoke(
+ tracker.getHandle(),
+ flags,
+ null,
+ null,
+ null
+ );
+ tracker.release();
+ if (revokeResult) {
+ throw new Error(
+ `rnp_key_revoke failed for fingerprint=${keyFingerprint}`
+ );
+ }
+ await this.saveKeyRings();
+ },
+
+ _getKeyHandleByKeyIdOrFingerprint(ffi, id, findPrimary) {
+ if (!id.startsWith("0x")) {
+ throw new Error("unexpected identifier " + id);
+ } else {
+ // remove 0x
+ id = id.substring(2);
+ }
+
+ let type = null;
+ if (id.length == 16) {
+ type = "keyid";
+ } else if (id.length == 40 || id.length == 32) {
+ type = "fingerprint";
+ } else {
+ throw new Error("key/fingerprint identifier of unexpected length: " + id);
+ }
+
+ let key = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_locate_key(ffi, type, id, key.address())) {
+ throw new Error("rnp_locate_key failed, " + type + ", " + id);
+ }
+
+ if (!key.isNull() && findPrimary) {
+ let is_subkey = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_sub(key, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (is_subkey.value) {
+ let primaryKey = this.getPrimaryKeyHandleFromSub(ffi, key);
+ RNPLib.rnp_key_handle_destroy(key);
+ key = primaryKey;
+ }
+ }
+
+ if (!key.isNull() && this.isBadKey(key, null, ffi)) {
+ RNPLib.rnp_key_handle_destroy(key);
+ key = new RNPLib.rnp_key_handle_t();
+ }
+
+ return key;
+ },
+
+ getPrimaryKeyHandleByKeyIdOrFingerprint(ffi, id) {
+ return this._getKeyHandleByKeyIdOrFingerprint(ffi, id, true);
+ },
+
+ getKeyHandleByKeyIdOrFingerprint(ffi, id) {
+ return this._getKeyHandleByKeyIdOrFingerprint(ffi, id, false);
+ },
+
+ async getKeyHandleByIdentifier(ffi, id) {
+ let key = null;
+
+ if (id.startsWith("<")) {
+ //throw new Error("search by email address not yet implemented: " + id);
+ if (!id.endsWith(">")) {
+ throw new Error(
+ "if search identifier starts with < then it must end with > : " + id
+ );
+ }
+ key = await this.findKeyByEmail(id);
+ } else {
+ key = this.getKeyHandleByKeyIdOrFingerprint(ffi, id);
+ }
+ return key;
+ },
+
+ isKeyUsableFor(key, usage) {
+ let allowed = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_allows_usage(key, usage, allowed.address())) {
+ throw new Error("rnp_key_allows_usage failed");
+ }
+ if (!allowed.value) {
+ return false;
+ }
+
+ if (usage != str_sign) {
+ return true;
+ }
+
+ return (
+ RNPLib.getSecretAvailableFromHandle(key) &&
+ RNPLib.isSecretKeyMaterialAvailable(key)
+ );
+ },
+
+ getSuitableSubkey(primary, usage) {
+ let sub_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_subkey_count(primary, sub_count.address())) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+
+ // For compatibility with GnuPG, when encrypting to a single subkey,
+ // encrypt to the most recently created subkey. (Bug 1665281)
+ let newest_created = null;
+ let newest_handle = null;
+
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (RNPLib.rnp_key_get_subkey_at(primary, i, sub_handle.address())) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+ let skip =
+ this.isBadKey(sub_handle, primary, null) ||
+ this.isKeyExpired(sub_handle);
+ if (!skip) {
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(sub_handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+ if (key_revoked.value) {
+ skip = true;
+ }
+ }
+ if (!skip) {
+ if (!this.isKeyUsableFor(sub_handle, usage)) {
+ skip = true;
+ }
+ }
+
+ if (!skip) {
+ let created = this.getKeyCreatedValueFromHandle(sub_handle);
+ if (!newest_handle || created > newest_created) {
+ if (newest_handle) {
+ RNPLib.rnp_key_handle_destroy(newest_handle);
+ }
+ newest_handle = sub_handle;
+ sub_handle = null;
+ newest_created = created;
+ }
+ }
+
+ if (sub_handle) {
+ RNPLib.rnp_key_handle_destroy(sub_handle);
+ }
+ }
+
+ return newest_handle;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible public key, for the given key
+ * that exactly matches the given userId.
+ *
+ * @param {rnp_key_handle_t} key - RNP key handle.
+ * @param {string} uidString - The userID to include.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ getSuitableEncryptKeyAsAutocrypt(key, userId) {
+ // Prefer usable subkeys, because they are always newer
+ // (or same age) as primary key.
+
+ let use_sub = this.getSuitableSubkey(key, str_encrypt);
+ if (!use_sub && !this.isKeyUsableFor(key, str_encrypt)) {
+ return "";
+ }
+
+ let result = this.getAutocryptKeyB64ByHandle(key, use_sub, userId);
+
+ if (use_sub) {
+ RNPLib.rnp_key_handle_destroy(use_sub);
+ }
+ return result;
+ },
+
+ addSuitableEncryptKey(key, op) {
+ // Prefer usable subkeys, because they are always newer
+ // (or same age) as primary key.
+
+ let use_sub = this.getSuitableSubkey(key, str_encrypt);
+ if (!use_sub && !this.isKeyUsableFor(key, str_encrypt)) {
+ throw new Error("no suitable subkey found for " + str_encrypt);
+ }
+
+ if (
+ RNPLib.rnp_op_encrypt_add_recipient(op, use_sub != null ? use_sub : key)
+ ) {
+ throw new Error("rnp_op_encrypt_add_recipient sender failed");
+ }
+ if (use_sub) {
+ RNPLib.rnp_key_handle_destroy(use_sub);
+ }
+ },
+
+ addAliasKeys(aliasKeys, op) {
+ for (let ak of aliasKeys) {
+ let key = this.getKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, "0x" + ak);
+ if (!key || key.isNull()) {
+ console.debug(
+ "addAliasKeys: cannot find key used by alias rule: " + ak
+ );
+ return false;
+ }
+ this.addSuitableEncryptKey(key, op);
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ return true;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible public key, for the given email
+ * address.
+ *
+ * @param {string} email - Use a userID with this email address.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ async getRecipientAutocryptKeyForEmail(email) {
+ email = email.toLowerCase();
+
+ let key = await this.findKeyByEmail("<" + email + ">", true);
+ if (!key || key.isNull()) {
+ return "";
+ }
+
+ let keyInfo = {};
+ let ok = this.getKeyInfoFromHandle(
+ RNPLib.ffi,
+ key,
+ keyInfo,
+ false,
+ false,
+ false
+ );
+ if (!ok) {
+ throw new Error("getKeyInfoFromHandle failed");
+ }
+
+ let result = "";
+ let userId = keyInfo.userIds.find(
+ uid =>
+ uid.type == "uid" &&
+ lazy.EnigmailFuncs.getEmailFromUserID(uid.userId).toLowerCase() == email
+ );
+ if (userId) {
+ result = this.getSuitableEncryptKeyAsAutocrypt(key, userId.userId);
+ }
+ RNPLib.rnp_key_handle_destroy(key);
+ return result;
+ },
+
+ async addEncryptionKeyForEmail(email, op) {
+ let key = await this.findKeyByEmail(email, true);
+ if (!key || key.isNull()) {
+ return false;
+ }
+ this.addSuitableEncryptKey(key, op);
+ RNPLib.rnp_key_handle_destroy(key);
+ return true;
+ },
+
+ getEmailWithoutBrackets(email) {
+ if (email.startsWith("<") && email.endsWith(">")) {
+ return email.substring(1, email.length - 1);
+ }
+ return email;
+ },
+
+ async encryptAndOrSign(plaintext, args, resultStatus) {
+ let signedInner;
+
+ if (args.sign && args.senderKeyIsExternal) {
+ if (!lazy.GPGME.allDependenciesLoaded()) {
+ throw new Error(
+ "invalid configuration, request to use external GnuPG key, but GPGME isn't working"
+ );
+ }
+ if (args.sigTypeClear) {
+ throw new Error(
+ "unexpected signing request with external GnuPG key configuration"
+ );
+ }
+
+ if (args.encrypt) {
+ // If we are asked to encrypt and sign at the same time, it
+ // means we're asked to produce the combined OpenPGP encoding.
+ // We ask GPG to produce a regular signature, and will then
+ // combine it with the encryption produced by RNP.
+ let orgEncrypt = args.encrypt;
+ args.encrypt = false;
+ signedInner = await lazy.GPGME.sign(plaintext, args, resultStatus);
+ args.encrypt = orgEncrypt;
+ } else {
+ // We aren't asked to encrypt, but sign only. That means the
+ // caller needs the detatched signature, either for MIME
+ // mime encoding with separate signature part, or for the nested
+ // approach with separate signing and encryption layers.
+ return lazy.GPGME.signDetached(plaintext, args, resultStatus);
+ }
+ }
+
+ resultStatus.exitCode = -1;
+ resultStatus.statusFlags = 0;
+ resultStatus.statusMsg = "";
+ resultStatus.errorMsg = "";
+
+ let data_array;
+ if (args.sign && args.senderKeyIsExternal) {
+ data_array = lazy.ctypes.uint8_t.array()(signedInner);
+ } else {
+ let arr = plaintext.split("").map(e => e.charCodeAt());
+ data_array = lazy.ctypes.uint8_t.array()(arr);
+ }
+
+ let input = new RNPLib.rnp_input_t();
+ if (
+ RNPLib.rnp_input_from_memory(
+ input.address(),
+ data_array,
+ data_array.length,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ let output = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ let op;
+ if (args.encrypt) {
+ op = new RNPLib.rnp_op_encrypt_t();
+ if (
+ RNPLib.rnp_op_encrypt_create(op.address(), RNPLib.ffi, input, output)
+ ) {
+ throw new Error("rnp_op_encrypt_create failed");
+ }
+ } else if (args.sign && !args.senderKeyIsExternal) {
+ op = new RNPLib.rnp_op_sign_t();
+ if (args.sigTypeClear) {
+ if (
+ RNPLib.rnp_op_sign_cleartext_create(
+ op.address(),
+ RNPLib.ffi,
+ input,
+ output
+ )
+ ) {
+ throw new Error("rnp_op_sign_cleartext_create failed");
+ }
+ } else if (args.sigTypeDetached) {
+ if (
+ RNPLib.rnp_op_sign_detached_create(
+ op.address(),
+ RNPLib.ffi,
+ input,
+ output
+ )
+ ) {
+ throw new Error("rnp_op_sign_detached_create failed");
+ }
+ } else {
+ throw new Error(
+ "not yet implemented scenario: signing, neither clear nor encrypt, without encryption"
+ );
+ }
+ } else {
+ throw new Error("invalid parameters, neither encrypt nor sign");
+ }
+
+ let senderKeyTracker = null;
+ let subKeyTracker = null;
+
+ try {
+ if ((args.sign && !args.senderKeyIsExternal) || args.encryptToSender) {
+ {
+ // Use a temporary scope to ensure the senderKey variable
+ // cannot be accessed later on.
+ let senderKey = await this.getKeyHandleByIdentifier(
+ RNPLib.ffi,
+ args.sender
+ );
+ if (!senderKey || senderKey.isNull()) {
+ return null;
+ }
+
+ senderKeyTracker = new RnpPrivateKeyUnlockTracker(senderKey);
+ senderKeyTracker.setAllowPromptingUserForPassword(true);
+ senderKeyTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ }
+
+ // Manually configured external key overrides the check for
+ // a valid personal key.
+ if (!args.senderKeyIsExternal) {
+ if (!senderKeyTracker.isSecret()) {
+ throw new Error(
+ `configured sender key ${args.sender} isn't available`
+ );
+ }
+ if (
+ !(await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(
+ senderKeyTracker.getFingerprint()
+ ))
+ ) {
+ throw new Error(
+ `configured sender key ${args.sender} isn't accepted as a personal key`
+ );
+ }
+ }
+
+ if (args.encryptToSender) {
+ this.addSuitableEncryptKey(senderKeyTracker.getHandle(), op);
+ }
+
+ if (args.sign && !args.senderKeyIsExternal) {
+ let signingKeyTrackerReference = senderKeyTracker;
+
+ // Prefer usable subkeys, because they are always newer
+ // (or same age) as primary key.
+ let usableSubKeyHandle = this.getSuitableSubkey(
+ senderKeyTracker.getHandle(),
+ str_sign
+ );
+ if (
+ !usableSubKeyHandle &&
+ !this.isKeyUsableFor(senderKeyTracker.getHandle(), str_sign)
+ ) {
+ throw new Error("no suitable (sub)key found for " + str_sign);
+ }
+ if (usableSubKeyHandle) {
+ subKeyTracker = new RnpPrivateKeyUnlockTracker(usableSubKeyHandle);
+ subKeyTracker.setAllowPromptingUserForPassword(true);
+ subKeyTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ if (subKeyTracker.available()) {
+ signingKeyTrackerReference = subKeyTracker;
+ }
+ }
+
+ await signingKeyTrackerReference.unlock();
+
+ if (args.encrypt) {
+ if (
+ RNPLib.rnp_op_encrypt_add_signature(
+ op,
+ signingKeyTrackerReference.getHandle(),
+ null
+ )
+ ) {
+ throw new Error("rnp_op_encrypt_add_signature failed");
+ }
+ } else if (
+ RNPLib.rnp_op_sign_add_signature(
+ op,
+ signingKeyTrackerReference.getHandle(),
+ null
+ )
+ ) {
+ throw new Error("rnp_op_sign_add_signature failed");
+ }
+ // This was just a reference, no ownership.
+ signingKeyTrackerReference = null;
+ }
+ }
+
+ if (args.encrypt) {
+ // If we have an alias definition, it will be used, and the usual
+ // lookup by email address will be skipped. Earlier code should
+ // have already checked that alias keys are available and usable
+ // for encryption, so we fail if a problem is found.
+
+ for (let rcpList of [args.to, args.bcc]) {
+ for (let rcpEmail of rcpList) {
+ rcpEmail = rcpEmail.toLowerCase();
+ let aliasKeys = args.aliasKeys.get(
+ this.getEmailWithoutBrackets(rcpEmail)
+ );
+ if (aliasKeys) {
+ if (!this.addAliasKeys(aliasKeys, op)) {
+ resultStatus.statusFlags |=
+ lazy.EnigmailConstants.INVALID_RECIPIENT;
+ return null;
+ }
+ } else if (!(await this.addEncryptionKeyForEmail(rcpEmail, op))) {
+ resultStatus.statusFlags |=
+ lazy.EnigmailConstants.INVALID_RECIPIENT;
+ return null;
+ }
+ }
+ }
+
+ if (AppConstants.MOZ_UPDATE_CHANNEL != "release") {
+ let debugKey = Services.prefs.getStringPref(
+ "mail.openpgp.debug.extra_encryption_key"
+ );
+ if (debugKey) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ debugKey
+ );
+ if (!handle.isNull()) {
+ console.debug("encrypting to debug key " + debugKey);
+ this.addSuitableEncryptKey(handle, op);
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+ }
+ }
+
+ // Don't use AEAD as long as RNP uses v5 packets which aren't
+ // widely compatible with other clients.
+ if (RNPLib.rnp_op_encrypt_set_aead(op, "NONE")) {
+ throw new Error("rnp_op_encrypt_set_aead failed");
+ }
+
+ if (RNPLib.rnp_op_encrypt_set_cipher(op, "AES256")) {
+ throw new Error("rnp_op_encrypt_set_cipher failed");
+ }
+
+ // TODO, map args.signatureHash string to RNP and call
+ // rnp_op_encrypt_set_hash
+ if (RNPLib.rnp_op_encrypt_set_hash(op, "SHA256")) {
+ throw new Error("rnp_op_encrypt_set_hash failed");
+ }
+
+ if (RNPLib.rnp_op_encrypt_set_armor(op, args.armor)) {
+ throw new Error("rnp_op_encrypt_set_armor failed");
+ }
+
+ if (args.sign && args.senderKeyIsExternal) {
+ if (RNPLib.rnp_op_encrypt_set_flags(op, RNPLib.RNP_ENCRYPT_NOWRAP)) {
+ throw new Error("rnp_op_encrypt_set_flags failed");
+ }
+ }
+
+ let rv = RNPLib.rnp_op_encrypt_execute(op);
+ if (rv) {
+ throw new Error("rnp_op_encrypt_execute failed: " + rv);
+ }
+ RNPLib.rnp_op_encrypt_destroy(op);
+ } else if (args.sign && !args.senderKeyIsExternal) {
+ if (RNPLib.rnp_op_sign_set_hash(op, "SHA256")) {
+ throw new Error("rnp_op_sign_set_hash failed");
+ }
+ // TODO, map args.signatureHash string to RNP and call
+ // rnp_op_encrypt_set_hash
+
+ if (RNPLib.rnp_op_sign_set_armor(op, args.armor)) {
+ throw new Error("rnp_op_sign_set_armor failed");
+ }
+
+ if (RNPLib.rnp_op_sign_execute(op)) {
+ throw new Error("rnp_op_sign_execute failed");
+ }
+ RNPLib.rnp_op_sign_destroy(op);
+ }
+ } finally {
+ if (subKeyTracker) {
+ subKeyTracker.release();
+ }
+ if (senderKeyTracker) {
+ senderKeyTracker.release();
+ }
+ }
+
+ RNPLib.rnp_input_destroy(input);
+
+ let result = null;
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ !RNPLib.rnp_output_memory_get_buf(
+ output,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(output);
+
+ resultStatus.exitCode = 0;
+
+ if (args.encrypt) {
+ resultStatus.statusFlags |= lazy.EnigmailConstants.END_ENCRYPTION;
+ }
+
+ if (args.sign) {
+ resultStatus.statusFlags |= lazy.EnigmailConstants.SIG_CREATED;
+ }
+
+ return result;
+ },
+
+ /**
+ * @param {number} expiryTime - Time to check, in seconds from the epoch.
+ * @returns {boolean} - true if the given time is after now.
+ */
+ isExpiredTime(expiryTime) {
+ if (!expiryTime) {
+ return false;
+ }
+ let nowSeconds = Math.floor(Date.now() / 1000);
+ return nowSeconds > expiryTime;
+ },
+
+ isKeyExpired(handle) {
+ let expiration = new lazy.ctypes.uint32_t();
+ if (RNPLib.rnp_key_get_expiration(handle, expiration.address())) {
+ throw new Error("rnp_key_get_expiration failed");
+ }
+ if (!expiration.value) {
+ return false;
+ }
+
+ let created = this.getKeyCreatedValueFromHandle(handle);
+ let expirationSeconds = created + expiration.value;
+ return this.isExpiredTime(expirationSeconds);
+ },
+
+ async findKeyByEmail(id, onlyIfAcceptableAsRecipientKey = false) {
+ if (!id.startsWith("<") || !id.endsWith(">") || id.includes(" ")) {
+ throw new Error(`Invalid argument; id=${id}`);
+ }
+
+ let emailWithoutBrackets = id.substring(1, id.length - 1);
+
+ let iter = new RNPLib.rnp_identifier_iterator_t();
+ let grip = new lazy.ctypes.char.ptr();
+
+ if (
+ RNPLib.rnp_identifier_iterator_create(RNPLib.ffi, iter.address(), "grip")
+ ) {
+ throw new Error("rnp_identifier_iterator_create failed");
+ }
+
+ let foundHandle = null;
+ let tentativeUnverifiedHandle = null;
+
+ while (
+ !foundHandle &&
+ !RNPLib.rnp_identifier_iterator_next(iter, grip.address())
+ ) {
+ if (grip.isNull()) {
+ break;
+ }
+
+ let have_handle = false;
+ let handle = new RNPLib.rnp_key_handle_t();
+
+ try {
+ let is_subkey = new lazy.ctypes.bool();
+ let uid_count = new lazy.ctypes.size_t();
+
+ if (RNPLib.rnp_locate_key(RNPLib.ffi, "grip", grip, handle.address())) {
+ throw new Error("rnp_locate_key failed");
+ }
+ have_handle = true;
+ if (RNPLib.rnp_key_is_sub(handle, is_subkey.address())) {
+ throw new Error("rnp_key_is_sub failed");
+ }
+ if (is_subkey.value) {
+ continue;
+ }
+ if (this.isBadKey(handle, null, RNPLib.ffi)) {
+ continue;
+ }
+ let key_revoked = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_is_revoked(handle, key_revoked.address())) {
+ throw new Error("rnp_key_is_revoked failed");
+ }
+
+ if (key_revoked.value) {
+ continue;
+ }
+
+ if (this.isKeyExpired(handle)) {
+ continue;
+ }
+
+ if (RNPLib.rnp_key_get_uid_count(handle, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+
+ let foundUid = false;
+ for (let i = 0; i < uid_count.value && !foundUid; i++) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+
+ if (
+ RNPLib.rnp_key_get_uid_handle_at(handle, i, uid_handle.address())
+ ) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+
+ if (!this.isBadUid(uid_handle) && !this.isRevokedUid(uid_handle)) {
+ let uid_str = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_uid_at(handle, i, uid_str.address())) {
+ throw new Error("rnp_key_get_uid_at failed");
+ }
+
+ let userId = uid_str.readStringReplaceMalformed();
+ RNPLib.rnp_buffer_destroy(uid_str);
+
+ if (
+ lazy.EnigmailFuncs.getEmailFromUserID(userId).toLowerCase() ==
+ emailWithoutBrackets
+ ) {
+ foundUid = true;
+
+ if (onlyIfAcceptableAsRecipientKey) {
+ // a key is acceptable, either:
+ // - without secret key, it's accepted verified or unverified
+ // - with secret key, must be marked as personal
+
+ let have_secret = new lazy.ctypes.bool();
+ if (RNPLib.rnp_key_have_secret(handle, have_secret.address())) {
+ throw new Error("rnp_key_have_secret failed");
+ }
+
+ let fingerprint = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_fprint(handle, fingerprint.address())) {
+ throw new Error("rnp_key_get_fprint failed");
+ }
+ let fpr = fingerprint.readString();
+ RNPLib.rnp_buffer_destroy(fingerprint);
+
+ if (have_secret.value) {
+ let isAccepted =
+ await lazy.PgpSqliteDb2.isAcceptedAsPersonalKey(fpr);
+ if (isAccepted) {
+ foundHandle = handle;
+ have_handle = false;
+ if (tentativeUnverifiedHandle) {
+ RNPLib.rnp_key_handle_destroy(tentativeUnverifiedHandle);
+ tentativeUnverifiedHandle = null;
+ }
+ }
+ } else {
+ let acceptanceResult = {};
+ try {
+ await lazy.PgpSqliteDb2.getAcceptance(
+ fpr,
+ emailWithoutBrackets,
+ acceptanceResult
+ );
+ } catch (ex) {
+ console.debug("getAcceptance failed: " + ex);
+ }
+
+ if (!acceptanceResult.emailDecided) {
+ continue;
+ }
+ if (acceptanceResult.fingerprintAcceptance == "unverified") {
+ /* keep searching for a better, verified key */
+ if (!tentativeUnverifiedHandle) {
+ tentativeUnverifiedHandle = handle;
+ have_handle = false;
+ }
+ } else if (
+ acceptanceResult.fingerprintAcceptance == "verified"
+ ) {
+ foundHandle = handle;
+ have_handle = false;
+ if (tentativeUnverifiedHandle) {
+ RNPLib.rnp_key_handle_destroy(tentativeUnverifiedHandle);
+ tentativeUnverifiedHandle = null;
+ }
+ }
+ }
+ } else {
+ foundHandle = handle;
+ have_handle = false;
+ }
+ }
+ }
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+ } catch (ex) {
+ console.log(ex);
+ } finally {
+ if (have_handle) {
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+ }
+ }
+
+ if (!foundHandle && tentativeUnverifiedHandle) {
+ foundHandle = tentativeUnverifiedHandle;
+ tentativeUnverifiedHandle = null;
+ }
+
+ RNPLib.rnp_identifier_iterator_destroy(iter);
+ return foundHandle;
+ },
+
+ async getPublicKey(id, store = RNPLib.ffi) {
+ let result = "";
+ let key = await this.getKeyHandleByIdentifier(store, id);
+
+ if (key.isNull()) {
+ return result;
+ }
+
+ let flags =
+ RNPLib.RNP_KEY_EXPORT_ARMORED |
+ RNPLib.RNP_KEY_EXPORT_PUBLIC |
+ RNPLib.RNP_KEY_EXPORT_SUBKEYS;
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(output_to_memory.address(), 0);
+
+ if (RNPLib.rnp_key_export(key, output_to_memory, flags)) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(output_to_memory);
+ RNPLib.rnp_key_handle_destroy(key);
+ return result;
+ },
+
+ /**
+ * Exports a public key, strips all signatures added by others,
+ * and optionally also strips user IDs. Self-signatures are kept.
+ * The given key handle will not be modified. The input key will be
+ * copied to a temporary area, only the temporary copy will be
+ * modified. The result key will be streamed to the given output.
+ *
+ * @param {rnp_key_handle_t} expKey - RNP key handle
+ * @param {boolean} keepUserIDs - if true keep users IDs
+ * @param {rnp_output_t} out_binary - output stream handle
+ *
+ */
+ export_pubkey_strip_sigs_uids(expKey, keepUserIDs, out_binary) {
+ let expKeyId = this.getKeyIDFromHandle(expKey);
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let exportFlags =
+ RNPLib.RNP_KEY_EXPORT_SUBKEYS | RNPLib.RNP_KEY_EXPORT_PUBLIC;
+ let importFlags = RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS;
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ if (RNPLib.rnp_key_export(expKey, output_to_memory, exportFlags)) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ }
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ result_buf,
+ result_len,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ if (RNPLib.rnp_import_keys(tempFFI, input_from_memory, importFlags, null)) {
+ throw new Error("rnp_import_keys failed");
+ }
+
+ let tempKey = this.getKeyHandleByKeyIdOrFingerprint(
+ tempFFI,
+ "0x" + expKeyId
+ );
+
+ // Strip
+
+ if (!keepUserIDs) {
+ let uid_count = new lazy.ctypes.size_t();
+ if (RNPLib.rnp_key_get_uid_count(tempKey, uid_count.address())) {
+ throw new Error("rnp_key_get_uid_count failed");
+ }
+ for (let i = uid_count.value; i > 0; i--) {
+ let uid_handle = new RNPLib.rnp_uid_handle_t();
+ if (
+ RNPLib.rnp_key_get_uid_handle_at(tempKey, i - 1, uid_handle.address())
+ ) {
+ throw new Error("rnp_key_get_uid_handle_at failed");
+ }
+ if (RNPLib.rnp_uid_remove(tempKey, uid_handle)) {
+ throw new Error("rnp_uid_remove failed");
+ }
+ RNPLib.rnp_uid_handle_destroy(uid_handle);
+ }
+ }
+
+ if (
+ RNPLib.rnp_key_remove_signatures(
+ tempKey,
+ RNPLib.RNP_KEY_SIGNATURE_NON_SELF_SIG,
+ null,
+ null
+ )
+ ) {
+ throw new Error("rnp_key_remove_signatures failed");
+ }
+
+ if (RNPLib.rnp_key_export(tempKey, out_binary, exportFlags)) {
+ throw new Error("rnp_key_export failed");
+ }
+ RNPLib.rnp_key_handle_destroy(tempKey);
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+ RNPLib.rnp_ffi_destroy(tempFFI);
+ },
+
+ /**
+ * Export one or multiple public keys.
+ *
+ * @param {string[]} idArrayFull - an array of key IDs or fingerprints
+ * that should be exported as full keys including all attributes.
+ * @param {string[]} idArrayReduced - an array of key IDs or
+ * fingerprints that should be exported with all self-signatures,
+ * but without signatures from others.
+ * @param {string[]} idArrayMinimal - an array of key IDs or
+ * fingerprints that should be exported as minimized keys.
+ * @returns {string} - An ascii armored key block containing all
+ * requested (available) keys.
+ */
+ getMultiplePublicKeys(idArrayFull, idArrayReduced, idArrayMinimal) {
+ let out_final = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(out_final.address(), 0);
+
+ let out_binary = new RNPLib.rnp_output_t();
+ let rv;
+ if (
+ (rv = RNPLib.rnp_output_to_armor(
+ out_final,
+ out_binary.address(),
+ "public key"
+ ))
+ ) {
+ throw new Error("rnp_output_to_armor failed:" + rv);
+ }
+
+ if ((rv = RNPLib.rnp_output_armor_set_line_length(out_binary, 64))) {
+ throw new Error("rnp_output_armor_set_line_length failed:" + rv);
+ }
+
+ let flags = RNPLib.RNP_KEY_EXPORT_PUBLIC | RNPLib.RNP_KEY_EXPORT_SUBKEYS;
+
+ if (idArrayFull) {
+ for (let id of idArrayFull) {
+ let key = this.getKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, id);
+ if (key.isNull()) {
+ continue;
+ }
+
+ if (RNPLib.rnp_key_export(key, out_binary, flags)) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ }
+
+ if (idArrayReduced) {
+ for (let id of idArrayReduced) {
+ let key = this.getPrimaryKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, id);
+ if (key.isNull()) {
+ continue;
+ }
+
+ this.export_pubkey_strip_sigs_uids(key, true, out_binary);
+
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ }
+
+ if (idArrayMinimal) {
+ for (let id of idArrayMinimal) {
+ let key = this.getPrimaryKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, id);
+ if (key.isNull()) {
+ continue;
+ }
+
+ this.export_pubkey_strip_sigs_uids(key, false, out_binary);
+
+ RNPLib.rnp_key_handle_destroy(key);
+ }
+ }
+
+ if ((rv = RNPLib.rnp_output_finish(out_binary))) {
+ throw new Error("rnp_output_finish failed: " + rv);
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ out_final,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ let result = "";
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(out_binary);
+ RNPLib.rnp_output_destroy(out_final);
+
+ return result;
+ },
+
+ /**
+ * The RNP library may store keys in a format that isn't compatible
+ * with GnuPG, see bug 1713621 for an example where this happened.
+ *
+ * This function modifies the input key to make it compatible.
+ *
+ * The caller must ensure that the key is unprotected when calling
+ * this function, and must apply the desired protection afterwards.
+ */
+ ensureECCSubkeyIsGnuPGCompatible(tempKey) {
+ let algo = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_alg(tempKey, algo.address())) {
+ throw new Error("rnp_key_get_alg failed");
+ }
+ let algoStr = algo.readString();
+ RNPLib.rnp_buffer_destroy(algo);
+
+ if (algoStr.toLowerCase() != "ecdh") {
+ return;
+ }
+
+ let curve = new lazy.ctypes.char.ptr();
+ if (RNPLib.rnp_key_get_curve(tempKey, curve.address())) {
+ throw new Error("rnp_key_get_curve failed");
+ }
+ let curveStr = curve.readString();
+ RNPLib.rnp_buffer_destroy(curve);
+
+ if (curveStr.toLowerCase() != "curve25519") {
+ return;
+ }
+
+ let tweak_status = new lazy.ctypes.bool();
+ let rc = RNPLib.rnp_key_25519_bits_tweaked(tempKey, tweak_status.address());
+ if (rc) {
+ throw new Error("rnp_key_25519_bits_tweaked failed: " + rc);
+ }
+
+ // If it's not tweaked yet, then tweak to make it compatible.
+ if (!tweak_status.value) {
+ rc = RNPLib.rnp_key_25519_bits_tweak(tempKey);
+ if (rc) {
+ throw new Error("rnp_key_25519_bits_tweak failed: " + rc);
+ }
+ }
+ },
+
+ async backupSecretKeys(fprs, backupPassword) {
+ if (!fprs.length) {
+ throw new Error("invalid fprs parameter");
+ }
+
+ /*
+ * Strategy:
+ * - copy keys to a temporary space, in-memory only (ffi)
+ * - if we failed to decrypt the secret keys, return null
+ * - change the password of all secret keys in the temporary space
+ * - export from the temporary space
+ */
+
+ let out_final = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(out_final.address(), 0);
+
+ let out_binary = new RNPLib.rnp_output_t();
+ let rv;
+ if (
+ (rv = RNPLib.rnp_output_to_armor(
+ out_final,
+ out_binary.address(),
+ "secret key"
+ ))
+ ) {
+ throw new Error("rnp_output_to_armor failed:" + rv);
+ }
+
+ let tempFFI = RNPLib.prepare_ffi();
+ if (!tempFFI) {
+ throw new Error("Couldn't initialize librnp.");
+ }
+
+ let exportFlags =
+ RNPLib.RNP_KEY_EXPORT_SUBKEYS | RNPLib.RNP_KEY_EXPORT_SECRET;
+ let importFlags =
+ RNPLib.RNP_LOAD_SAVE_PUBLIC_KEYS | RNPLib.RNP_LOAD_SAVE_SECRET_KEYS;
+
+ let unlockFailed = false;
+ let pwCache = {
+ passwords: [],
+ };
+
+ for (let fpr of fprs) {
+ let fprStr = fpr;
+ let expKey = await this.getKeyHandleByIdentifier(
+ RNPLib.ffi,
+ "0x" + fprStr
+ );
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ if (RNPLib.rnp_key_export(expKey, output_to_memory, exportFlags)) {
+ throw new Error("rnp_key_export failed");
+ }
+ RNPLib.rnp_key_handle_destroy(expKey);
+ expKey = null;
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ throw new Error("rnp_output_memory_get_buf failed");
+ }
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+ if (
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ result_buf,
+ result_len,
+ false
+ )
+ ) {
+ throw new Error("rnp_input_from_memory failed");
+ }
+
+ if (
+ RNPLib.rnp_import_keys(tempFFI, input_from_memory, importFlags, null)
+ ) {
+ throw new Error("rnp_import_keys failed");
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+ input_from_memory = null;
+ output_to_memory = null;
+ result_buf = null;
+
+ let tracker = RnpPrivateKeyUnlockTracker.constructFromFingerprint(
+ fprStr,
+ tempFFI
+ );
+ if (!tracker.available()) {
+ tracker.release();
+ continue;
+ }
+
+ tracker.setAllowPromptingUserForPassword(true);
+ tracker.setAllowAutoUnlockWithCachedPasswords(true);
+ tracker.setPasswordCache(pwCache);
+ tracker.setRememberUnlockPassword(true);
+
+ await tracker.unlock();
+ if (!tracker.isUnlocked()) {
+ unlockFailed = true;
+ tracker.release();
+ break;
+ }
+
+ tracker.unprotect();
+ tracker.setPassphrase(backupPassword);
+
+ let sub_count = new lazy.ctypes.size_t();
+ if (
+ RNPLib.rnp_key_get_subkey_count(
+ tracker.getHandle(),
+ sub_count.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_count failed");
+ }
+ for (let i = 0; i < sub_count.value; i++) {
+ let sub_handle = new RNPLib.rnp_key_handle_t();
+ if (
+ RNPLib.rnp_key_get_subkey_at(
+ tracker.getHandle(),
+ i,
+ sub_handle.address()
+ )
+ ) {
+ throw new Error("rnp_key_get_subkey_at failed");
+ }
+
+ let subTracker = new RnpPrivateKeyUnlockTracker(sub_handle);
+ if (subTracker.available()) {
+ subTracker.setAllowPromptingUserForPassword(true);
+ subTracker.setAllowAutoUnlockWithCachedPasswords(true);
+ subTracker.setPasswordCache(pwCache);
+ subTracker.setRememberUnlockPassword(true);
+
+ await subTracker.unlock();
+ if (!subTracker.isUnlocked()) {
+ unlockFailed = true;
+ } else {
+ subTracker.unprotect();
+ this.ensureECCSubkeyIsGnuPGCompatible(subTracker.getHandle());
+ subTracker.setPassphrase(backupPassword);
+ }
+ }
+ subTracker.release();
+ if (unlockFailed) {
+ break;
+ }
+ }
+
+ if (
+ !unlockFailed &&
+ RNPLib.rnp_key_export(tracker.getHandle(), out_binary, exportFlags)
+ ) {
+ throw new Error("rnp_key_export failed");
+ }
+
+ tracker.release();
+ if (unlockFailed) {
+ break;
+ }
+ }
+ RNPLib.rnp_ffi_destroy(tempFFI);
+
+ let result = "";
+ if (!unlockFailed) {
+ if ((rv = RNPLib.rnp_output_finish(out_binary))) {
+ throw new Error("rnp_output_finish failed: " + rv);
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ out_final,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = char_array.readString();
+ }
+ }
+
+ RNPLib.rnp_output_destroy(out_binary);
+ RNPLib.rnp_output_destroy(out_final);
+
+ return result;
+ },
+
+ async unlockAndGetNewRevocation(id, pass) {
+ let result = "";
+ let key = await this.getKeyHandleByIdentifier(RNPLib.ffi, id);
+
+ if (key.isNull()) {
+ return result;
+ }
+
+ let tracker = new RnpPrivateKeyUnlockTracker(key);
+ tracker.setAllowPromptingUserForPassword(false);
+ tracker.setAllowAutoUnlockWithCachedPasswords(false);
+ tracker.unlockWithPassword(pass);
+ if (!tracker.isUnlocked()) {
+ throw new Error(`Couldn't unlock key ${key.fpr}`);
+ }
+
+ let out_final = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(out_final.address(), 0);
+
+ let out_binary = new RNPLib.rnp_output_t();
+ let rv;
+ if (
+ (rv = RNPLib.rnp_output_to_armor(
+ out_final,
+ out_binary.address(),
+ "public key"
+ ))
+ ) {
+ throw new Error("rnp_output_to_armor failed:" + rv);
+ }
+
+ if (
+ (rv = RNPLib.rnp_key_export_revocation(
+ key,
+ out_binary,
+ 0,
+ null,
+ null,
+ null
+ ))
+ ) {
+ throw new Error("rnp_key_export_revocation failed: " + rv);
+ }
+
+ if ((rv = RNPLib.rnp_output_finish(out_binary))) {
+ throw new Error("rnp_output_finish failed: " + rv);
+ }
+
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let exitCode = RNPLib.rnp_output_memory_get_buf(
+ out_final,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!exitCode) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_output_destroy(out_binary);
+ RNPLib.rnp_output_destroy(out_final);
+ tracker.release();
+ return result;
+ },
+
+ enArmorString(input, type) {
+ let arr = input.split("").map(e => e.charCodeAt());
+ let input_array = lazy.ctypes.uint8_t.array()(arr);
+
+ return this.enArmorCData(input_array, input_array.length, type);
+ },
+
+ enArmorCDataMessage(buf, len) {
+ return this.enArmorCData(buf, len, "message");
+ },
+
+ enArmorCData(buf, len, type) {
+ let input_array = lazy.ctypes.cast(buf, lazy.ctypes.uint8_t.array(len));
+
+ let input_from_memory = new RNPLib.rnp_input_t();
+ RNPLib.rnp_input_from_memory(
+ input_from_memory.address(),
+ input_array,
+ len,
+ false
+ );
+
+ let max_out = len * 2 + 150; // extra bytes for head/tail/hash lines
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ RNPLib.rnp_output_to_memory(output_to_memory.address(), max_out);
+
+ if (RNPLib.rnp_enarmor(input_from_memory, output_to_memory, type)) {
+ throw new Error("rnp_enarmor failed");
+ }
+
+ let result = "";
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ if (
+ !RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ )
+ ) {
+ let char_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.char.array(result_len.value).ptr
+ ).contents;
+
+ result = char_array.readString();
+ }
+
+ RNPLib.rnp_input_destroy(input_from_memory);
+ RNPLib.rnp_output_destroy(output_to_memory);
+
+ return result;
+ },
+
+ // Will change the expiration date of all given keys to newExpiry.
+ // fingerprintArray is an array, containing fingerprints, both
+ // primary key fingerprints and subkey fingerprints are allowed.
+ // The function assumes that all involved keys have already been
+ // unlocked. We shouldn't rely on password callbacks for unlocking,
+ // as it would be confusing if only some keys are changed.
+ async changeExpirationDate(fingerprintArray, newExpiry) {
+ for (let fingerprint of fingerprintArray) {
+ let handle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ "0x" + fingerprint
+ );
+
+ if (handle.isNull()) {
+ continue;
+ }
+
+ if (RNPLib.rnp_key_set_expiration(handle, newExpiry)) {
+ throw new Error(`rnp_key_set_expiration failed for ${fingerprint}`);
+ }
+ RNPLib.rnp_key_handle_destroy(handle);
+ }
+
+ await this.saveKeyRings();
+ return true;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible key for the given key handles.
+ * If subkey is given, it must refer to an existing encryption subkey.
+ * This is a wrapper around RNP function rnp_key_export_autocrypt.
+ *
+ * @param {rnp_key_handle_t} primHandle - The handle of a primary key.
+ * @param {?rnp_key_handle_t} subHandle - The handle of an encryption subkey or null.
+ * @param {string} uidString - The userID to include.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ getAutocryptKeyB64ByHandle(primHandle, subHandle, userId) {
+ if (primHandle.isNull()) {
+ throw new Error("getAutocryptKeyB64ByHandle invalid parameter");
+ }
+
+ let output_to_memory = new RNPLib.rnp_output_t();
+ if (RNPLib.rnp_output_to_memory(output_to_memory.address(), 0)) {
+ throw new Error("rnp_output_to_memory failed");
+ }
+
+ let result = "";
+
+ if (
+ RNPLib.rnp_key_export_autocrypt(
+ primHandle,
+ subHandle,
+ userId,
+ output_to_memory,
+ 0
+ )
+ ) {
+ console.debug("rnp_key_export_autocrypt failed");
+ } else {
+ let result_buf = new lazy.ctypes.uint8_t.ptr();
+ let result_len = new lazy.ctypes.size_t();
+ let rv = RNPLib.rnp_output_memory_get_buf(
+ output_to_memory,
+ result_buf.address(),
+ result_len.address(),
+ false
+ );
+
+ if (!rv) {
+ // result_len is of type UInt64, I don't know of a better way
+ // to convert it to an integer.
+ let b_len = parseInt(result_len.value.toString());
+
+ // type casting the pointer type to an array type allows us to
+ // access the elements by index.
+ let uint8_array = lazy.ctypes.cast(
+ result_buf,
+ lazy.ctypes.uint8_t.array(result_len.value).ptr
+ ).contents;
+
+ let str = "";
+ for (let i = 0; i < b_len; i++) {
+ str += String.fromCharCode(uint8_array[i]);
+ }
+
+ result = btoa(str);
+ }
+ }
+
+ RNPLib.rnp_output_destroy(output_to_memory);
+
+ return result;
+ },
+
+ /**
+ * Get a minimal Autocrypt-compatible key for the given key ID.
+ * If subKeyId is given, it must refer to an existing encryption subkey.
+ * This is a wrapper around RNP function rnp_key_export_autocrypt.
+ *
+ * @param {string} primaryKeyId - The ID of a primary key.
+ * @param {?string} subKeyId - The ID of an encryption subkey or null.
+ * @param {string} uidString - The userID to include.
+ * @returns {string} The encoded key, or the empty string on failure.
+ */
+ getAutocryptKeyB64(primaryKeyId, subKeyId, uidString) {
+ let subHandle = null;
+
+ if (subKeyId) {
+ subHandle = this.getKeyHandleByKeyIdOrFingerprint(RNPLib.ffi, subKeyId);
+ if (subHandle.isNull()) {
+ // Although subKeyId is optional, if it's given, it must be valid.
+ return "";
+ }
+ }
+
+ let primHandle = this.getKeyHandleByKeyIdOrFingerprint(
+ RNPLib.ffi,
+ primaryKeyId
+ );
+
+ let result = this.getAutocryptKeyB64ByHandle(
+ primHandle,
+ subHandle,
+ uidString
+ );
+
+ if (!primHandle.isNull()) {
+ RNPLib.rnp_key_handle_destroy(primHandle);
+ }
+ if (subHandle) {
+ RNPLib.rnp_key_handle_destroy(subHandle);
+ }
+ return result;
+ },
+
+ /**
+ * Helper function to produce the string that will be shown to the
+ * user, when the user is asked to unlock a key. If the key is a
+ * subkey, it might help to user to identify the respective key by
+ * also mentioning the key ID of the primary key, so both IDs are
+ * shown when prompting to unlock a subkey.
+ * Parameter nonDefaultFFI is required, if the prompt is related to
+ * a key that isn't (yet) stored in the global storage, for example
+ * a key that is being prepared for import or export in a temporary
+ * ffi space.
+ *
+ * @param {rnp_key_handle_t} handle - produce a passphrase prompt
+ * string based on the properties of this key.
+ * @param {rnp_ffi_t} ffi - the RNP FFI that relates the handle
+ * @returns {String} - a string that asks the user to enter the
+ * passphrase for the given string parameter, including details
+ * that allow the user to identify the key.
+ */
+ async getPassphrasePrompt(handle, ffi) {
+ let parentOfHandle = this.getPrimaryKeyHandleIfSub(ffi, handle);
+ let useThisHandle = !parentOfHandle.isNull() ? parentOfHandle : handle;
+
+ let keyObj = {};
+ if (
+ !this.getKeyInfoFromHandle(ffi, useThisHandle, keyObj, false, true, true)
+ ) {
+ return "";
+ }
+
+ let mainKeyId = keyObj.keyId;
+ let subKeyId;
+ if (!parentOfHandle.isNull()) {
+ subKeyId = this.getKeyIDFromHandle(handle);
+ }
+
+ if (subKeyId) {
+ return l10n.formatValue("passphrase-prompt2-sub", {
+ subkey: subKeyId,
+ key: mainKeyId,
+ date: keyObj.created,
+ username_and_email: keyObj.userId,
+ });
+ }
+ return l10n.formatValue("passphrase-prompt2", {
+ key: mainKeyId,
+ date: keyObj.created,
+ username_and_email: keyObj.userId,
+ });
+ },
+};