From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../extensions/openpgp/content/modules/keyObj.jsm | 679 +++++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 comm/mail/extensions/openpgp/content/modules/keyObj.jsm (limited to 'comm/mail/extensions/openpgp/content/modules/keyObj.jsm') 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; + } +} -- cgit v1.2.3