summaryrefslogtreecommitdiffstats
path: root/comm/mail/extensions/openpgp/content/modules/keyObj.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/extensions/openpgp/content/modules/keyObj.jsm')
-rw-r--r--comm/mail/extensions/openpgp/content/modules/keyObj.jsm679
1 files changed, 679 insertions, 0 deletions
diff --git a/comm/mail/extensions/openpgp/content/modules/keyObj.jsm b/comm/mail/extensions/openpgp/content/modules/keyObj.jsm
new file mode 100644
index 0000000000..ed9137cb3a
--- /dev/null
+++ b/comm/mail/extensions/openpgp/content/modules/keyObj.jsm
@@ -0,0 +1,679 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["newEnigmailKeyObj"];
+
+/**
+ This module implements the EnigmailKeyObj class with the following members:
+
+ - keyId - 16 digits (8-byte) public key ID (/not/ preceded with 0x)
+ - userId - main user ID
+ - fpr - fingerprint
+ - fprFormatted - a formatted version of the fingerprint following the scheme .... .... ....
+ - expiry - Expiry date as printable string
+ - expiryTime - Expiry time as seconds after 01/01/1970
+ - created - Key creation date as printable string
+ - keyCreated - Key creation date/time as number
+ - keyTrust - key trust code as provided by GnuPG (calculated key validity)
+ - keyUseFor - key usage type as provided by GnuPG (key capabilities)
+ - ownerTrust - owner trust as provided by GnuPG
+ - photoAvailable - [Boolean] true if photo is available
+ - secretAvailable - [Boolean] true if secret key is available
+ - algoSym - public key algorithm type (String, e.g. RSA)
+ - keySize - size of public key
+ - type - "pub" or "grp"
+ - userIds - [Array]: - Contains ALL UIDs (including the primary UID)
+ * userId - User ID
+ * keyTrust - trust level of user ID
+ * uidFpr - fingerprint of the user ID
+ * type - one of "uid" (regular user ID), "uat" (photo)
+ * uatNum - photo number (starting with 0 for each key)
+ - subKeys - [Array]:
+ * keyId - subkey ID (16 digits (8-byte))
+ * expiry - Expiry date as printable string
+ * expiryTime - Expiry time as seconds after 01/01/1970
+ * created - Subkey creation date as printable string
+ * keyCreated - Subkey creation date/time as number
+ * keyTrust - key trust code as provided by GnuPG
+ * keyUseFor - key usage type as provided by GnuPG
+ * algoSym - subkey algorithm type (String, e.g. RSA)
+ * keySize - subkey size
+ * type - "sub"
+
+ - methods:
+ * hasSubUserIds
+ * getKeyExpiry
+ * getEncryptionValidity
+ * getSigningValidity
+ * getPubKeyValidity
+ * clone
+ * getMinimalPubKey
+ * getVirtualKeySize
+*/
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ EnigmailCryptoAPI: "chrome://openpgp/content/modules/cryptoAPI.jsm",
+ EnigmailFuncs: "chrome://openpgp/content/modules/funcs.jsm",
+ EnigmailKey: "chrome://openpgp/content/modules/key.jsm",
+ EnigmailLog: "chrome://openpgp/content/modules/log.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "l10n", () => {
+ return new Localization(["messenger/openpgp/openpgp.ftl"], true);
+});
+
+function newEnigmailKeyObj(keyData) {
+ return new EnigmailKeyObj(keyData);
+}
+
+class EnigmailKeyObj {
+ constructor(keyData) {
+ this.keyId = "";
+ this.expiry = "";
+ this.expiryTime = 0;
+ this.created = "";
+ this.keyTrust = "";
+ this.keyUseFor = "";
+ this.ownerTrust = "";
+ this.algoSym = "";
+ this.keySize = "";
+ this.userId = "";
+ this.userIds = [];
+ this.subKeys = [];
+ this.fpr = "";
+ this.minimalKeyBlock = [];
+ this.photoAvailable = false;
+ this.secretAvailable = false;
+ this.secretMaterial = false;
+
+ this.type = keyData.type;
+ if ("keyId" in keyData) {
+ this.keyId = keyData.keyId;
+ }
+ if ("expiryTime" in keyData) {
+ this.expiryTime = keyData.expiryTime;
+ this.expiry = keyData.expiryTime
+ ? new Services.intl.DateTimeFormat().format(
+ new Date(keyData.expiryTime * 1000)
+ )
+ : "";
+ }
+ if ("effectiveExpiryTime" in keyData) {
+ this.effectiveExpiryTime = keyData.effectiveExpiryTime;
+ this.effectiveExpiry = keyData.effectiveExpiryTime
+ ? new Services.intl.DateTimeFormat().format(
+ new Date(keyData.effectiveExpiryTime * 1000)
+ )
+ : "";
+ }
+
+ const ATTRS = [
+ "created",
+ "keyCreated",
+ "keyTrust",
+ "keyUseFor",
+ "ownerTrust",
+ "algoSym",
+ "keySize",
+ "userIds",
+ "subKeys",
+ "fpr",
+ "secretAvailable",
+ "secretMaterial",
+ "photoAvailable",
+ "userId",
+ "hasIgnoredAttributes",
+ ];
+ for (let i of ATTRS) {
+ if (i in keyData) {
+ this[i] = keyData[i];
+ }
+ }
+ }
+
+ /**
+ * create a copy of the object
+ */
+ clone() {
+ let cp = new EnigmailKeyObj(["copy"]);
+ for (let i in this) {
+ if (i !== "fprFormatted") {
+ if (typeof this[i] !== "function") {
+ if (typeof this[i] === "object") {
+ cp[i] = lazy.EnigmailFuncs.cloneObj(this[i]);
+ } else {
+ cp[i] = this[i];
+ }
+ }
+ }
+ }
+
+ return cp;
+ }
+
+ /**
+ * Does the key have secondary user IDs?
+ *
+ * @return: Boolean - true if yes; false if no
+ */
+ hasSubUserIds() {
+ let nUid = 0;
+ for (let i in this.userIds) {
+ if (this.userIds[i].type === "uid") {
+ ++nUid;
+ }
+ }
+
+ return nUid >= 2;
+ }
+
+ /**
+ * Get a formatted version of the fingerprint:
+ * 1234 5678 90AB CDEF .... ....
+ *
+ * @returns String - the formatted fingerprint
+ */
+ get fprFormatted() {
+ let f = lazy.EnigmailKey.formatFpr(this.fpr);
+ if (f.length === 0) {
+ f = this.fpr;
+ }
+ return f;
+ }
+
+ /**
+ * Determine if the public key is valid. If not, return a description why it's not
+ *
+ * @returns Object:
+ * - keyValid: Boolean (true if key is valid)
+ * - reason: String (explanation of invalidity)
+ */
+ getPubKeyValidity(exceptionReason = null) {
+ let retVal = {
+ keyValid: false,
+ reason: "",
+ };
+ if (this.keyTrust.search(/r/i) >= 0) {
+ // public key revoked
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-pub-key-revoked", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ } else if (
+ exceptionReason != "ignoreExpired" &&
+ this.keyTrust.search(/e/i) >= 0
+ ) {
+ // public key expired
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-pub-key-expired", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ } else {
+ retVal.keyValid = true;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Check whether a key can be used for signing and return a description of why not
+ *
+ * @returns Object:
+ * - keyValid: Boolean (true if key is valid)
+ * - reason: String (explanation of invalidity)
+ */
+ getSigningValidity(exceptionReason = null) {
+ let retVal = this.getPubKeyValidity(exceptionReason);
+
+ if (!retVal.keyValid) {
+ return retVal;
+ }
+
+ if (!this.secretAvailable) {
+ retVal.keyValid = false;
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-no-secret-key", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ return retVal;
+ }
+
+ if (/s/.test(this.keyUseFor) && this.secretMaterial) {
+ return retVal;
+ }
+
+ retVal.keyValid = false;
+ let expired = 0;
+ let revoked = 0;
+ let found = 0;
+ let noSecret = 0;
+
+ for (let sk in this.subKeys) {
+ if (this.subKeys[sk].keyUseFor.search(/s/) >= 0) {
+ if (
+ this.subKeys[sk].keyTrust.search(/e/i) >= 0 &&
+ exceptionReason != "ignoreExpired"
+ ) {
+ ++expired;
+ } else if (this.subKeys[sk].keyTrust.search(/r/i) >= 0) {
+ ++revoked;
+ } else if (!this.subKeys[sk].secretMaterial) {
+ ++noSecret;
+ } else {
+ // found subkey usable
+ ++found;
+ }
+ }
+ }
+
+ if (!found) {
+ if (exceptionReason != "ignoreExpired" && expired) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-sign-sub-keys-expired",
+ {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ }
+ );
+ } else if (revoked) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-sign-sub-keys-revoked",
+ {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ }
+ );
+ } else if (noSecret) {
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-no-secret-key", {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ });
+ } else {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-pub-key-not-for-signing",
+ {
+ userId: this.userId,
+ keyId: "0x" + this.keyId,
+ }
+ );
+ }
+ } else {
+ retVal.keyValid = true;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Check whether a key can be used for encryption and return a description of why not
+ *
+ * @param {boolean} requireDecryptionKey:
+ * If true, require secret key material to be available
+ * for at least one encryption key.
+ * @param {string} exceptionReason:
+ * Can be used to override the requirement to check for
+ * full validity, and accept certain scenarios as valid.
+ * If value is set to "ignoreExpired",
+ * then an expired key isn't treated as invalid.
+ * Set to null to get the default behavior.
+ * @param {string} subId:
+ * A key ID of a subkey or null.
+ * If subId is null, any part of the key will be
+ * considered when looking for a valid encryption key.
+ * If subId is non-null, only this subkey will be
+ * checked.
+ *
+ * @returns Object:
+ * - keyValid: Boolean (true if key is valid)
+ * - reason: String (explanation of invalidity)
+ */
+ getEncryptionValidity(
+ requireDecryptionKey,
+ exceptionReason = null,
+ subId = null
+ ) {
+ let retVal = this.getPubKeyValidity(exceptionReason);
+ if (!retVal.keyValid) {
+ return retVal;
+ }
+
+ if (
+ !subId &&
+ this.keyUseFor.search(/e/) >= 0 &&
+ (!requireDecryptionKey || this.secretMaterial)
+ ) {
+ // We can stop and return the result we already found,
+ // because we aren't looking at a specific subkey (!subId),
+ // and the primary key is usable for encryption.
+ // If we must own secret key material (requireDecryptionKey),
+ // in this scenario it's sufficient to have secret material for
+ // the primary key.
+ return retVal;
+ }
+
+ retVal.keyValid = false;
+
+ let expired = 0;
+ let revoked = 0;
+ let found = 0;
+ let noSecret = 0;
+
+ for (let sk of this.subKeys) {
+ if (subId && subId != sk.keyId) {
+ continue;
+ }
+
+ if (sk.keyUseFor.search(/e/) >= 0) {
+ if (
+ sk.keyTrust.search(/e/i) >= 0 &&
+ exceptionReason != "ignoreExpired"
+ ) {
+ ++expired;
+ } else if (sk.keyTrust.search(/r/i) >= 0) {
+ ++revoked;
+ } else if (requireDecryptionKey && !sk.secretMaterial) {
+ ++noSecret;
+ } else {
+ // found subkey usable
+ ++found;
+ }
+ }
+ }
+
+ if (!found) {
+ let idToShow = subId ? subId : this.keyId;
+
+ if (exceptionReason != "ignoreExpired" && expired) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-enc-sub-keys-expired",
+ {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ }
+ );
+ } else if (revoked) {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-enc-sub-keys-revoked",
+ {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ }
+ );
+ } else if (noSecret) {
+ retVal.reason = lazy.l10n.formatValueSync("key-ring-no-secret-key", {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ });
+ } else {
+ retVal.reason = lazy.l10n.formatValueSync(
+ "key-ring-pub-key-not-for-encryption",
+ {
+ userId: this.userId,
+ keyId: "0x" + idToShow,
+ }
+ );
+ }
+ } else {
+ retVal.keyValid = true;
+ }
+
+ return retVal;
+ }
+
+ /**
+ * Determine the next expiry date of the key. This is either the public key expiry date,
+ * or the maximum expiry date of a signing or encryption subkey. I.e. this returns the next
+ * date at which the key cannot be used for signing and/or encryption anymore
+ *
+ * @returns Number - The expiry date as seconds after 01/01/1970
+ */
+ getKeyExpiry() {
+ let expiryDate = Number.MAX_VALUE;
+ let encryption = -1;
+ let signing = -1;
+
+ // check public key expiry date
+ if (this.expiryTime > 0) {
+ expiryDate = this.expiryTime;
+ }
+
+ for (let sk in this.subKeys) {
+ if (this.subKeys[sk].keyUseFor.search(/[eE]/) >= 0) {
+ let expiry = this.subKeys[sk].expiryTime;
+ if (expiry === 0) {
+ expiry = Number.MAX_VALUE;
+ }
+ encryption = Math.max(encryption, expiry);
+ } else if (this.subKeys[sk].keyUseFor.search(/[sS]/) >= 0) {
+ let expiry = this.subKeys[sk].expiryTime;
+ if (expiry === 0) {
+ expiry = Number.MAX_VALUE;
+ }
+ signing = Math.max(signing, expiry);
+ }
+ }
+
+ if (expiryDate > encryption) {
+ if (this.keyUseFor.search(/[eE]/) < 0) {
+ expiryDate = encryption;
+ }
+ }
+
+ if (expiryDate > signing) {
+ if (this.keyUseFor.search(/[Ss]/) < 0) {
+ expiryDate = signing;
+ }
+ }
+
+ return expiryDate;
+ }
+
+ /**
+ * Export the minimum key for the public key object:
+ * public key, desired UID, newest signing/encryption subkey
+ *
+ * @param {string} emailAddr: [optional] email address of UID to extract. Use primary UID if null .
+ *
+ * @returns Object:
+ * - exitCode (0 = success)
+ * - errorMsg (if exitCode != 0)
+ * - keyData: BASE64-encded string of key data
+ */
+ getMinimalPubKey(emailAddr) {
+ lazy.EnigmailLog.DEBUG(
+ "keyObj.jsm: EnigmailKeyObj.getMinimalPubKey: " + this.keyId + "\n"
+ );
+
+ if (emailAddr) {
+ try {
+ emailAddr = lazy.EnigmailFuncs.stripEmail(emailAddr.toLowerCase());
+ } catch (x) {
+ emailAddr = emailAddr.toLowerCase();
+ }
+
+ let foundUid = false,
+ uid = "";
+ for (let i in this.userIds) {
+ try {
+ uid = lazy.EnigmailFuncs.stripEmail(
+ this.userIds[i].userId.toLowerCase()
+ );
+ } catch (x) {
+ uid = this.userIds[i].userId.toLowerCase();
+ }
+
+ if (uid == emailAddr) {
+ foundUid = true;
+ break;
+ }
+ }
+ if (!foundUid) {
+ emailAddr = false;
+ }
+ }
+
+ if (!emailAddr) {
+ emailAddr = this.userId;
+ }
+
+ try {
+ emailAddr = lazy.EnigmailFuncs.stripEmail(emailAddr.toLowerCase());
+ } catch (x) {
+ emailAddr = emailAddr.toLowerCase();
+ }
+
+ let newestSigningKey = 0,
+ newestEncryptionKey = 0,
+ subkeysArr = null;
+
+ // search for valid subkeys
+ for (let sk in this.subKeys) {
+ if (!"indDre".includes(this.subKeys[sk].keyTrust)) {
+ if (this.subKeys[sk].keyUseFor.search(/[sS]/) >= 0) {
+ // found signing subkey
+ if (this.subKeys[sk].keyCreated > newestSigningKey) {
+ newestSigningKey = this.subKeys[sk].keyCreated;
+ }
+ }
+ if (this.subKeys[sk].keyUseFor.search(/[eE]/) >= 0) {
+ // found encryption subkey
+ if (this.subKeys[sk].keyCreated > newestEncryptionKey) {
+ newestEncryptionKey = this.subKeys[sk].keyCreated;
+ }
+ }
+ }
+ }
+
+ if (newestSigningKey > 0 && newestEncryptionKey > 0) {
+ subkeysArr = [newestEncryptionKey, newestSigningKey];
+ }
+
+ if (!(emailAddr in this.minimalKeyBlock)) {
+ const cApi = lazy.EnigmailCryptoAPI();
+ this.minimalKeyBlock[emailAddr] = cApi.sync(
+ cApi.getMinimalPubKey(this.fpr, emailAddr, subkeysArr)
+ );
+ }
+ return this.minimalKeyBlock[emailAddr];
+ }
+
+ /**
+ * Obtain a "virtual" key size that allows to compare different algorithms with each other
+ * e.g. elliptic curve keys have small key sizes with high cryptographic strength
+ *
+ *
+ * @returns Number: a virtual size
+ */
+ getVirtualKeySize() {
+ lazy.EnigmailLog.DEBUG(
+ "keyObj.jsm: EnigmailKeyObj.getVirtualKeySize: " + this.keyId + "\n"
+ );
+
+ switch (this.algoSym) {
+ case "DSA":
+ return this.keySize / 2;
+ case "ECDSA":
+ return this.keySize * 8;
+ case "EDDSA":
+ return this.keySize * 32;
+ default:
+ return this.keySize;
+ }
+ }
+
+ /**
+ * @param {boolean} minimalKey - if true, reduce key to minimum required
+ *
+ * @returns {object}:
+ * - {Number} exitCode: result code (0: OK)
+ * - {String} keyData: ASCII armored key data material
+ * - {String} errorMsg: error message in case exitCode !== 0
+ */
+ getSecretKey(minimalKey) {
+ const cApi = lazy.EnigmailCryptoAPI();
+ return cApi.sync(cApi.extractSecretKey(this.fpr, minimalKey));
+ }
+
+ iSimpleOneSubkeySameExpiry() {
+ if (this.subKeys.length == 0) {
+ return true;
+ }
+
+ if (this.subKeys.length > 1) {
+ return false;
+ }
+
+ let subKey = this.subKeys[0];
+
+ if (!this.expiryTime && !subKey.expiryTime) {
+ return true;
+ }
+
+ let deltaSeconds = this.expiryTime - subKey.expiryTime;
+ if (deltaSeconds < 0) {
+ deltaSeconds *= -1;
+ }
+
+ // If expiry dates differ by less than a half day, then we
+ // treat it as having roughly the same expiry date.
+ return deltaSeconds < 12 * 60 * 60;
+ }
+
+ /**
+ * Obtain the list of alternative email addresses, except the one
+ * that is given as the parameter.
+ *
+ * @param {boolean} exceptThisEmail - an email address that will
+ * be excluded in the result array.
+ * @returns {string[]} - an array of all email addresses found in all
+ * of the key's user IDs, excluding exceptThisEmail.
+ */
+ getAlternativeEmails(exceptThisEmail) {
+ let result = [];
+
+ for (let u of this.userIds) {
+ let email;
+ try {
+ email = lazy.EnigmailFuncs.stripEmail(u.userId.toLowerCase());
+ } catch (x) {
+ email = u.userId.toLowerCase();
+ }
+
+ if (email == exceptThisEmail) {
+ continue;
+ }
+
+ result.push(email);
+ }
+
+ return result;
+ }
+
+ getUserIdWithEmail(email) {
+ for (let u of this.userIds) {
+ let e;
+ try {
+ e = lazy.EnigmailFuncs.stripEmail(u.userId.toLowerCase());
+ } catch (x) {
+ e = u.userId.toLowerCase();
+ }
+
+ if (email == e) {
+ return u;
+ }
+ }
+
+ return null;
+ }
+}