diff --git a/src/cleartext.js b/src/cleartext.js index 92b03dfe..37c03c58 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -32,117 +32,117 @@ import { Signature } from './signature'; import { createVerificationObjects, createSignaturePackets } from './message'; /** - * @class - * @classdesc Class that represents an OpenPGP cleartext signed message. + * Class that represents an OpenPGP cleartext signed message. * See {@link https://tools.ietf.org/html/rfc4880#section-7} - * @param {String} text The cleartext of the signed message - * @param {module:signature.Signature} signature The detached signature or an empty signature for unsigned messages */ -export function CleartextMessage(text, signature) { - if (!(this instanceof CleartextMessage)) { - return new CleartextMessage(text, signature); - } - // normalize EOL to canonical form - this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n'); - if (signature && !(signature instanceof Signature)) { - throw new Error('Invalid signature input'); +export class CleartextMessage { + /** + * @param {String} text The cleartext of the signed message + * @param {module:signature.Signature} signature The detached signature or an empty signature for unsigned messages + */ + constructor(text, signature) { + // normalize EOL to canonical form + this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n'); + if (signature && !(signature instanceof Signature)) { + throw new Error('Invalid signature input'); + } + this.signature = signature || new Signature(new PacketList()); } - this.signature = signature || new Signature(new PacketList()); -} -/** - * Returns the key IDs of the keys that signed the cleartext message - * @returns {Array} array of keyid objects - */ -CleartextMessage.prototype.getSigningKeyIds = function() { - const keyIds = []; - const signatureList = this.signature.packets; - signatureList.forEach(function(packet) { - keyIds.push(packet.issuerKeyId); - }); - return keyIds; -}; - -/** - * Sign the cleartext message - * @param {Array} privateKeys private keys with decrypted secret key data for signing - * @param {Signature} signature (optional) any existing detached signature - * @param {Date} date (optional) The creation time of the signature that should be created - * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] - * @returns {Promise} new cleartext message with signed content - * @async - */ -CleartextMessage.prototype.sign = async function(privateKeys, signature = null, date = new Date(), userIds = []) { - return new CleartextMessage(this.text, await this.signDetached(privateKeys, signature, date, userIds)); -}; + /** + * Returns the key IDs of the keys that signed the cleartext message + * @returns {Array} array of keyid objects + */ + getSigningKeyIds() { + const keyIds = []; + const signatureList = this.signature.packets; + signatureList.forEach(function(packet) { + keyIds.push(packet.issuerKeyId); + }); + return keyIds; + } -/** - * Sign the cleartext message - * @param {Array} privateKeys private keys with decrypted secret key data for signing - * @param {Signature} signature (optional) any existing detached signature - * @param {Date} date (optional) The creation time of the signature that should be created - * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] - * @returns {Promise} new detached signature of message content - * @async - */ -CleartextMessage.prototype.signDetached = async function(privateKeys, signature = null, date = new Date(), userIds = []) { - const literalDataPacket = new LiteralDataPacket(); - literalDataPacket.setText(this.text); + /** + * Sign the cleartext message + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @param {Signature} signature (optional) any existing detached signature + * @param {Date} date (optional) The creation time of the signature that should be created + * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @returns {Promise} new cleartext message with signed content + * @async + */ + async sign(privateKeys, signature = null, date = new Date(), userIds = []) { + return new CleartextMessage(this.text, await this.signDetached(privateKeys, signature, date, userIds)); + } - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true)); -}; + /** + * Sign the cleartext message + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @param {Signature} signature (optional) any existing detached signature + * @param {Date} date (optional) The creation time of the signature that should be created + * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @returns {Promise} new detached signature of message content + * @async + */ + async signDetached(privateKeys, signature = null, date = new Date(), userIds = []) { + const literalDataPacket = new LiteralDataPacket(); + literalDataPacket.setText(this.text); + + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true)); + } -/** - * Verify signatures of cleartext signed message - * @param {Array} keys array of keys to verify signatures - * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @returns {Promise>} list of signer's keyid and validity of signature - * @async - */ -CleartextMessage.prototype.verify = function(keys, date = new Date()) { - return this.verifyDetached(this.signature, keys, date); -}; + /** + * Verify signatures of cleartext signed message + * @param {Array} keys array of keys to verify signatures + * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @returns {Promise>} list of signer's keyid and validity of signature + * @async + */ + verify(keys, date = new Date()) { + return this.verifyDetached(this.signature, keys, date); + } -/** - * Verify signatures of cleartext signed message - * @param {Array} keys array of keys to verify signatures - * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @returns {Promise>} list of signer's keyid and validity of signature - * @async - */ -CleartextMessage.prototype.verifyDetached = function(signature, keys, date = new Date()) { - const signatureList = signature.packets; - const literalDataPacket = new LiteralDataPacket(); - // we assume that cleartext signature is generated based on UTF8 cleartext - literalDataPacket.setText(this.text); - return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true); -}; + /** + * Verify signatures of cleartext signed message + * @param {Array} keys array of keys to verify signatures + * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @returns {Promise>} list of signer's keyid and validity of signature + * @async + */ + verifyDetached(signature, keys, date = new Date()) { + const signatureList = signature.packets; + const literalDataPacket = new LiteralDataPacket(); + // we assume that cleartext signature is generated based on UTF8 cleartext + literalDataPacket.setText(this.text); + return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true); + } -/** - * Get cleartext - * @returns {String} cleartext of message - */ -CleartextMessage.prototype.getText = function() { - // normalize end of line to \n - return this.text.replace(/\r\n/g, '\n'); -}; + /** + * Get cleartext + * @returns {String} cleartext of message + */ + getText() { + // normalize end of line to \n + return this.text.replace(/\r\n/g, '\n'); + } -/** - * Returns ASCII armored text of cleartext signed message - * @returns {String | ReadableStream} ASCII armor - */ -CleartextMessage.prototype.armor = function() { - let hashes = this.signature.packets.map(function(packet) { - return enums.read(enums.hash, packet.hashAlgorithm).toUpperCase(); - }); - hashes = hashes.filter(function(item, i, ar) { return ar.indexOf(item) === i; }); - const body = { - hash: hashes.join(), - text: this.text, - data: this.signature.packets.write() - }; - return armor.encode(enums.armor.signed, body); -}; + /** + * Returns ASCII armored text of cleartext signed message + * @returns {String | ReadableStream} ASCII armor + */ + armor() { + let hashes = this.signature.packets.map(function(packet) { + return enums.read(enums.hash, packet.hashAlgorithm).toUpperCase(); + }); + hashes = hashes.filter(function(item, i, ar) { return ar.indexOf(item) === i; }); + const body = { + hash: hashes.join(), + text: this.text, + data: this.signature.packets.write() + }; + return armor.encode(enums.armor.signed, body); + } +} /** diff --git a/src/config/localStorage.js b/src/config/localStorage.js index 8b1f0947..f3ae4e16 100644 --- a/src/config/localStorage.js +++ b/src/config/localStorage.js @@ -5,31 +5,30 @@ /** * This object is used for storing and retrieving configuration from HTML5 local storage. - * @constructor */ -function LocalStorage() {} - -/** - * Reads the config out of the HTML5 local storage - * and initializes the object config. - * if config is null the default config will be used - */ -LocalStorage.prototype.read = function () { - const raw = globalThis.localStorage.getItem("config"); - const cf = (raw === null ? null : JSON.parse(raw)); - if (cf === null) { - this.config = this.default_config; - this.write(); - } else { - this.config = cf; +class LocalStorage { + /** + * Reads the config out of the HTML5 local storage + * and initializes the object config. + * if config is null the default config will be used + */ + read() { + const raw = globalThis.localStorage.getItem("config"); + const cf = (raw === null ? null : JSON.parse(raw)); + if (cf === null) { + this.config = this.default_config; + this.write(); + } else { + this.config = cf; + } } -}; -/** - * Writes the config to HTML5 local storage - */ -LocalStorage.prototype.write = function () { - globalThis.localStorage.setItem("config", JSON.stringify(this.config)); -}; + /** + * Writes the config to HTML5 local storage + */ + write() { + globalThis.localStorage.setItem("config", JSON.stringify(this.config)); + } +} export default LocalStorage; diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index 308c1e88..1f92dbc9 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -137,80 +137,79 @@ const curves = { } }; -/** - * @constructor - */ -function Curve(oid_or_name, params) { - try { - if (util.isArray(oid_or_name) || - util.isUint8Array(oid_or_name)) { - // by oid byte array - oid_or_name = new OID(oid_or_name); - } - if (oid_or_name instanceof OID) { - // by curve OID - oid_or_name = oid_or_name.getName(); +class Curve { + constructor(oid_or_name, params) { + try { + if (util.isArray(oid_or_name) || + util.isUint8Array(oid_or_name)) { + // by oid byte array + oid_or_name = new OID(oid_or_name); + } + if (oid_or_name instanceof OID) { + // by curve OID + oid_or_name = oid_or_name.getName(); + } + // by curve name or oid string + this.name = enums.write(enums.curve, oid_or_name); + } catch (err) { + throw new Error('Not valid curve'); } - // by curve name or oid string - this.name = enums.write(enums.curve, oid_or_name); - } catch (err) { - throw new Error('Not valid curve'); - } - params = params || curves[this.name]; + params = params || curves[this.name]; - this.keyType = params.keyType; + this.keyType = params.keyType; - this.oid = params.oid; - this.hash = params.hash; - this.cipher = params.cipher; - this.node = params.node && curves[this.name]; - this.web = params.web && curves[this.name]; - this.payloadSize = params.payloadSize; - if (this.web && util.getWebCrypto()) { - this.type = 'web'; - } else if (this.node && util.getNodeCrypto()) { - this.type = 'node'; - } else if (this.name === 'curve25519') { - this.type = 'curve25519'; - } else if (this.name === 'ed25519') { - this.type = 'ed25519'; + this.oid = params.oid; + this.hash = params.hash; + this.cipher = params.cipher; + this.node = params.node && curves[this.name]; + this.web = params.web && curves[this.name]; + this.payloadSize = params.payloadSize; + if (this.web && util.getWebCrypto()) { + this.type = 'web'; + } else if (this.node && util.getNodeCrypto()) { + this.type = 'node'; + } else if (this.name === 'curve25519') { + this.type = 'curve25519'; + } else if (this.name === 'ed25519') { + this.type = 'ed25519'; + } } -} -Curve.prototype.genKeyPair = async function () { - let keyPair; - switch (this.type) { - case 'web': - try { - return await webGenKeyPair(this.name); - } catch (err) { - util.printDebugError("Browser did not support generating ec key " + err.message); - break; + async genKeyPair() { + let keyPair; + switch (this.type) { + case 'web': + try { + return await webGenKeyPair(this.name); + } catch (err) { + util.printDebugError("Browser did not support generating ec key " + err.message); + break; + } + case 'node': + return nodeGenKeyPair(this.name); + case 'curve25519': { + const privateKey = await random.getRandomBytes(32); + privateKey[0] = (privateKey[0] & 127) | 64; + privateKey[31] &= 248; + const secretKey = privateKey.slice().reverse(); + keyPair = nacl.box.keyPair.fromSecretKey(secretKey); + const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); + return { publicKey, privateKey }; + } + case 'ed25519': { + const privateKey = await random.getRandomBytes(32); + const keyPair = nacl.sign.keyPair.fromSeed(privateKey); + const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); + return { publicKey, privateKey }; } - case 'node': - return nodeGenKeyPair(this.name); - case 'curve25519': { - const privateKey = await random.getRandomBytes(32); - privateKey[0] = (privateKey[0] & 127) | 64; - privateKey[31] &= 248; - const secretKey = privateKey.slice().reverse(); - keyPair = nacl.box.keyPair.fromSecretKey(secretKey); - const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); - return { publicKey, privateKey }; - } - case 'ed25519': { - const privateKey = await random.getRandomBytes(32); - const keyPair = nacl.sign.keyPair.fromSeed(privateKey); - const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); - return { publicKey, privateKey }; } + const indutnyCurve = await getIndutnyCurve(this.name); + keyPair = await indutnyCurve.genKeyPair({ + entropy: util.uint8ArrayToStr(await random.getRandomBytes(32)) + }); + return { publicKey: new Uint8Array(keyPair.getPublic('array', false)), privateKey: keyPair.getPrivate().toArrayLike(Uint8Array) }; } - const indutnyCurve = await getIndutnyCurve(this.name); - keyPair = await indutnyCurve.genKeyPair({ - entropy: util.uint8ArrayToStr(await random.getRandomBytes(32)) - }); - return { publicKey: new Uint8Array(keyPair.getPublic('array', false)), privateKey: keyPair.getPrivate().toArrayLike(Uint8Array) }; -}; +} async function generate(curve) { curve = new Curve(curve); diff --git a/src/crypto/random.js b/src/crypto/random.js index d372d628..021e4e28 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -82,64 +82,66 @@ export default { /** * Buffer for secure random numbers */ -function RandomBuffer() { - this.buffer = null; - this.size = null; - this.callback = null; -} - -/** - * Initialize buffer - * @param {Integer} size size of buffer - */ -RandomBuffer.prototype.init = function(size, callback) { - this.buffer = new Uint8Array(size); - this.size = 0; - this.callback = callback; -}; - -/** - * Concat array of secure random numbers to buffer - * @param {Uint8Array} buf - */ -RandomBuffer.prototype.set = function(buf) { - if (!this.buffer) { - throw new Error('RandomBuffer is not initialized'); +class RandomBuffer { + constructor() { + this.buffer = null; + this.size = null; + this.callback = null; } - if (!(buf instanceof Uint8Array)) { - throw new Error('Invalid type: buf not an Uint8Array'); - } - const freeSpace = this.buffer.length - this.size; - if (buf.length > freeSpace) { - buf = buf.subarray(0, freeSpace); - } - // set buf with offset old size of buffer - this.buffer.set(buf, this.size); - this.size += buf.length; -}; -/** - * Take numbers out of buffer and copy to array - * @param {Uint8Array} buf the destination array - */ -RandomBuffer.prototype.get = async function(buf) { - if (!this.buffer) { - throw new Error('RandomBuffer is not initialized'); - } - if (!(buf instanceof Uint8Array)) { - throw new Error('Invalid type: buf not an Uint8Array'); + /** + * Initialize buffer + * @param {Integer} size size of buffer + */ + init(size, callback) { + this.buffer = new Uint8Array(size); + this.size = 0; + this.callback = callback; } - if (this.size < buf.length) { - if (!this.callback) { - throw new Error('Random number buffer depleted'); + + /** + * Concat array of secure random numbers to buffer + * @param {Uint8Array} buf + */ + set(buf) { + if (!this.buffer) { + throw new Error('RandomBuffer is not initialized'); } - // Wait for random bytes from main context, then try again - await this.callback(); - return this.get(buf); + if (!(buf instanceof Uint8Array)) { + throw new Error('Invalid type: buf not an Uint8Array'); + } + const freeSpace = this.buffer.length - this.size; + if (buf.length > freeSpace) { + buf = buf.subarray(0, freeSpace); + } + // set buf with offset old size of buffer + this.buffer.set(buf, this.size); + this.size += buf.length; } - for (let i = 0; i < buf.length; i++) { - buf[i] = this.buffer[--this.size]; - // clear buffer value - this.buffer[this.size] = 0; + + /** + * Take numbers out of buffer and copy to array + * @param {Uint8Array} buf the destination array + */ + async get(buf) { + if (!this.buffer) { + throw new Error('RandomBuffer is not initialized'); + } + if (!(buf instanceof Uint8Array)) { + throw new Error('Invalid type: buf not an Uint8Array'); + } + if (this.size < buf.length) { + if (!this.callback) { + throw new Error('Random number buffer depleted'); + } + // Wait for random bytes from main context, then try again + await this.callback(); + return this.get(buf); + } + for (let i = 0; i < buf.length; i++) { + buf[i] = this.buffer[--this.size]; + // clear buffer value + this.buffer[this.size] = 0; + } } -}; +} diff --git a/src/hkp.js b/src/hkp.js index 82cdba84..084bfe3d 100644 --- a/src/hkp.js +++ b/src/hkp.js @@ -23,67 +23,68 @@ import config from './config'; -/** - * Initialize the HKP client and configure it with the key server url and fetch function. - * @constructor - * @param {String} keyServerBaseUrl (optional) The HKP key server base url including - * the protocol to use, e.g. 'https://pgp.mit.edu'; defaults to - * openpgp.config.keyserver (https://keyserver.ubuntu.com) - */ -function HKP(keyServerBaseUrl) { - this._baseUrl = keyServerBaseUrl || config.keyserver; - this._fetch = typeof globalThis.fetch === 'function' ? globalThis.fetch : require('node-fetch'); -} - -/** - * Search for a public key on the key server either by key ID or part of the user ID. - * @param {String} options.keyId The long public key ID. - * @param {String} options.query This can be any part of the key user ID such as name - * or email address. - * @returns {Promise} The ascii armored public key. - * @async - */ -HKP.prototype.lookup = function(options) { - let uri = this._baseUrl + '/pks/lookup?op=get&options=mr&search='; - const fetch = this._fetch; - - if (options.keyId) { - uri += '0x' + encodeURIComponent(options.keyId); - } else if (options.query) { - uri += encodeURIComponent(options.query); - } else { - throw new Error('You must provide a query parameter!'); +class HKP { + /** + * Initialize the HKP client and configure it with the key server url and fetch function. + * @param {String} keyServerBaseUrl (optional) The HKP key server base url including + * the protocol to use, e.g. 'https://pgp.mit.edu'; defaults to + * openpgp.config.keyserver (https://keyserver.ubuntu.com) + */ + constructor(keyServerBaseUrl) { + this._baseUrl = keyServerBaseUrl || config.keyserver; + this._fetch = typeof globalThis.fetch === 'function' ? globalThis.fetch : require('node-fetch'); } - return fetch(uri).then(function(response) { - if (response.status === 200) { - return response.text(); - } - }).then(function(publicKeyArmored) { - if (!publicKeyArmored || publicKeyArmored.indexOf('-----END PGP PUBLIC KEY BLOCK-----') < 0) { - return; + /** + * Search for a public key on the key server either by key ID or part of the user ID. + * @param {String} options.keyId The long public key ID. + * @param {String} options.query This can be any part of the key user ID such as name + * or email address. + * @returns {Promise} The ascii armored public key. + * @async + */ + lookup(options) { + let uri = this._baseUrl + '/pks/lookup?op=get&options=mr&search='; + const fetch = this._fetch; + + if (options.keyId) { + uri += '0x' + encodeURIComponent(options.keyId); + } else if (options.query) { + uri += encodeURIComponent(options.query); + } else { + throw new Error('You must provide a query parameter!'); } - return publicKeyArmored.trim(); - }); -}; -/** - * Upload a public key to the server. - * @param {String} publicKeyArmored An ascii armored public key to be uploaded. - * @returns {Promise} - * @async - */ -HKP.prototype.upload = function(publicKeyArmored) { - const uri = this._baseUrl + '/pks/add'; - const fetch = this._fetch; + return fetch(uri).then(function(response) { + if (response.status === 200) { + return response.text(); + } + }).then(function(publicKeyArmored) { + if (!publicKeyArmored || publicKeyArmored.indexOf('-----END PGP PUBLIC KEY BLOCK-----') < 0) { + return; + } + return publicKeyArmored.trim(); + }); + } - return fetch(uri, { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, - body: 'keytext=' + encodeURIComponent(publicKeyArmored) - }); -}; + /** + * Upload a public key to the server. + * @param {String} publicKeyArmored An ascii armored public key to be uploaded. + * @returns {Promise} + * @async + */ + upload(publicKeyArmored) { + const uri = this._baseUrl + '/pks/add'; + const fetch = this._fetch; + + return fetch(uri, { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + body: 'keytext=' + encodeURIComponent(publicKeyArmored) + }); + } +} export default HKP; diff --git a/src/key/key.js b/src/key/key.js index ac82caf4..d648c907 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -39,10 +39,8 @@ import SubKey from './subkey'; import * as helper from './helper'; /** - * @class - * @classdesc Class that represents an OpenPGP key. Must contain a primary key. + * Class that represents an OpenPGP key. Must contain a primary key. * Can contain additional subkeys, signatures, user ids, user attributes. - * @param {PacketList} packetlist The packets that form this key * @borrows PublicKeyPacket#getKeyId as Key#getKeyId * @borrows PublicKeyPacket#getFingerprint as Key#getFingerprint * @borrows PublicKeyPacket#hasSameFingerprintAs as Key#hasSameFingerprintAs @@ -50,861 +48,867 @@ import * as helper from './helper'; * @borrows PublicKeyPacket#getCreationTime as Key#getCreationTime * @borrows PublicKeyPacket#isDecrypted as Key#isDecrypted */ -export default function Key(packetlist) { - if (!(this instanceof Key)) { - return new Key(packetlist); - } - // same data as in packetlist but in structured form - this.keyPacket = null; - this.revocationSignatures = []; - this.directSignatures = []; - this.users = []; - this.subKeys = []; - this.packetlist2structure(packetlist); - if (!this.keyPacket || !this.users.length) { - throw new Error('Invalid key: need at least key and user ID packet'); +class Key { + /** + * @param {PacketList} packetlist The packets that form this key + */ + constructor(packetlist) { + if (!(this instanceof Key)) { + return new Key(packetlist); + } + // same data as in packetlist but in structured form + this.keyPacket = null; + this.revocationSignatures = []; + this.directSignatures = []; + this.users = []; + this.subKeys = []; + this.packetlist2structure(packetlist); + if (!this.keyPacket || !this.users.length) { + throw new Error('Invalid key: need at least key and user ID packet'); + } } -} -Object.defineProperty(Key.prototype, 'primaryKey', { - get() { + get primaryKey() { return this.keyPacket; - }, - configurable: true, - enumerable: true -}); + } -/** - * Transforms packetlist to structured key data - * @param {PacketList} packetlist The packets that form a key - */ -Key.prototype.packetlist2structure = function(packetlist) { - let user; - let primaryKeyId; - let subKey; - for (let i = 0; i < packetlist.length; i++) { - switch (packetlist[i].tag) { - case enums.packet.publicKey: - case enums.packet.secretKey: - if (this.keyPacket) { - throw new Error('Key block contains multiple keys'); - } - this.keyPacket = packetlist[i]; - primaryKeyId = this.getKeyId(); - break; - case enums.packet.userID: - case enums.packet.userAttribute: - user = new User(packetlist[i]); - this.users.push(user); - break; - case enums.packet.publicSubkey: - case enums.packet.secretSubkey: - user = null; - subKey = new SubKey(packetlist[i]); - this.subKeys.push(subKey); - break; - case enums.packet.signature: - switch (packetlist[i].signatureType) { - case enums.signature.certGeneric: - case enums.signature.certPersona: - case enums.signature.certCasual: - case enums.signature.certPositive: - if (!user) { - util.printDebug('Dropping certification signatures without preceding user packet'); - continue; - } - if (packetlist[i].issuerKeyId.equals(primaryKeyId)) { - user.selfCertifications.push(packetlist[i]); - } else { - user.otherCertifications.push(packetlist[i]); - } - break; - case enums.signature.certRevocation: - if (user) { - user.revocationSignatures.push(packetlist[i]); - } else { + /** + * Transforms packetlist to structured key data + * @param {PacketList} packetlist The packets that form a key + */ + packetlist2structure(packetlist) { + let user; + let primaryKeyId; + let subKey; + for (let i = 0; i < packetlist.length; i++) { + switch (packetlist[i].tag) { + case enums.packet.publicKey: + case enums.packet.secretKey: + if (this.keyPacket) { + throw new Error('Key block contains multiple keys'); + } + this.keyPacket = packetlist[i]; + primaryKeyId = this.getKeyId(); + break; + case enums.packet.userID: + case enums.packet.userAttribute: + user = new User(packetlist[i]); + this.users.push(user); + break; + case enums.packet.publicSubkey: + case enums.packet.secretSubkey: + user = null; + subKey = new SubKey(packetlist[i]); + this.subKeys.push(subKey); + break; + case enums.packet.signature: + switch (packetlist[i].signatureType) { + case enums.signature.certGeneric: + case enums.signature.certPersona: + case enums.signature.certCasual: + case enums.signature.certPositive: + if (!user) { + util.printDebug('Dropping certification signatures without preceding user packet'); + continue; + } + if (packetlist[i].issuerKeyId.equals(primaryKeyId)) { + user.selfCertifications.push(packetlist[i]); + } else { + user.otherCertifications.push(packetlist[i]); + } + break; + case enums.signature.certRevocation: + if (user) { + user.revocationSignatures.push(packetlist[i]); + } else { + this.directSignatures.push(packetlist[i]); + } + break; + case enums.signature.key: this.directSignatures.push(packetlist[i]); - } - break; - case enums.signature.key: - this.directSignatures.push(packetlist[i]); - break; - case enums.signature.subkeyBinding: - if (!subKey) { - util.printDebug('Dropping subkey binding signature without preceding subkey packet'); - continue; - } - subKey.bindingSignatures.push(packetlist[i]); - break; - case enums.signature.keyRevocation: - this.revocationSignatures.push(packetlist[i]); - break; - case enums.signature.subkeyRevocation: - if (!subKey) { - util.printDebug('Dropping subkey revocation signature without preceding subkey packet'); - continue; - } - subKey.revocationSignatures.push(packetlist[i]); - break; - } - break; + break; + case enums.signature.subkeyBinding: + if (!subKey) { + util.printDebug('Dropping subkey binding signature without preceding subkey packet'); + continue; + } + subKey.bindingSignatures.push(packetlist[i]); + break; + case enums.signature.keyRevocation: + this.revocationSignatures.push(packetlist[i]); + break; + case enums.signature.subkeyRevocation: + if (!subKey) { + util.printDebug('Dropping subkey revocation signature without preceding subkey packet'); + continue; + } + subKey.revocationSignatures.push(packetlist[i]); + break; + } + break; + } } } -}; -/** - * Transforms structured key data to packetlist - * @returns {PacketList} The packets that form a key - */ -Key.prototype.toPacketlist = function() { - const packetlist = new PacketList(); - packetlist.push(this.keyPacket); - packetlist.concat(this.revocationSignatures); - packetlist.concat(this.directSignatures); - this.users.map(user => packetlist.concat(user.toPacketlist())); - this.subKeys.map(subKey => packetlist.concat(subKey.toPacketlist())); - return packetlist; -}; - -/** - * Clones the key object - * @param {type/keyid} deep Whether to clone each packet, in addition to the list of packets - * @returns {Promise} cloned key - * @async - */ -Key.prototype.clone = async function(deep = false) { - if (deep) { + /** + * Transforms structured key data to packetlist + * @returns {PacketList} The packets that form a key + */ + toPacketlist() { const packetlist = new PacketList(); - await packetlist.read(this.toPacketlist().write(), helper.allowedKeyPackets); - return new Key(packetlist); - } - return new Key(this.toPacketlist()); -}; - -/** - * Returns an array containing all public or private subkeys matching keyId; - * If keyId is not present, returns all subkeys. - * @param {type/keyid} keyId - * @returns {Array} - */ -Key.prototype.getSubkeys = function(keyId = null) { - const subKeys = []; - this.subKeys.forEach(subKey => { - if (!keyId || subKey.getKeyId().equals(keyId, true)) { - subKeys.push(subKey); + packetlist.push(this.keyPacket); + packetlist.concat(this.revocationSignatures); + packetlist.concat(this.directSignatures); + this.users.map(user => packetlist.concat(user.toPacketlist())); + this.subKeys.map(subKey => packetlist.concat(subKey.toPacketlist())); + return packetlist; + } + + /** + * Clones the key object + * @param {type/keyid} deep Whether to clone each packet, in addition to the list of packets + * @returns {Promise} cloned key + * @async + */ + async clone(deep = false) { + if (deep) { + const packetlist = new PacketList(); + await packetlist.read(this.toPacketlist().write(), helper.allowedKeyPackets); + return new Key(packetlist); } - }); - return subKeys; -}; - -/** - * Returns an array containing all public or private keys matching keyId. - * If keyId is not present, returns all keys starting with the primary key. - * @param {type/keyid} keyId - * @returns {Array} - */ -Key.prototype.getKeys = function(keyId = null) { - const keys = []; - if (!keyId || this.getKeyId().equals(keyId, true)) { - keys.push(this); + return new Key(this.toPacketlist()); + } + + /** + * Returns an array containing all public or private subkeys matching keyId; + * If keyId is not present, returns all subkeys. + * @param {type/keyid} keyId + * @returns {Array} + */ + getSubkeys(keyId = null) { + const subKeys = []; + this.subKeys.forEach(subKey => { + if (!keyId || subKey.getKeyId().equals(keyId, true)) { + subKeys.push(subKey); + } + }); + return subKeys; + } + + /** + * Returns an array containing all public or private keys matching keyId. + * If keyId is not present, returns all keys starting with the primary key. + * @param {type/keyid} keyId + * @returns {Array} + */ + getKeys(keyId = null) { + const keys = []; + if (!keyId || this.getKeyId().equals(keyId, true)) { + keys.push(this); + } + return keys.concat(this.getSubkeys(keyId)); } - return keys.concat(this.getSubkeys(keyId)); -}; - -/** - * Returns key IDs of all keys - * @returns {Array} - */ -Key.prototype.getKeyIds = function() { - return this.getKeys().map(key => key.getKeyId()); -}; - -/** - * Returns userids - * @returns {Array} array of userids - */ -Key.prototype.getUserIds = function() { - return this.users.map(user => { - return user.userId ? user.userId.userid : null; - }).filter(userid => userid !== null); -}; -/** - * Returns true if this is a public key - * @returns {Boolean} - */ -Key.prototype.isPublic = function() { - return this.keyPacket.tag === enums.packet.publicKey; -}; + /** + * Returns key IDs of all keys + * @returns {Array} + */ + getKeyIds() { + return this.getKeys().map(key => key.getKeyId()); + } -/** - * Returns true if this is a private key - * @returns {Boolean} - */ -Key.prototype.isPrivate = function() { - return this.keyPacket.tag === enums.packet.secretKey; -}; + /** + * Returns userids + * @returns {Array} array of userids + */ + getUserIds() { + return this.users.map(user => { + return user.userId ? user.userId.userid : null; + }).filter(userid => userid !== null); + } -/** - * Returns key as public key (shallow copy) - * @returns {module:key.Key} new public Key - */ -Key.prototype.toPublic = function() { - const packetlist = new PacketList(); - const keyPackets = this.toPacketlist(); - let bytes; - let pubKeyPacket; - let pubSubkeyPacket; - for (let i = 0; i < keyPackets.length; i++) { - switch (keyPackets[i].tag) { - case enums.packet.secretKey: - bytes = keyPackets[i].writePublicKey(); - pubKeyPacket = new PublicKeyPacket(); - pubKeyPacket.read(bytes); - packetlist.push(pubKeyPacket); - break; - case enums.packet.secretSubkey: - bytes = keyPackets[i].writePublicKey(); - pubSubkeyPacket = new PublicSubkeyPacket(); - pubSubkeyPacket.read(bytes); - packetlist.push(pubSubkeyPacket); - break; - default: - packetlist.push(keyPackets[i]); - } - } - return new Key(packetlist); -}; + /** + * Returns true if this is a public key + * @returns {Boolean} + */ + isPublic() { + return this.keyPacket.tag === enums.packet.publicKey; + } -/** - * Returns ASCII armored text of key - * @returns {ReadableStream} ASCII armor - */ -Key.prototype.armor = function() { - const type = this.isPublic() ? enums.armor.publicKey : enums.armor.privateKey; - return armor.encode(type, this.toPacketlist().write()); -}; + /** + * Returns true if this is a private key + * @returns {Boolean} + */ + isPrivate() { + return this.keyPacket.tag === enums.packet.secretKey; + } -/** - * Returns last created key or key by given keyId that is available for signing and verification - * @param {module:type/keyid} keyId, optional - * @param {Date} date (optional) use the given date for verification instead of the current time - * @param {Object} userId, optional user ID - * @returns {Promise} key or null if no signing key has been found - * @async - */ -Key.prototype.getSigningKey = async function (keyId = null, date = new Date(), userId = {}) { - await this.verifyPrimaryKey(date, userId); - const primaryKey = this.keyPacket; - const subKeys = this.subKeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); - let exception; - for (let i = 0; i < subKeys.length; i++) { - if (!keyId || subKeys[i].getKeyId().equals(keyId)) { - try { - await subKeys[i].verify(primaryKey, date); - const dataToVerify = { key: primaryKey, bind: subKeys[i].keyPacket }; - const bindingSignature = await helper.getLatestValidSignature(subKeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); - if ( - bindingSignature && - bindingSignature.embeddedSignature && - helper.isValidSigningKeyPacket(subKeys[i].keyPacket, bindingSignature) && - await helper.getLatestValidSignature([bindingSignature.embeddedSignature], subKeys[i].keyPacket, enums.signature.keyBinding, dataToVerify, date) - ) { - return subKeys[i]; - } - } catch (e) { - exception = e; + /** + * Returns key as public key (shallow copy) + * @returns {module:key.Key} new public Key + */ + toPublic() { + const packetlist = new PacketList(); + const keyPackets = this.toPacketlist(); + let bytes; + let pubKeyPacket; + let pubSubkeyPacket; + for (let i = 0; i < keyPackets.length; i++) { + switch (keyPackets[i].tag) { + case enums.packet.secretKey: + bytes = keyPackets[i].writePublicKey(); + pubKeyPacket = new PublicKeyPacket(); + pubKeyPacket.read(bytes); + packetlist.push(pubKeyPacket); + break; + case enums.packet.secretSubkey: + bytes = keyPackets[i].writePublicKey(); + pubSubkeyPacket = new PublicSubkeyPacket(); + pubSubkeyPacket.read(bytes); + packetlist.push(pubSubkeyPacket); + break; + default: + packetlist.push(keyPackets[i]); } } + return new Key(packetlist); } - const primaryUser = await this.getPrimaryUser(date, userId); - if ((!keyId || primaryKey.getKeyId().equals(keyId)) && - helper.isValidSigningKeyPacket(primaryKey, primaryUser.selfCertification)) { - return this; - } - throw util.wrapError('Could not find valid signing key packet in key ' + this.getKeyId().toHex(), exception); -}; -/** - * Returns last created key or key by given keyId that is available for encryption or decryption - * @param {module:type/keyid} keyId, optional - * @param {Date} date, optional - * @param {String} userId, optional - * @returns {Promise} key or null if no encryption key has been found - * @async - */ -Key.prototype.getEncryptionKey = async function(keyId, date = new Date(), userId = {}) { - await this.verifyPrimaryKey(date, userId); - const primaryKey = this.keyPacket; - // V4: by convention subkeys are preferred for encryption service - const subKeys = this.subKeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); - let exception; - for (let i = 0; i < subKeys.length; i++) { - if (!keyId || subKeys[i].getKeyId().equals(keyId)) { - try { - await subKeys[i].verify(primaryKey, date); - const dataToVerify = { key: primaryKey, bind: subKeys[i].keyPacket }; - const bindingSignature = await helper.getLatestValidSignature(subKeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); - if (bindingSignature && helper.isValidEncryptionKeyPacket(subKeys[i].keyPacket, bindingSignature)) { - return subKeys[i]; + /** + * Returns ASCII armored text of key + * @returns {ReadableStream} ASCII armor + */ + armor() { + const type = this.isPublic() ? enums.armor.publicKey : enums.armor.privateKey; + return armor.encode(type, this.toPacketlist().write()); + } + + /** + * Returns last created key or key by given keyId that is available for signing and verification + * @param {module:type/keyid} keyId, optional + * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId, optional user ID + * @returns {Promise} key or null if no signing key has been found + * @async + */ + async getSigningKey(keyId = null, date = new Date(), userId = {}) { + await this.verifyPrimaryKey(date, userId); + const primaryKey = this.keyPacket; + const subKeys = this.subKeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); + let exception; + for (let i = 0; i < subKeys.length; i++) { + if (!keyId || subKeys[i].getKeyId().equals(keyId)) { + try { + await subKeys[i].verify(primaryKey, date); + const dataToVerify = { key: primaryKey, bind: subKeys[i].keyPacket }; + const bindingSignature = await helper.getLatestValidSignature(subKeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); + if ( + bindingSignature && + bindingSignature.embeddedSignature && + helper.isValidSigningKeyPacket(subKeys[i].keyPacket, bindingSignature) && + await helper.getLatestValidSignature([bindingSignature.embeddedSignature], subKeys[i].keyPacket, enums.signature.keyBinding, dataToVerify, date) + ) { + return subKeys[i]; + } + } catch (e) { + exception = e; } - } catch (e) { - exception = e; } } - } - // if no valid subkey for encryption, evaluate primary key - const primaryUser = await this.getPrimaryUser(date, userId); - if ((!keyId || primaryKey.getKeyId().equals(keyId)) && - helper.isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification)) { - return this; - } - throw util.wrapError('Could not find valid encryption key packet in key ' + this.getKeyId().toHex(), exception); -}; - -/** - * Returns all keys that are available for decryption, matching the keyId when given - * This is useful to retrieve keys for session key decryption - * @param {module:type/keyid} keyId, optional - * @param {Date} date, optional - * @param {String} userId, optional - * @returns {Promise>} array of decryption keys - * @async - */ -Key.prototype.getDecryptionKeys = async function(keyId, date = new Date(), userId = {}) { - const primaryKey = this.keyPacket; - const keys = []; - for (let i = 0; i < this.subKeys.length; i++) { - if (!keyId || this.subKeys[i].getKeyId().equals(keyId, true)) { - try { - const dataToVerify = { key: primaryKey, bind: this.subKeys[i].keyPacket }; - const bindingSignature = await helper.getLatestValidSignature(this.subKeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); - if (bindingSignature && helper.isValidDecryptionKeyPacket(bindingSignature)) { - keys.push(this.subKeys[i]); + const primaryUser = await this.getPrimaryUser(date, userId); + if ((!keyId || primaryKey.getKeyId().equals(keyId)) && + helper.isValidSigningKeyPacket(primaryKey, primaryUser.selfCertification)) { + return this; + } + throw util.wrapError('Could not find valid signing key packet in key ' + this.getKeyId().toHex(), exception); + } + + /** + * Returns last created key or key by given keyId that is available for encryption or decryption + * @param {module:type/keyid} keyId, optional + * @param {Date} date, optional + * @param {String} userId, optional + * @returns {Promise} key or null if no encryption key has been found + * @async + */ + async getEncryptionKey(keyId, date = new Date(), userId = {}) { + await this.verifyPrimaryKey(date, userId); + const primaryKey = this.keyPacket; + // V4: by convention subkeys are preferred for encryption service + const subKeys = this.subKeys.slice().sort((a, b) => b.keyPacket.created - a.keyPacket.created); + let exception; + for (let i = 0; i < subKeys.length; i++) { + if (!keyId || subKeys[i].getKeyId().equals(keyId)) { + try { + await subKeys[i].verify(primaryKey, date); + const dataToVerify = { key: primaryKey, bind: subKeys[i].keyPacket }; + const bindingSignature = await helper.getLatestValidSignature(subKeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); + if (bindingSignature && helper.isValidEncryptionKeyPacket(subKeys[i].keyPacket, bindingSignature)) { + return subKeys[i]; + } + } catch (e) { + exception = e; } - } catch (e) {} + } + } + // if no valid subkey for encryption, evaluate primary key + const primaryUser = await this.getPrimaryUser(date, userId); + if ((!keyId || primaryKey.getKeyId().equals(keyId)) && + helper.isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification)) { + return this; + } + throw util.wrapError('Could not find valid encryption key packet in key ' + this.getKeyId().toHex(), exception); + } + + /** + * Returns all keys that are available for decryption, matching the keyId when given + * This is useful to retrieve keys for session key decryption + * @param {module:type/keyid} keyId, optional + * @param {Date} date, optional + * @param {String} userId, optional + * @returns {Promise>} array of decryption keys + * @async + */ + async getDecryptionKeys(keyId, date = new Date(), userId = {}) { + const primaryKey = this.keyPacket; + const keys = []; + for (let i = 0; i < this.subKeys.length; i++) { + if (!keyId || this.subKeys[i].getKeyId().equals(keyId, true)) { + try { + const dataToVerify = { key: primaryKey, bind: this.subKeys[i].keyPacket }; + const bindingSignature = await helper.getLatestValidSignature(this.subKeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); + if (bindingSignature && helper.isValidDecryptionKeyPacket(bindingSignature)) { + keys.push(this.subKeys[i]); + } + } catch (e) {} + } } - } - - // evaluate primary key - const primaryUser = await this.getPrimaryUser(date, userId); - if ((!keyId || primaryKey.getKeyId().equals(keyId, true)) && - helper.isValidDecryptionKeyPacket(primaryUser.selfCertification)) { - keys.push(this); - } - return keys; -}; + // evaluate primary key + const primaryUser = await this.getPrimaryUser(date, userId); + if ((!keyId || primaryKey.getKeyId().equals(keyId, true)) && + helper.isValidDecryptionKeyPacket(primaryUser.selfCertification)) { + keys.push(this); + } -/** - * Encrypts all secret key and subkey packets matching keyId - * @param {String|Array} passphrases - if multiple passphrases, then should be in same order as packets each should encrypt - * @param {module:type/keyid} keyId - * @returns {Promise>} - * @async - */ -Key.prototype.encrypt = async function(passphrases, keyId = null) { - if (!this.isPrivate()) { - throw new Error("Nothing to encrypt in a public key"); + return keys; } - const keys = this.getKeys(keyId); - passphrases = util.isArray(passphrases) ? passphrases : new Array(keys.length).fill(passphrases); - if (passphrases.length !== keys.length) { - throw new Error("Invalid number of passphrases for key"); - } + /** + * Encrypts all secret key and subkey packets matching keyId + * @param {String|Array} passphrases - if multiple passphrases, then should be in same order as packets each should encrypt + * @param {module:type/keyid} keyId + * @returns {Promise>} + * @async + */ + async encrypt(passphrases, keyId = null) { + if (!this.isPrivate()) { + throw new Error("Nothing to encrypt in a public key"); + } - return Promise.all(keys.map(async function(key, i) { - const { keyPacket } = key; - await keyPacket.encrypt(passphrases[i]); - keyPacket.clearPrivateParams(); - return keyPacket; - })); -}; + const keys = this.getKeys(keyId); + passphrases = util.isArray(passphrases) ? passphrases : new Array(keys.length).fill(passphrases); + if (passphrases.length !== keys.length) { + throw new Error("Invalid number of passphrases for key"); + } -/** - * Decrypts all secret key and subkey packets matching keyId - * @param {String|Array} passphrases - * @param {module:type/keyid} keyId - * @returns {Promise} true if all matching key and subkey packets decrypted successfully - * @throws {Error} if any matching key or subkey packets did not decrypt successfully - * @async - */ -Key.prototype.decrypt = async function(passphrases, keyId = null) { - if (!this.isPrivate()) { - throw new Error("Nothing to decrypt in a public key"); + return Promise.all(keys.map(async function(key, i) { + const { keyPacket } = key; + await keyPacket.encrypt(passphrases[i]); + keyPacket.clearPrivateParams(); + return keyPacket; + })); } - passphrases = util.isArray(passphrases) ? passphrases : [passphrases]; - const results = await Promise.all(this.getKeys(keyId).map(async function(key) { - let decrypted = false; - let error = null; - await Promise.all(passphrases.map(async function(passphrase) { - try { - await key.keyPacket.decrypt(passphrase); - // If we are decrypting a single key packet, we also validate it directly - if (keyId) await key.keyPacket.validate(); - decrypted = true; - } catch (e) { - error = e; + /** + * Decrypts all secret key and subkey packets matching keyId + * @param {String|Array} passphrases + * @param {module:type/keyid} keyId + * @returns {Promise} true if all matching key and subkey packets decrypted successfully + * @throws {Error} if any matching key or subkey packets did not decrypt successfully + * @async + */ + async decrypt(passphrases, keyId = null) { + if (!this.isPrivate()) { + throw new Error("Nothing to decrypt in a public key"); + } + passphrases = util.isArray(passphrases) ? passphrases : [passphrases]; + + const results = await Promise.all(this.getKeys(keyId).map(async function(key) { + let decrypted = false; + let error = null; + await Promise.all(passphrases.map(async function(passphrase) { + try { + await key.keyPacket.decrypt(passphrase); + // If we are decrypting a single key packet, we also validate it directly + if (keyId) await key.keyPacket.validate(); + decrypted = true; + } catch (e) { + error = e; + } + })); + if (!decrypted) { + throw error; } + return decrypted; })); - if (!decrypted) { - throw error; + + if (!keyId) { + // The full key should be decrypted and we can validate it all + await this.validate(); } - return decrypted; - })); - if (!keyId) { - // The full key should be decrypted and we can validate it all - await this.validate(); - } + return results.every(result => result === true); + } + + /** + * Check whether the private and public primary key parameters correspond + * Together with verification of binding signatures, this guarantees key integrity + * In case of gnu-dummy primary key, it is enough to validate any signing subkeys + * otherwise all encryption subkeys are validated + * If only gnu-dummy keys are found, we cannot properly validate so we throw an error + * @throws {Error} if validation was not successful and the key cannot be trusted + * @async + */ + async validate() { + if (!this.isPrivate()) { + throw new Error("Cannot validate a public key"); + } - return results.every(result => result === true); -}; + let signingKeyPacket; + if (!this.primaryKey.isDummy()) { + signingKeyPacket = this.primaryKey; + } else { + /** + * It is enough to validate any signing keys + * since its binding signatures are also checked + */ + const signingKey = await this.getSigningKey(null, null); + // This could again be a dummy key + if (signingKey && !signingKey.keyPacket.isDummy()) { + signingKeyPacket = signingKey.keyPacket; + } + } -/** - * Check whether the private and public primary key parameters correspond - * Together with verification of binding signatures, this guarantees key integrity - * In case of gnu-dummy primary key, it is enough to validate any signing subkeys - * otherwise all encryption subkeys are validated - * If only gnu-dummy keys are found, we cannot properly validate so we throw an error - * @throws {Error} if validation was not successful and the key cannot be trusted - * @async - */ -Key.prototype.validate = async function() { - if (!this.isPrivate()) { - throw new Error("Cannot validate a public key"); - } + if (signingKeyPacket) { + return signingKeyPacket.validate(); + } else { + const keys = this.getKeys(); + const allDummies = keys.map(key => key.keyPacket.isDummy()).every(Boolean); + if (allDummies) { + throw new Error("Cannot validate an all-gnu-dummy key"); + } - let signingKeyPacket; - if (!this.primaryKey.isDummy()) { - signingKeyPacket = this.primaryKey; - } else { - /** - * It is enough to validate any signing keys - * since its binding signatures are also checked - */ - const signingKey = await this.getSigningKey(null, null); - // This could again be a dummy key - if (signingKey && !signingKey.keyPacket.isDummy()) { - signingKeyPacket = signingKey.keyPacket; + return Promise.all(keys.map(async key => key.keyPacket.validate())); } } - if (signingKeyPacket) { - return signingKeyPacket.validate(); - } else { - const keys = this.getKeys(); - const allDummies = keys.map(key => key.keyPacket.isDummy()).every(Boolean); - if (allDummies) { - throw new Error("Cannot validate an all-gnu-dummy key"); + /** + * Clear private key parameters + */ + clearPrivateParams() { + if (!this.isPrivate()) { + throw new Error("Can't clear private parameters of a public key"); } - - return Promise.all(keys.map(async key => key.keyPacket.validate())); + this.getKeys().forEach(({ keyPacket }) => { + if (keyPacket.isDecrypted()) { + keyPacket.clearPrivateParams(); + } + }); } -}; -/** - * Clear private key parameters - */ -Key.prototype.clearPrivateParams = function () { - if (!this.isPrivate()) { - throw new Error("Can't clear private parameters of a public key"); - } - this.getKeys().forEach(({ keyPacket }) => { - if (keyPacket.isDecrypted()) { - keyPacket.clearPrivateParams(); + /** + * Checks if a signature on a key is revoked + * @param {SignaturePacket} signature The signature to verify + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} key, optional The key to verify the signature + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} True if the certificate is revoked + * @async + */ + async isRevoked(signature, key, date = new Date()) { + return helper.isDataRevoked( + this.keyPacket, enums.signature.keyRevocation, { key: this.keyPacket }, this.revocationSignatures, signature, key, date + ); + } + + /** + * Verify primary key. Checks for revocation signatures, expiration time + * and valid self signature. Throws if the primary key is invalid. + * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID + * @returns {Promise} The status of the primary key + * @async + */ + async verifyPrimaryKey(date = new Date(), userId = {}) { + const primaryKey = this.keyPacket; + // check for key revocation signatures + if (await this.isRevoked(null, null, date)) { + throw new Error('Primary key is revoked'); + } + // check for at least one self signature. Self signature of user ID not mandatory + // See {@link https://tools.ietf.org/html/rfc4880#section-11.1} + if (!this.users.some(user => user.userId && user.selfCertifications.length)) { + throw new Error('No self-certifications'); + } + // check for valid, unrevoked, unexpired self signature + const { selfCertification } = await this.getPrimaryUser(date, userId); + // check for expiration time + if (helper.isDataExpired(primaryKey, selfCertification, date)) { + throw new Error('Primary key is expired'); } - }); -}; - -/** - * Checks if a signature on a key is revoked - * @param {SignaturePacket} signature The signature to verify - * @param {PublicSubkeyPacket| - * SecretSubkeyPacket| - * PublicKeyPacket| - * SecretKeyPacket} key, optional The key to verify the signature - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} True if the certificate is revoked - * @async - */ -Key.prototype.isRevoked = async function(signature, key, date = new Date()) { - return helper.isDataRevoked( - this.keyPacket, enums.signature.keyRevocation, { key: this.keyPacket }, this.revocationSignatures, signature, key, date - ); -}; - -/** - * Verify primary key. Checks for revocation signatures, expiration time - * and valid self signature. Throws if the primary key is invalid. - * @param {Date} date (optional) use the given date for verification instead of the current time - * @param {Object} userId (optional) user ID - * @returns {Promise} The status of the primary key - * @async - */ -Key.prototype.verifyPrimaryKey = async function(date = new Date(), userId = {}) { - const primaryKey = this.keyPacket; - // check for key revocation signatures - if (await this.isRevoked(null, null, date)) { - throw new Error('Primary key is revoked'); - } - // check for at least one self signature. Self signature of user ID not mandatory - // See {@link https://tools.ietf.org/html/rfc4880#section-11.1} - if (!this.users.some(user => user.userId && user.selfCertifications.length)) { - throw new Error('No self-certifications'); - } - // check for valid, unrevoked, unexpired self signature - const { selfCertification } = await this.getPrimaryUser(date, userId); - // check for expiration time - if (helper.isDataExpired(primaryKey, selfCertification, date)) { - throw new Error('Primary key is expired'); } -}; - -/** - * Returns the latest date when the key can be used for encrypting, signing, or both, depending on the `capabilities` paramater. - * When `capabilities` is null, defaults to returning the expiry date of the primary key. - * Returns null if `capabilities` is passed and the key does not have the specified capabilities or is revoked or invalid. - * Returns Infinity if the key doesn't expire. - * @param {encrypt|sign|encrypt_sign} capabilities, optional - * @param {module:type/keyid} keyId, optional - * @param {Object} userId, optional user ID - * @returns {Promise} - * @async - */ -Key.prototype.getExpirationTime = async function(capabilities, keyId, userId) { - const primaryUser = await this.getPrimaryUser(null, userId); - const selfCert = primaryUser.selfCertification; - const keyExpiry = helper.getExpirationTime(this.keyPacket, selfCert); - const sigExpiry = selfCert.getExpirationTime(); - let expiry = keyExpiry < sigExpiry ? keyExpiry : sigExpiry; - if (capabilities === 'encrypt' || capabilities === 'encrypt_sign') { - const encryptKey = - await this.getEncryptionKey(keyId, expiry, userId).catch(() => {}) || - await this.getEncryptionKey(keyId, null, userId).catch(() => {}); - if (!encryptKey) return null; - const encryptExpiry = await encryptKey.getExpirationTime(this.keyPacket); - if (encryptExpiry < expiry) expiry = encryptExpiry; - } - if (capabilities === 'sign' || capabilities === 'encrypt_sign') { - const signKey = - await this.getSigningKey(keyId, expiry, userId).catch(() => {}) || - await this.getSigningKey(keyId, null, userId).catch(() => {}); - if (!signKey) return null; - const signExpiry = await signKey.getExpirationTime(this.keyPacket); - if (signExpiry < expiry) expiry = signExpiry; - } - return expiry; -}; -/** - * Returns primary user and most significant (latest valid) self signature - * - if multiple primary users exist, returns the one with the latest self signature - * - otherwise, returns the user with the latest self signature - * @param {Date} date (optional) use the given date for verification instead of the current time - * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists - * @returns {Promise<{user: module:key.User, - * selfCertification: SignaturePacket}>} The primary user and the self signature - * @async - */ -Key.prototype.getPrimaryUser = async function(date = new Date(), userId = {}) { - const primaryKey = this.keyPacket; - const users = []; - let exception; - for (let i = 0; i < this.users.length; i++) { - try { - const user = this.users[i]; - if (!user.userId) { - continue; + /** + * Returns the latest date when the key can be used for encrypting, signing, or both, depending on the `capabilities` paramater. + * When `capabilities` is null, defaults to returning the expiry date of the primary key. + * Returns null if `capabilities` is passed and the key does not have the specified capabilities or is revoked or invalid. + * Returns Infinity if the key doesn't expire. + * @param {encrypt|sign|encrypt_sign} capabilities, optional + * @param {module:type/keyid} keyId, optional + * @param {Object} userId, optional user ID + * @returns {Promise} + * @async + */ + async getExpirationTime(capabilities, keyId, userId) { + const primaryUser = await this.getPrimaryUser(null, userId); + const selfCert = primaryUser.selfCertification; + const keyExpiry = helper.getExpirationTime(this.keyPacket, selfCert); + const sigExpiry = selfCert.getExpirationTime(); + let expiry = keyExpiry < sigExpiry ? keyExpiry : sigExpiry; + if (capabilities === 'encrypt' || capabilities === 'encrypt_sign') { + const encryptKey = + await this.getEncryptionKey(keyId, expiry, userId).catch(() => {}) || + await this.getEncryptionKey(keyId, null, userId).catch(() => {}); + if (!encryptKey) return null; + const encryptExpiry = await encryptKey.getExpirationTime(this.keyPacket); + if (encryptExpiry < expiry) expiry = encryptExpiry; + } + if (capabilities === 'sign' || capabilities === 'encrypt_sign') { + const signKey = + await this.getSigningKey(keyId, expiry, userId).catch(() => {}) || + await this.getSigningKey(keyId, null, userId).catch(() => {}); + if (!signKey) return null; + const signExpiry = await signKey.getExpirationTime(this.keyPacket); + if (signExpiry < expiry) expiry = signExpiry; + } + return expiry; + } + + /** + * Returns primary user and most significant (latest valid) self signature + * - if multiple primary users exist, returns the one with the latest self signature + * - otherwise, returns the user with the latest self signature + * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists + * @returns {Promise<{user: module:key.User, + * selfCertification: SignaturePacket}>} The primary user and the self signature + * @async + */ + async getPrimaryUser(date = new Date(), userId = {}) { + const primaryKey = this.keyPacket; + const users = []; + let exception; + for (let i = 0; i < this.users.length; i++) { + try { + const user = this.users[i]; + if (!user.userId) { + continue; + } + if ( + (userId.name !== undefined && user.userId.name !== userId.name) || + (userId.email !== undefined && user.userId.email !== userId.email) || + (userId.comment !== undefined && user.userId.comment !== userId.comment) + ) { + throw new Error('Could not find user that matches that user ID'); + } + const dataToVerify = { userId: user.userId, key: primaryKey }; + const selfCertification = await helper.getLatestValidSignature(user.selfCertifications, primaryKey, enums.signature.certGeneric, dataToVerify, date); + users.push({ index: i, user, selfCertification }); + } catch (e) { + exception = e; } - if ( - (userId.name !== undefined && user.userId.name !== userId.name) || - (userId.email !== undefined && user.userId.email !== userId.email) || - (userId.comment !== undefined && user.userId.comment !== userId.comment) - ) { - throw new Error('Could not find user that matches that user ID'); + } + if (!users.length) { + throw exception || new Error('Could not find primary user'); + } + await Promise.all(users.map(async function (a) { + return a.user.revoked || a.user.isRevoked(primaryKey, a.selfCertification, null, date); + })); + // sort by primary user flag and signature creation time + const primaryUser = users.sort(function(a, b) { + const A = a.selfCertification; + const B = b.selfCertification; + return B.revoked - A.revoked || A.isPrimaryUserID - B.isPrimaryUserID || A.created - B.created; + }).pop(); + const { user, selfCertification: cert } = primaryUser; + if (cert.revoked || await user.isRevoked(primaryKey, cert, null, date)) { + throw new Error('Primary user is revoked'); + } + return primaryUser; + } + + /** + * Update key with new components from specified key with same key ID: + * users, subkeys, certificates are merged into the destination key, + * duplicates and expired signatures are ignored. + * + * If the specified key is a private key and the destination key is public, + * the destination key is transformed to a private key. + * @param {module:key.Key} key Source key to merge + * @returns {Promise} + * @async + */ + async update(key) { + if (!this.hasSameFingerprintAs(key)) { + throw new Error('Key update method: fingerprints of keys not equal'); + } + if (this.isPublic() && key.isPrivate()) { + // check for equal subkey packets + const equal = (this.subKeys.length === key.subKeys.length) && + (this.subKeys.every(destSubKey => { + return key.subKeys.some(srcSubKey => { + return destSubKey.hasSameFingerprintAs(srcSubKey); + }); + })); + if (!equal) { + throw new Error('Cannot update public key with private key if subkey mismatch'); } - const dataToVerify = { userId: user.userId, key: primaryKey }; - const selfCertification = await helper.getLatestValidSignature(user.selfCertifications, primaryKey, enums.signature.certGeneric, dataToVerify, date); - users.push({ index: i, user, selfCertification }); - } catch (e) { - exception = e; + this.keyPacket = key.keyPacket; } - } - if (!users.length) { - throw exception || new Error('Could not find primary user'); - } - await Promise.all(users.map(async function (a) { - return a.user.revoked || a.user.isRevoked(primaryKey, a.selfCertification, null, date); - })); - // sort by primary user flag and signature creation time - const primaryUser = users.sort(function(a, b) { - const A = a.selfCertification; - const B = b.selfCertification; - return B.revoked - A.revoked || A.isPrimaryUserID - B.isPrimaryUserID || A.created - B.created; - }).pop(); - const { user, selfCertification: cert } = primaryUser; - if (cert.revoked || await user.isRevoked(primaryKey, cert, null, date)) { - throw new Error('Primary user is revoked'); - } - return primaryUser; -}; - -/** - * Update key with new components from specified key with same key ID: - * users, subkeys, certificates are merged into the destination key, - * duplicates and expired signatures are ignored. - * - * If the specified key is a private key and the destination key is public, - * the destination key is transformed to a private key. - * @param {module:key.Key} key Source key to merge - * @returns {Promise} - * @async - */ -Key.prototype.update = async function(key) { - if (!this.hasSameFingerprintAs(key)) { - throw new Error('Key update method: fingerprints of keys not equal'); - } - if (this.isPublic() && key.isPrivate()) { - // check for equal subkey packets - const equal = (this.subKeys.length === key.subKeys.length) && - (this.subKeys.every(destSubKey => { - return key.subKeys.some(srcSubKey => { - return destSubKey.hasSameFingerprintAs(srcSubKey); - }); - })); - if (!equal) { - throw new Error('Cannot update public key with private key if subkey mismatch'); - } - this.keyPacket = key.keyPacket; - } - // revocation signatures - await helper.mergeSignatures(key, this, 'revocationSignatures', srcRevSig => { - return helper.isDataRevoked(this.keyPacket, enums.signature.keyRevocation, this, [srcRevSig], null, key.keyPacket); - }); - // direct signatures - await helper.mergeSignatures(key, this, 'directSignatures'); - // TODO replace when Promise.some or Promise.any are implemented - // users - await Promise.all(key.users.map(async srcUser => { - let found = false; - await Promise.all(this.users.map(async dstUser => { - if ((srcUser.userId && dstUser.userId && - (srcUser.userId.userid === dstUser.userId.userid)) || - (srcUser.userAttribute && (srcUser.userAttribute.equals(dstUser.userAttribute)))) { - await dstUser.update(srcUser, this.keyPacket); - found = true; + // revocation signatures + await helper.mergeSignatures(key, this, 'revocationSignatures', srcRevSig => { + return helper.isDataRevoked(this.keyPacket, enums.signature.keyRevocation, this, [srcRevSig], null, key.keyPacket); + }); + // direct signatures + await helper.mergeSignatures(key, this, 'directSignatures'); + // TODO replace when Promise.some or Promise.any are implemented + // users + await Promise.all(key.users.map(async srcUser => { + let found = false; + await Promise.all(this.users.map(async dstUser => { + if ((srcUser.userId && dstUser.userId && + (srcUser.userId.userid === dstUser.userId.userid)) || + (srcUser.userAttribute && (srcUser.userAttribute.equals(dstUser.userAttribute)))) { + await dstUser.update(srcUser, this.keyPacket); + found = true; + } + })); + if (!found) { + this.users.push(srcUser); } })); - if (!found) { - this.users.push(srcUser); - } - })); - // TODO replace when Promise.some or Promise.any are implemented - // subkeys - await Promise.all(key.subKeys.map(async srcSubKey => { - let found = false; - await Promise.all(this.subKeys.map(async dstSubKey => { - if (dstSubKey.hasSameFingerprintAs(srcSubKey)) { - await dstSubKey.update(srcSubKey, this.keyPacket); - found = true; + // TODO replace when Promise.some or Promise.any are implemented + // subkeys + await Promise.all(key.subKeys.map(async srcSubKey => { + let found = false; + await Promise.all(this.subKeys.map(async dstSubKey => { + if (dstSubKey.hasSameFingerprintAs(srcSubKey)) { + await dstSubKey.update(srcSubKey, this.keyPacket); + found = true; + } + })); + if (!found) { + this.subKeys.push(srcSubKey); } })); - if (!found) { - this.subKeys.push(srcSubKey); - } - })); -}; - -/** - * Revokes the key - * @param {Object} reasonForRevocation optional, object indicating the reason for revocation - * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation - * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation - * @param {Date} date optional, override the creationtime of the revocation signature - * @returns {Promise} new key with revocation signature - * @async - */ -Key.prototype.revoke = async function({ - flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, - string: reasonForRevocationString = '' -} = {}, date = new Date()) { - if (this.isPublic()) { - throw new Error('Need private key for revoking'); - } - const dataToSign = { key: this.keyPacket }; - const key = await this.clone(); - key.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, null, this.keyPacket, { - signatureType: enums.signature.keyRevocation, - reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), - reasonForRevocationString - }, date)); - return key; -}; - -/** - * Get revocation certificate from a revoked key. - * (To get a revocation certificate for an unrevoked key, call revoke() first.) - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} armored revocation certificate - * @async - */ -Key.prototype.getRevocationCertificate = async function(date = new Date()) { - const dataToVerify = { key: this.keyPacket }; - const revocationSignature = await helper.getLatestValidSignature(this.revocationSignatures, this.keyPacket, enums.signature.keyRevocation, dataToVerify, date); - const packetlist = new PacketList(); - packetlist.push(revocationSignature); - return armor.encode(enums.armor.publicKey, packetlist.write(), null, null, 'This is a revocation certificate'); -}; - -/** - * Applies a revocation certificate to a key - * This adds the first signature packet in the armored text to the key, - * if it is a valid revocation signature. - * @param {String} revocationCertificate armored revocation certificate - * @returns {Promise} new revoked key - * @async - */ -Key.prototype.applyRevocationCertificate = async function(revocationCertificate) { - const input = await armor.decode(revocationCertificate); - const packetlist = new PacketList(); - await packetlist.read(input.data, { SignaturePacket }); - const revocationSignature = packetlist.findPacket(enums.packet.signature); - if (!revocationSignature || revocationSignature.signatureType !== enums.signature.keyRevocation) { - throw new Error('Could not find revocation signature packet'); - } - if (!revocationSignature.issuerKeyId.equals(this.getKeyId())) { - throw new Error('Revocation signature does not match key'); - } - if (revocationSignature.isExpired()) { - throw new Error('Revocation signature is expired'); - } - try { - await revocationSignature.verify(this.keyPacket, enums.signature.keyRevocation, { key: this.keyPacket }); - } catch (e) { - throw util.wrapError('Could not verify revocation signature', e); - } - const key = await this.clone(); - key.revocationSignatures.push(revocationSignature); - return key; -}; - -/** - * Signs primary user of key - * @param {Array} privateKey decrypted private keys for signing - * @param {Date} date (optional) use the given date for verification instead of the current time - * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists - * @returns {Promise} new public key with new certificate signature - * @async - */ -Key.prototype.signPrimaryUser = async function(privateKeys, date, userId) { - const { index, user } = await this.getPrimaryUser(date, userId); - const userSign = await user.sign(this.keyPacket, privateKeys); - const key = await this.clone(); - key.users[index] = userSign; - return key; -}; - -/** - * Signs all users of key - * @param {Array} privateKeys decrypted private keys for signing - * @returns {Promise} new public key with new certificate signature - * @async - */ -Key.prototype.signAllUsers = async function(privateKeys) { - const that = this; - const key = await this.clone(); - key.users = await Promise.all(this.users.map(function(user) { - return user.sign(that.keyPacket, privateKeys); - })); - return key; -}; - -/** - * Verifies primary user of key - * - if no arguments are given, verifies the self certificates; - * - otherwise, verifies all certificates signed with given keys. - * @param {Array} keys array of keys to verify certificate signatures - * @param {Date} date (optional) use the given date for verification instead of the current time - * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists - * @returns {Promise>} List of signer's keyid and validity of signature - * @async - */ -Key.prototype.verifyPrimaryUser = async function(keys, date, userId) { - const primaryKey = this.keyPacket; - const { user } = await this.getPrimaryUser(date, userId); - const results = keys ? await user.verifyAllCertifications(primaryKey, keys) : - [{ keyid: primaryKey.keyid, valid: await user.verify(primaryKey).catch(() => false) }]; - return results; -}; + } -/** - * Verifies all users of key - * - if no arguments are given, verifies the self certificates; - * - otherwise, verifies all certificates signed with given keys. - * @param {Array} keys array of keys to verify certificate signatures - * @returns {Promise>} list of userid, signer's keyid and validity of signature - * @async - */ -Key.prototype.verifyAllUsers = async function(keys) { - const results = []; - const primaryKey = this.keyPacket; - await Promise.all(this.users.map(async function(user) { - const signatures = keys ? await user.verifyAllCertifications(primaryKey, keys) : + /** + * Revokes the key + * @param {Object} reasonForRevocation optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date optional, override the creationtime of the revocation signature + * @returns {Promise} new key with revocation signature + * @async + */ + async revoke( + { + flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, + string: reasonForRevocationString = '' + } = {}, + date = new Date() + ) { + if (this.isPublic()) { + throw new Error('Need private key for revoking'); + } + const dataToSign = { key: this.keyPacket }; + const key = await this.clone(); + key.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, null, this.keyPacket, { + signatureType: enums.signature.keyRevocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date)); + return key; + } + + /** + * Get revocation certificate from a revoked key. + * (To get a revocation certificate for an unrevoked key, call revoke() first.) + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} armored revocation certificate + * @async + */ + async getRevocationCertificate(date = new Date()) { + const dataToVerify = { key: this.keyPacket }; + const revocationSignature = await helper.getLatestValidSignature(this.revocationSignatures, this.keyPacket, enums.signature.keyRevocation, dataToVerify, date); + const packetlist = new PacketList(); + packetlist.push(revocationSignature); + return armor.encode(enums.armor.publicKey, packetlist.write(), null, null, 'This is a revocation certificate'); + } + + /** + * Applies a revocation certificate to a key + * This adds the first signature packet in the armored text to the key, + * if it is a valid revocation signature. + * @param {String} revocationCertificate armored revocation certificate + * @returns {Promise} new revoked key + * @async + */ + async applyRevocationCertificate(revocationCertificate) { + const input = await armor.decode(revocationCertificate); + const packetlist = new PacketList(); + await packetlist.read(input.data, { SignaturePacket }); + const revocationSignature = packetlist.findPacket(enums.packet.signature); + if (!revocationSignature || revocationSignature.signatureType !== enums.signature.keyRevocation) { + throw new Error('Could not find revocation signature packet'); + } + if (!revocationSignature.issuerKeyId.equals(this.getKeyId())) { + throw new Error('Revocation signature does not match key'); + } + if (revocationSignature.isExpired()) { + throw new Error('Revocation signature is expired'); + } + try { + await revocationSignature.verify(this.keyPacket, enums.signature.keyRevocation, { key: this.keyPacket }); + } catch (e) { + throw util.wrapError('Could not verify revocation signature', e); + } + const key = await this.clone(); + key.revocationSignatures.push(revocationSignature); + return key; + } + + /** + * Signs primary user of key + * @param {Array} privateKey decrypted private keys for signing + * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists + * @returns {Promise} new public key with new certificate signature + * @async + */ + async signPrimaryUser(privateKeys, date, userId) { + const { index, user } = await this.getPrimaryUser(date, userId); + const userSign = await user.sign(this.keyPacket, privateKeys); + const key = await this.clone(); + key.users[index] = userSign; + return key; + } + + /** + * Signs all users of key + * @param {Array} privateKeys decrypted private keys for signing + * @returns {Promise} new public key with new certificate signature + * @async + */ + async signAllUsers(privateKeys) { + const that = this; + const key = await this.clone(); + key.users = await Promise.all(this.users.map(function(user) { + return user.sign(that.keyPacket, privateKeys); + })); + return key; + } + + /** + * Verifies primary user of key + * - if no arguments are given, verifies the self certificates; + * - otherwise, verifies all certificates signed with given keys. + * @param {Array} keys array of keys to verify certificate signatures + * @param {Date} date (optional) use the given date for verification instead of the current time + * @param {Object} userId (optional) user ID to get instead of the primary user, if it exists + * @returns {Promise>} List of signer's keyid and validity of signature + * @async + */ + async verifyPrimaryUser(keys, date, userId) { + const primaryKey = this.keyPacket; + const { user } = await this.getPrimaryUser(date, userId); + const results = keys ? await user.verifyAllCertifications(primaryKey, keys) : [{ keyid: primaryKey.keyid, valid: await user.verify(primaryKey).catch(() => false) }]; - signatures.forEach(signature => { - results.push({ - userid: user.userId.userid, - keyid: signature.keyid, - valid: signature.valid + return results; + } + + /** + * Verifies all users of key + * - if no arguments are given, verifies the self certificates; + * - otherwise, verifies all certificates signed with given keys. + * @param {Array} keys array of keys to verify certificate signatures + * @returns {Promise>} list of userid, signer's keyid and validity of signature + * @async + */ + async verifyAllUsers(keys) { + const results = []; + const primaryKey = this.keyPacket; + await Promise.all(this.users.map(async function(user) { + const signatures = keys ? await user.verifyAllCertifications(primaryKey, keys) : + [{ keyid: primaryKey.keyid, valid: await user.verify(primaryKey).catch(() => false) }]; + signatures.forEach(signature => { + results.push({ + userid: user.userId.userid, + keyid: signature.keyid, + valid: signature.valid + }); }); - }); - })); - return results; -}; - -/** - * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. - * Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key. - * @param {Integer} options.rsaBits number of bits for the key creation. - * @param {Number} [options.keyExpirationTime=0] - * The number of seconds after the key creation time that the key expires - * @param {String} curve (optional) Elliptic curve for ECC keys - * @param {Date} date (optional) Override the creation date of the key and the key signatures - * @param {Boolean} sign (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false - * @returns {Promise} - * @async - */ -Key.prototype.addSubkey = async function(options = {}) { - if (!this.isPrivate()) { - throw new Error("Cannot add a subkey to a public key"); - } - if (options.passphrase) { - throw new Error("Subkey could not be encrypted here, please encrypt whole key"); - } - if (util.getWebCryptoAll() && options.rsaBits < 2048) { - throw new Error('When using webCrypto rsaBits should be 2048 or 4096, found: ' + options.rsaBits); - } - const secretKeyPacket = this.primaryKey; - if (!secretKeyPacket.isDecrypted()) { - throw new Error("Key is not decrypted"); - } - const defaultOptions = secretKeyPacket.getAlgorithmInfo(); - options = helper.sanitizeKeyOptions(options, defaultOptions); - const keyPacket = await helper.generateSecretSubkey(options); - const bindingSignature = await helper.createBindingSignature(keyPacket, secretKeyPacket, options); - const packetList = this.toPacketlist(); - packetList.push(keyPacket); - packetList.push(bindingSignature); - return new Key(packetList); -}; + })); + return results; + } + + /** + * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. + * Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key. + * @param {Integer} options.rsaBits number of bits for the key creation. + * @param {Number} [options.keyExpirationTime=0] + * The number of seconds after the key creation time that the key expires + * @param {String} curve (optional) Elliptic curve for ECC keys + * @param {Date} date (optional) Override the creation date of the key and the key signatures + * @param {Boolean} sign (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false + * @returns {Promise} + * @async + */ + async addSubkey(options = {}) { + if (!this.isPrivate()) { + throw new Error("Cannot add a subkey to a public key"); + } + if (options.passphrase) { + throw new Error("Subkey could not be encrypted here, please encrypt whole key"); + } + if (util.getWebCryptoAll() && options.rsaBits < 2048) { + throw new Error('When using webCrypto rsaBits should be 2048 or 4096, found: ' + options.rsaBits); + } + const secretKeyPacket = this.primaryKey; + if (!secretKeyPacket.isDecrypted()) { + throw new Error("Key is not decrypted"); + } + const defaultOptions = secretKeyPacket.getAlgorithmInfo(); + options = helper.sanitizeKeyOptions(options, defaultOptions); + const keyPacket = await helper.generateSecretSubkey(options); + const bindingSignature = await helper.createBindingSignature(keyPacket, secretKeyPacket, options); + const packetList = this.toPacketlist(); + packetList.push(keyPacket); + packetList.push(bindingSignature); + return new Key(packetList); + } +} ['getKeyId', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'isDecrypted', 'hasSameFingerprintAs'].forEach(name => { Key.prototype[name] = SubKey.prototype[name]; }); + +export default Key; diff --git a/src/key/subkey.js b/src/key/subkey.js index 082a31e9..56649581 100644 --- a/src/key/subkey.js +++ b/src/key/subkey.js @@ -10,8 +10,7 @@ import * as helper from './helper'; import { PacketList } from '../packet'; /** - * @class - * @classdesc Class that represents a subkey packet and the relevant signatures. + * Class that represents a subkey packet and the relevant signatures. * @borrows PublicSubkeyPacket#getKeyId as SubKey#getKeyId * @borrows PublicSubkeyPacket#getFingerprint as SubKey#getFingerprint * @borrows PublicSubkeyPacket#hasSameFingerprintAs as SubKey#hasSameFingerprintAs @@ -19,160 +18,169 @@ import { PacketList } from '../packet'; * @borrows PublicSubkeyPacket#getCreationTime as SubKey#getCreationTime * @borrows PublicSubkeyPacket#isDecrypted as SubKey#isDecrypted */ -export default function SubKey(subKeyPacket) { - if (!(this instanceof SubKey)) { - return new SubKey(subKeyPacket); +class SubKey { + constructor(subKeyPacket) { + if (!(this instanceof SubKey)) { + return new SubKey(subKeyPacket); + } + this.keyPacket = subKeyPacket; + this.bindingSignatures = []; + this.revocationSignatures = []; } - this.keyPacket = subKeyPacket; - this.bindingSignatures = []; - this.revocationSignatures = []; -} - -/** - * Transforms structured subkey data to packetlist - * @returns {PacketListPacket} - */ -SubKey.prototype.toPacketlist = function() { - const packetlist = new PacketList(); - packetlist.push(this.keyPacket); - packetlist.concat(this.revocationSignatures); - packetlist.concat(this.bindingSignatures); - return packetlist; -}; -/** - * Checks if a binding signature of a subkey is revoked - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {SignaturePacket} signature The binding signature to verify - * @param {PublicSubkeyPacket| - * SecretSubkeyPacket| - * PublicKeyPacket| - * SecretKeyPacket} key, optional The key to verify the signature - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} True if the binding signature is revoked - * @async - */ -SubKey.prototype.isRevoked = async function(primaryKey, signature, key, date = new Date()) { - return helper.isDataRevoked( - primaryKey, enums.signature.subkeyRevocation, { - key: primaryKey, - bind: this.keyPacket - }, this.revocationSignatures, signature, key, date - ); -}; - - -/** - * Verify subkey. Checks for revocation signatures, expiration time - * and valid binding signature. Throws if the subkey is invalid. - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} - * @async - */ -SubKey.prototype.verify = async function(primaryKey, date = new Date()) { - const dataToVerify = { key: primaryKey, bind: this.keyPacket }; - // check subkey binding signatures - const bindingSignature = await helper.getLatestValidSignature(this.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); - // check binding signature is not revoked - if (bindingSignature.revoked || await this.isRevoked(primaryKey, bindingSignature, null, date)) { - throw new Error('Subkey is revoked'); - } - // check for expiration time - if (helper.isDataExpired(this.keyPacket, bindingSignature, date)) { - throw new Error('Subkey is expired'); + /** + * Transforms structured subkey data to packetlist + * @returns {PacketListPacket} + */ + toPacketlist() { + const packetlist = new PacketList(); + packetlist.push(this.keyPacket); + packetlist.concat(this.revocationSignatures); + packetlist.concat(this.bindingSignatures); + return packetlist; } -}; -/** - * Returns the expiration time of the subkey or Infinity if key does not expire - * Returns null if the subkey is invalid. - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} - * @async - */ -SubKey.prototype.getExpirationTime = async function(primaryKey, date = new Date()) { - const dataToVerify = { key: primaryKey, bind: this.keyPacket }; - let bindingSignature; - try { - bindingSignature = await helper.getLatestValidSignature(this.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); - } catch (e) { - return null; + /** + * Checks if a binding signature of a subkey is revoked + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {SignaturePacket} signature The binding signature to verify + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} key, optional The key to verify the signature + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} True if the binding signature is revoked + * @async + */ + async isRevoked(primaryKey, signature, key, date = new Date()) { + return helper.isDataRevoked( + primaryKey, enums.signature.subkeyRevocation, { + key: primaryKey, + bind: this.keyPacket + }, this.revocationSignatures, signature, key, date + ); } - const keyExpiry = helper.getExpirationTime(this.keyPacket, bindingSignature); - const sigExpiry = bindingSignature.getExpirationTime(); - return keyExpiry < sigExpiry ? keyExpiry : sigExpiry; -}; -/** - * Update subkey with new components from specified subkey - * @param {module:key~SubKey} subKey Source subkey to merge - * @param {SecretKeyPacket| - SecretSubkeyPacket} primaryKey primary key used for validation - * @returns {Promise} - * @async - */ -SubKey.prototype.update = async function(subKey, primaryKey) { - if (!this.hasSameFingerprintAs(subKey)) { - throw new Error('SubKey update method: fingerprints of subkeys not equal'); + /** + * Verify subkey. Checks for revocation signatures, expiration time + * and valid binding signature. Throws if the subkey is invalid. + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} + * @async + */ + async verify(primaryKey, date = new Date()) { + const dataToVerify = { key: primaryKey, bind: this.keyPacket }; + // check subkey binding signatures + const bindingSignature = await helper.getLatestValidSignature(this.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); + // check binding signature is not revoked + if (bindingSignature.revoked || await this.isRevoked(primaryKey, bindingSignature, null, date)) { + throw new Error('Subkey is revoked'); + } + // check for expiration time + if (helper.isDataExpired(this.keyPacket, bindingSignature, date)) { + throw new Error('Subkey is expired'); + } } - // key packet - if (this.keyPacket.tag === enums.packet.publicSubkey && - subKey.keyPacket.tag === enums.packet.secretSubkey) { - this.keyPacket = subKey.keyPacket; + + /** + * Returns the expiration time of the subkey or Infinity if key does not expire + * Returns null if the subkey is invalid. + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} + * @async + */ + async getExpirationTime(primaryKey, date = new Date()) { + const dataToVerify = { key: primaryKey, bind: this.keyPacket }; + let bindingSignature; + try { + bindingSignature = await helper.getLatestValidSignature(this.bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date); + } catch (e) { + return null; + } + const keyExpiry = helper.getExpirationTime(this.keyPacket, bindingSignature); + const sigExpiry = bindingSignature.getExpirationTime(); + return keyExpiry < sigExpiry ? keyExpiry : sigExpiry; } - // update missing binding signatures - const that = this; - const dataToVerify = { key: primaryKey, bind: that.keyPacket }; - await helper.mergeSignatures(subKey, this, 'bindingSignatures', async function(srcBindSig) { - for (let i = 0; i < that.bindingSignatures.length; i++) { - if (that.bindingSignatures[i].issuerKeyId.equals(srcBindSig.issuerKeyId)) { - if (srcBindSig.created > that.bindingSignatures[i].created) { - that.bindingSignatures[i] = srcBindSig; + + /** + * Update subkey with new components from specified subkey + * @param {module:key~SubKey} subKey Source subkey to merge + * @param {SecretKeyPacket| + SecretSubkeyPacket} primaryKey primary key used for validation + * @returns {Promise} + * @async + */ + async update(subKey, primaryKey) { + if (!this.hasSameFingerprintAs(subKey)) { + throw new Error('SubKey update method: fingerprints of subkeys not equal'); + } + // key packet + if (this.keyPacket.tag === enums.packet.publicSubkey && + subKey.keyPacket.tag === enums.packet.secretSubkey) { + this.keyPacket = subKey.keyPacket; + } + // update missing binding signatures + const that = this; + const dataToVerify = { key: primaryKey, bind: that.keyPacket }; + await helper.mergeSignatures(subKey, this, 'bindingSignatures', async function(srcBindSig) { + for (let i = 0; i < that.bindingSignatures.length; i++) { + if (that.bindingSignatures[i].issuerKeyId.equals(srcBindSig.issuerKeyId)) { + if (srcBindSig.created > that.bindingSignatures[i].created) { + that.bindingSignatures[i] = srcBindSig; + } + return false; } + } + try { + return srcBindSig.verified || await srcBindSig.verify(primaryKey, enums.signature.subkeyBinding, dataToVerify); + } catch (e) { return false; } - } - try { - return srcBindSig.verified || await srcBindSig.verify(primaryKey, enums.signature.subkeyBinding, dataToVerify); - } catch (e) { - return false; - } - }); - // revocation signatures - await helper.mergeSignatures(subKey, this, 'revocationSignatures', function(srcRevSig) { - return helper.isDataRevoked(primaryKey, enums.signature.subkeyRevocation, dataToVerify, [srcRevSig]); - }); -}; + }); + // revocation signatures + await helper.mergeSignatures(subKey, this, 'revocationSignatures', function(srcRevSig) { + return helper.isDataRevoked(primaryKey, enums.signature.subkeyRevocation, dataToVerify, [srcRevSig]); + }); + } -/** - * Revokes the subkey - * @param {SecretKeyPacket} primaryKey decrypted private primary key for revocation - * @param {Object} reasonForRevocation optional, object indicating the reason for revocation - * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation - * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation - * @param {Date} date optional, override the creationtime of the revocation signature - * @returns {Promise} new subkey with revocation signature - * @async - */ -SubKey.prototype.revoke = async function(primaryKey, { - flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, - string: reasonForRevocationString = '' -} = {}, date = new Date()) { - const dataToSign = { key: primaryKey, bind: this.keyPacket }; - const subKey = new SubKey(this.keyPacket); - subKey.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, null, primaryKey, { - signatureType: enums.signature.subkeyRevocation, - reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), - reasonForRevocationString - }, date)); - await subKey.update(this, primaryKey); - return subKey; -}; + /** + * Revokes the subkey + * @param {SecretKeyPacket} primaryKey decrypted private primary key for revocation + * @param {Object} reasonForRevocation optional, object indicating the reason for revocation + * @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation + * @param {String} reasonForRevocation.string optional, string explaining the reason for revocation + * @param {Date} date optional, override the creationtime of the revocation signature + * @returns {Promise} new subkey with revocation signature + * @async + */ + async revoke( + primaryKey, + { + flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason, + string: reasonForRevocationString = '' + } = {}, + date = new Date() + ) { + const dataToSign = { key: primaryKey, bind: this.keyPacket }; + const subKey = new SubKey(this.keyPacket); + subKey.revocationSignatures.push(await helper.createSignaturePacket(dataToSign, null, primaryKey, { + signatureType: enums.signature.subkeyRevocation, + reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag), + reasonForRevocationString + }, date)); + await subKey.update(this, primaryKey); + return subKey; + } + + hasSameFingerprintAs(other) { + return this.keyPacket.hasSameFingerprintAs(other.keyPacket || other); + } +} ['getKeyId', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'isDecrypted'].forEach(name => { SubKey.prototype[name] = @@ -181,7 +189,4 @@ SubKey.prototype.revoke = async function(primaryKey, { }; }); -SubKey.prototype.hasSameFingerprintAs = - function(other) { - return this.keyPacket.hasSameFingerprintAs(other.keyPacket || other); - }; +export default SubKey; diff --git a/src/key/user.js b/src/key/user.js index 16647d49..70d8800f 100644 --- a/src/key/user.js +++ b/src/key/user.js @@ -12,219 +12,221 @@ import { PacketList } from '../packet'; import { mergeSignatures, isDataRevoked, createSignaturePacket } from './helper'; /** - * @class - * @classdesc Class that represents an user ID or attribute packet and the relevant signatures. + * Class that represents an user ID or attribute packet and the relevant signatures. */ -export default function User(userPacket) { - if (!(this instanceof User)) { - return new User(userPacket); +class User { + constructor(userPacket) { + if (!(this instanceof User)) { + return new User(userPacket); + } + this.userId = userPacket.tag === enums.packet.userID ? userPacket : null; + this.userAttribute = userPacket.tag === enums.packet.userAttribute ? userPacket : null; + this.selfCertifications = []; + this.otherCertifications = []; + this.revocationSignatures = []; } - this.userId = userPacket.tag === enums.packet.userID ? userPacket : null; - this.userAttribute = userPacket.tag === enums.packet.userAttribute ? userPacket : null; - this.selfCertifications = []; - this.otherCertifications = []; - this.revocationSignatures = []; -} -/** - * Transforms structured user data to packetlist - * @returns {PacketList} - */ -User.prototype.toPacketlist = function() { - const packetlist = new PacketList(); - packetlist.push(this.userId || this.userAttribute); - packetlist.concat(this.revocationSignatures); - packetlist.concat(this.selfCertifications); - packetlist.concat(this.otherCertifications); - return packetlist; -}; - -/** - * Signs user - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {Array} privateKeys Decrypted private keys for signing - * @returns {Promise} New user with new certificate signatures - * @async - */ -User.prototype.sign = async function(primaryKey, privateKeys) { - const dataToSign = { - userId: this.userId, - userAttribute: this.userAttribute, - key: primaryKey - }; - const user = new User(dataToSign.userId || dataToSign.userAttribute); - user.otherCertifications = await Promise.all(privateKeys.map(async function(privateKey) { - if (privateKey.isPublic()) { - throw new Error('Need private key for signing'); - } - if (privateKey.hasSameFingerprintAs(primaryKey)) { - throw new Error('Not implemented for self signing'); - } - const signingKey = await privateKey.getSigningKey(); - return createSignaturePacket(dataToSign, privateKey, signingKey.keyPacket, { - // Most OpenPGP implementations use generic certification (0x10) - signatureType: enums.signature.certGeneric, - keyFlags: [enums.keyFlags.certifyKeys | enums.keyFlags.signData] - }); - })); - await user.update(this, primaryKey); - return user; -}; + /** + * Transforms structured user data to packetlist + * @returns {PacketList} + */ + toPacketlist() { + const packetlist = new PacketList(); + packetlist.push(this.userId || this.userAttribute); + packetlist.concat(this.revocationSignatures); + packetlist.concat(this.selfCertifications); + packetlist.concat(this.otherCertifications); + return packetlist; + } -/** - * Checks if a given certificate of the user is revoked - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {SignaturePacket} certificate The certificate to verify - * @param {PublicSubkeyPacket| - * SecretSubkeyPacket| - * PublicKeyPacket| - * SecretKeyPacket} key, optional The key to verify the signature - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} True if the certificate is revoked - * @async - */ -User.prototype.isRevoked = async function(primaryKey, certificate, key, date = new Date()) { - return isDataRevoked( - primaryKey, enums.signature.certRevocation, { - key: primaryKey, + /** + * Signs user + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {Array} privateKeys Decrypted private keys for signing + * @returns {Promise} New user with new certificate signatures + * @async + */ + async sign(primaryKey, privateKeys) { + const dataToSign = { userId: this.userId, - userAttribute: this.userAttribute - }, this.revocationSignatures, certificate, key, date - ); -}; - - -/** - * Verifies the user certificate. Throws if the user certificate is invalid. - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {SignaturePacket} certificate A certificate of this user - * @param {Array} keys Array of keys to verify certificate signatures - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} status of the certificate - * @async - */ -User.prototype.verifyCertificate = async function(primaryKey, certificate, keys, date = new Date()) { - const that = this; - const keyid = certificate.issuerKeyId; - const dataToVerify = { - userId: this.userId, - userAttribute: this.userAttribute, - key: primaryKey - }; - const results = await Promise.all(keys.map(async function(key) { - if (!key.getKeyIds().some(id => id.equals(keyid))) { - return null; - } - const signingKey = await key.getSigningKey(keyid, date); - if (certificate.revoked || await that.isRevoked(primaryKey, certificate, signingKey.keyPacket, date)) { - throw new Error('User certificate is revoked'); - } - try { - certificate.verified || await certificate.verify(signingKey.keyPacket, enums.signature.certGeneric, dataToVerify); - } catch (e) { - throw util.wrapError('User certificate is invalid', e); - } - if (certificate.isExpired(date)) { - throw new Error('User certificate is expired'); - } - return true; - })); - return results.find(result => result !== null) || null; -}; - -/** - * Verifies all user certificates - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {Array} keys Array of keys to verify certificate signatures - * @param {Date} date Use the given date instead of the current time - * @returns {Promise>} List of signer's keyid and validity of signature - * @async - */ -User.prototype.verifyAllCertifications = async function(primaryKey, keys, date = new Date()) { - const that = this; - const certifications = this.selfCertifications.concat(this.otherCertifications); - return Promise.all(certifications.map(async function(certification) { - return { - keyid: certification.issuerKeyId, - valid: await that.verifyCertificate(primaryKey, certification, keys, date).catch(() => false) + userAttribute: this.userAttribute, + key: primaryKey }; - })); -}; + const user = new User(dataToSign.userId || dataToSign.userAttribute); + user.otherCertifications = await Promise.all(privateKeys.map(async function(privateKey) { + if (privateKey.isPublic()) { + throw new Error('Need private key for signing'); + } + if (privateKey.hasSameFingerprintAs(primaryKey)) { + throw new Error('Not implemented for self signing'); + } + const signingKey = await privateKey.getSigningKey(); + return createSignaturePacket(dataToSign, privateKey, signingKey.keyPacket, { + // Most OpenPGP implementations use generic certification (0x10) + signatureType: enums.signature.certGeneric, + keyFlags: [enums.keyFlags.certifyKeys | enums.keyFlags.signData] + }); + })); + await user.update(this, primaryKey); + return user; + } -/** - * Verify User. Checks for existence of self signatures, revocation signatures - * and validity of self signature. Throws when there are no valid self signatures. - * @param {SecretKeyPacket| - * PublicKeyPacket} primaryKey The primary key packet - * @param {Date} date Use the given date instead of the current time - * @returns {Promise} Status of user - * @async - */ -User.prototype.verify = async function(primaryKey, date = new Date()) { - if (!this.selfCertifications.length) { - throw new Error('No self-certifications'); + /** + * Checks if a given certificate of the user is revoked + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {SignaturePacket} certificate The certificate to verify + * @param {PublicSubkeyPacket| + * SecretSubkeyPacket| + * PublicKeyPacket| + * SecretKeyPacket} key, optional The key to verify the signature + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} True if the certificate is revoked + * @async + */ + async isRevoked(primaryKey, certificate, key, date = new Date()) { + return isDataRevoked( + primaryKey, enums.signature.certRevocation, { + key: primaryKey, + userId: this.userId, + userAttribute: this.userAttribute + }, this.revocationSignatures, certificate, key, date + ); } - const that = this; - const dataToVerify = { - userId: this.userId, - userAttribute: this.userAttribute, - key: primaryKey - }; - // TODO replace when Promise.some or Promise.any are implemented - let exception; - for (let i = this.selfCertifications.length - 1; i >= 0; i--) { - try { - const selfCertification = this.selfCertifications[i]; - if (selfCertification.revoked || await that.isRevoked(primaryKey, selfCertification, undefined, date)) { - throw new Error('Self-certification is revoked'); + + /** + * Verifies the user certificate. Throws if the user certificate is invalid. + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {SignaturePacket} certificate A certificate of this user + * @param {Array} keys Array of keys to verify certificate signatures + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} status of the certificate + * @async + */ + async verifyCertificate(primaryKey, certificate, keys, date = new Date()) { + const that = this; + const keyid = certificate.issuerKeyId; + const dataToVerify = { + userId: this.userId, + userAttribute: this.userAttribute, + key: primaryKey + }; + const results = await Promise.all(keys.map(async function(key) { + if (!key.getKeyIds().some(id => id.equals(keyid))) { + return null; + } + const signingKey = await key.getSigningKey(keyid, date); + if (certificate.revoked || await that.isRevoked(primaryKey, certificate, signingKey.keyPacket, date)) { + throw new Error('User certificate is revoked'); } try { - selfCertification.verified || await selfCertification.verify(primaryKey, enums.signature.certGeneric, dataToVerify); + certificate.verified || await certificate.verify(signingKey.keyPacket, enums.signature.certGeneric, dataToVerify); } catch (e) { - throw util.wrapError('Self-certification is invalid', e); + throw util.wrapError('User certificate is invalid', e); } - if (selfCertification.isExpired(date)) { - throw new Error('Self-certification is expired'); + if (certificate.isExpired(date)) { + throw new Error('User certificate is expired'); } return true; - } catch (e) { - exception = e; - } + })); + return results.find(result => result !== null) || null; } - throw exception; -}; -/** - * Update user with new components from specified user - * @param {module:key.User} user Source user to merge - * @param {SecretKeyPacket| - * SecretSubkeyPacket} primaryKey primary key used for validation - * @returns {Promise} - * @async - */ -User.prototype.update = async function(user, primaryKey) { - const dataToVerify = { - userId: this.userId, - userAttribute: this.userAttribute, - key: primaryKey - }; - // self signatures - await mergeSignatures(user, this, 'selfCertifications', async function(srcSelfSig) { - try { - return srcSelfSig.verified || srcSelfSig.verify(primaryKey, enums.signature.certGeneric, dataToVerify); - } catch (e) { - return false; + /** + * Verifies all user certificates + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {Array} keys Array of keys to verify certificate signatures + * @param {Date} date Use the given date instead of the current time + * @returns {Promise>} List of signer's keyid and validity of signature + * @async + */ + async verifyAllCertifications(primaryKey, keys, date = new Date()) { + const that = this; + const certifications = this.selfCertifications.concat(this.otherCertifications); + return Promise.all(certifications.map(async function(certification) { + return { + keyid: certification.issuerKeyId, + valid: await that.verifyCertificate(primaryKey, certification, keys, date).catch(() => false) + }; + })); + } + + /** + * Verify User. Checks for existence of self signatures, revocation signatures + * and validity of self signature. Throws when there are no valid self signatures. + * @param {SecretKeyPacket| + * PublicKeyPacket} primaryKey The primary key packet + * @param {Date} date Use the given date instead of the current time + * @returns {Promise} Status of user + * @async + */ + async verify(primaryKey, date = new Date()) { + if (!this.selfCertifications.length) { + throw new Error('No self-certifications'); + } + const that = this; + const dataToVerify = { + userId: this.userId, + userAttribute: this.userAttribute, + key: primaryKey + }; + // TODO replace when Promise.some or Promise.any are implemented + let exception; + for (let i = this.selfCertifications.length - 1; i >= 0; i--) { + try { + const selfCertification = this.selfCertifications[i]; + if (selfCertification.revoked || await that.isRevoked(primaryKey, selfCertification, undefined, date)) { + throw new Error('Self-certification is revoked'); + } + try { + selfCertification.verified || await selfCertification.verify(primaryKey, enums.signature.certGeneric, dataToVerify); + } catch (e) { + throw util.wrapError('Self-certification is invalid', e); + } + if (selfCertification.isExpired(date)) { + throw new Error('Self-certification is expired'); + } + return true; + } catch (e) { + exception = e; + } } - }); - // other signatures - await mergeSignatures(user, this, 'otherCertifications'); - // revocation signatures - await mergeSignatures(user, this, 'revocationSignatures', function(srcRevSig) { - return isDataRevoked(primaryKey, enums.signature.certRevocation, dataToVerify, [srcRevSig]); - }); -}; + throw exception; + } + + /** + * Update user with new components from specified user + * @param {module:key.User} user Source user to merge + * @param {SecretKeyPacket| + * SecretSubkeyPacket} primaryKey primary key used for validation + * @returns {Promise} + * @async + */ + async update(user, primaryKey) { + const dataToVerify = { + userId: this.userId, + userAttribute: this.userAttribute, + key: primaryKey + }; + // self signatures + await mergeSignatures(user, this, 'selfCertifications', async function(srcSelfSig) { + try { + return srcSelfSig.verified || srcSelfSig.verify(primaryKey, enums.signature.certGeneric, dataToVerify); + } catch (e) { + return false; + } + }); + // other signatures + await mergeSignatures(user, this, 'otherCertifications'); + // revocation signatures + await mergeSignatures(user, this, 'revocationSignatures', function(srcRevSig) { + return isDataRevoked(primaryKey, enums.signature.certRevocation, dataToVerify, [srcRevSig]); + }); + } +} + +export default User; diff --git a/src/keyring/keyring.js b/src/keyring/keyring.js index dd837d96..7680b070 100644 --- a/src/keyring/keyring.js +++ b/src/keyring/keyring.js @@ -25,100 +25,172 @@ import { readAllArmored } from '../key'; import LocalStore from './localstore'; -/** - * Initialization routine for the keyring. - * @constructor - * @param {keyring/localstore} [storeHandler] class implementing loadPublic(), loadPrivate(), storePublic(), and storePrivate() methods - */ -function Keyring(storeHandler) { - this.storeHandler = storeHandler || new LocalStore(); -} +class Keyring { + /** + * Initialization routine for the keyring. + * @param {keyring/localstore} [storeHandler] class implementing loadPublic(), loadPrivate(), storePublic(), and storePrivate() methods + */ + constructor(storeHandler) { + this.storeHandler = storeHandler || new LocalStore(); + } -/** - * Calls the storeHandler to load the keys - * @async - */ -Keyring.prototype.load = async function () { - this.publicKeys = new KeyArray(await this.storeHandler.loadPublic()); - this.privateKeys = new KeyArray(await this.storeHandler.loadPrivate()); -}; + /** + * Calls the storeHandler to load the keys + * @async + */ + async load() { + this.publicKeys = new KeyArray(await this.storeHandler.loadPublic()); + this.privateKeys = new KeyArray(await this.storeHandler.loadPrivate()); + } -/** - * Calls the storeHandler to save the keys - * @async - */ -Keyring.prototype.store = async function () { - await Promise.all([ - this.storeHandler.storePublic(this.publicKeys.keys), - this.storeHandler.storePrivate(this.privateKeys.keys) - ]); -}; + /** + * Calls the storeHandler to save the keys + * @async + */ + async store() { + await Promise.all([ + this.storeHandler.storePublic(this.publicKeys.keys), + this.storeHandler.storePrivate(this.privateKeys.keys) + ]); + } -/** - * Clear the keyring - erase all the keys - */ -Keyring.prototype.clear = function() { - this.publicKeys.keys = []; - this.privateKeys.keys = []; -}; + /** + * Clear the keyring - erase all the keys + */ + clear() { + this.publicKeys.keys = []; + this.privateKeys.keys = []; + } -/** - * Searches the keyring for keys having the specified key id - * @param {String} keyId provided as string of lowercase hex number - * withouth 0x prefix (can be 16-character key ID or fingerprint) - * @param {Boolean} deep if true search also in subkeys - * @returns {Array|null} keys found or null - */ -Keyring.prototype.getKeysForId = function (keyId, deep) { - let result = []; - result = result.concat(this.publicKeys.getForId(keyId, deep) || []); - result = result.concat(this.privateKeys.getForId(keyId, deep) || []); - return result.length ? result : null; -}; + /** + * Searches the keyring for keys having the specified key id + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) + * @param {Boolean} deep if true search also in subkeys + * @returns {Array|null} keys found or null + */ + getKeysForId(keyId, deep) { + let result = []; + result = result.concat(this.publicKeys.getForId(keyId, deep) || []); + result = result.concat(this.privateKeys.getForId(keyId, deep) || []); + return result.length ? result : null; + } -/** - * Removes keys having the specified key id from the keyring - * @param {String} keyId provided as string of lowercase hex number - * withouth 0x prefix (can be 16-character key ID or fingerprint) - * @returns {Array|null} keys found or null - */ -Keyring.prototype.removeKeysForId = function (keyId) { - let result = []; - result = result.concat(this.publicKeys.removeForId(keyId) || []); - result = result.concat(this.privateKeys.removeForId(keyId) || []); - return result.length ? result : null; -}; + /** + * Removes keys having the specified key id from the keyring + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) + * @returns {Array|null} keys found or null + */ + removeKeysForId(keyId) { + let result = []; + result = result.concat(this.publicKeys.removeForId(keyId) || []); + result = result.concat(this.privateKeys.removeForId(keyId) || []); + return result.length ? result : null; + } -/** - * Get all public and private keys - * @returns {Array} all keys - */ -Keyring.prototype.getAllKeys = function () { - return this.publicKeys.keys.concat(this.privateKeys.keys); -}; + /** + * Get all public and private keys + * @returns {Array} all keys + */ + getAllKeys() { + return this.publicKeys.keys.concat(this.privateKeys.keys); + } +} /** * Array of keys - * @param {Array} keys The keys to store in this array */ -function KeyArray(keys) { - this.keys = keys; -} +class KeyArray { + /** + * @param {Array} keys The keys to store in this array + */ + constructor(keys) { + this.keys = keys; + } -/** - * Searches all keys in the KeyArray matching the address or address part of the user ids - * @param {String} email email address to search for - * @returns {Array} The public keys associated with provided email address. - */ -KeyArray.prototype.getForAddress = function(email) { - const results = []; - for (let i = 0; i < this.keys.length; i++) { - if (emailCheck(email, this.keys[i])) { - results.push(this.keys[i]); + /** + * Searches all keys in the KeyArray matching the address or address part of the user ids + * @param {String} email email address to search for + * @returns {Array} The public keys associated with provided email address. + */ + getForAddress(email) { + const results = []; + for (let i = 0; i < this.keys.length; i++) { + if (emailCheck(email, this.keys[i])) { + results.push(this.keys[i]); + } } + return results; } - return results; -}; + + /** + * Searches the KeyArray for a key having the specified key id + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) + * @param {Boolean} deep if true search also in subkeys + * @returns {module:key.Key|null} key found or null + */ + getForId(keyId, deep) { + for (let i = 0; i < this.keys.length; i++) { + if (keyIdCheck(keyId, this.keys[i])) { + return this.keys[i]; + } + if (deep && this.keys[i].subKeys.length) { + for (let j = 0; j < this.keys[i].subKeys.length; j++) { + if (keyIdCheck(keyId, this.keys[i].subKeys[j])) { + return this.keys[i]; + } + } + } + } + return null; + } + + /** + * Imports a key from an ascii armored message + * @param {String} armored message to read the keys/key from + * @async + */ + async importKey(armored) { + const imported = await readAllArmored(armored); + for (let i = 0; i < imported.length; i++) { + const key = imported[i]; + // check if key already in key array + const keyidHex = key.getKeyId().toHex(); + const keyFound = this.getForId(keyidHex); + if (keyFound) { + await keyFound.update(key); + } else { + this.push(key); + } + } + } + + /** + * Add key to KeyArray + * @param {module:key.Key} key The key that will be added to the keyring + * @returns {Number} The new length of the KeyArray + */ + push(key) { + return this.keys.push(key); + } + + /** + * Removes a key with the specified keyid from the keyring + * @param {String} keyId provided as string of lowercase hex number + * withouth 0x prefix (can be 16-character key ID or fingerprint) + * @returns {module:key.Key|null} The key object which has been removed or null + */ + removeForId(keyId) { + for (let i = 0; i < this.keys.length; i++) { + if (keyIdCheck(keyId, this.keys[i])) { + return this.keys.splice(i, 1)[0]; + } + } + return null; + } +} /** * Checks a key to see if it matches the specified email address @@ -157,71 +229,4 @@ function keyIdCheck(keyId, key) { return keyId === key.getFingerprint(); } -/** - * Searches the KeyArray for a key having the specified key id - * @param {String} keyId provided as string of lowercase hex number - * withouth 0x prefix (can be 16-character key ID or fingerprint) - * @param {Boolean} deep if true search also in subkeys - * @returns {module:key.Key|null} key found or null - */ -KeyArray.prototype.getForId = function (keyId, deep) { - for (let i = 0; i < this.keys.length; i++) { - if (keyIdCheck(keyId, this.keys[i])) { - return this.keys[i]; - } - if (deep && this.keys[i].subKeys.length) { - for (let j = 0; j < this.keys[i].subKeys.length; j++) { - if (keyIdCheck(keyId, this.keys[i].subKeys[j])) { - return this.keys[i]; - } - } - } - } - return null; -}; - -/** - * Imports a key from an ascii armored message - * @param {String} armored message to read the keys/key from - * @async - */ -KeyArray.prototype.importKey = async function (armored) { - const imported = await readAllArmored(armored); - for (let i = 0; i < imported.length; i++) { - const key = imported[i]; - // check if key already in key array - const keyidHex = key.getKeyId().toHex(); - const keyFound = this.getForId(keyidHex); - if (keyFound) { - await keyFound.update(key); - } else { - this.push(key); - } - } -}; - -/** - * Add key to KeyArray - * @param {module:key.Key} key The key that will be added to the keyring - * @returns {Number} The new length of the KeyArray - */ -KeyArray.prototype.push = function (key) { - return this.keys.push(key); -}; - -/** - * Removes a key with the specified keyid from the keyring - * @param {String} keyId provided as string of lowercase hex number - * withouth 0x prefix (can be 16-character key ID or fingerprint) - * @returns {module:key.Key|null} The key object which has been removed or null - */ -KeyArray.prototype.removeForId = function (keyId) { - for (let i = 0; i < this.keys.length; i++) { - if (keyIdCheck(keyId, this.keys[i])) { - return this.keys.splice(i, 1)[0]; - } - } - return null; -}; - export default Keyring; diff --git a/src/keyring/localstore.js b/src/keyring/localstore.js index 6ef3bcc4..31087c24 100644 --- a/src/keyring/localstore.js +++ b/src/keyring/localstore.js @@ -30,17 +30,58 @@ import { readArmored } from '../key'; /** * The class that deals with storage of the keyring. * Currently the only option is to use HTML5 local storage. - * @constructor - * @param {String} prefix prefix for itemnames in localstore */ -function LocalStore(prefix) { - prefix = prefix || 'openpgp-'; - this.publicKeysItem = prefix + this.publicKeysItem; - this.privateKeysItem = prefix + this.privateKeysItem; - if (typeof globalThis !== 'undefined' && globalThis.localStorage) { - this.storage = globalThis.localStorage; - } else { - this.storage = new (require('node-localstorage').LocalStorage)(config.nodeStore); +class LocalStore { + /** + * @param {String} prefix prefix for itemnames in localstore + */ + constructor(prefix) { + prefix = prefix || 'openpgp-'; + this.publicKeysItem = prefix + this.publicKeysItem; + this.privateKeysItem = prefix + this.privateKeysItem; + if (typeof globalThis !== 'undefined' && globalThis.localStorage) { + this.storage = globalThis.localStorage; + } else { + this.storage = new (require('node-localstorage').LocalStorage)(config.nodeStore); + } + } + + /** + * Load the public keys from HTML5 local storage. + * @returns {Array} array of keys retrieved from localstore + * @async + */ + async loadPublic() { + return loadKeys(this.storage, this.publicKeysItem); + } + + /** + * Load the private keys from HTML5 local storage. + * @returns {Array} array of keys retrieved from localstore + * @async + */ + async loadPrivate() { + return loadKeys(this.storage, this.privateKeysItem); + } + + /** + * Saves the current state of the public keys to HTML5 local storage. + * The key array gets stringified using JSON + * @param {Array} keys array of keys to save in localstore + * @async + */ + async storePublic(keys) { + await storeKeys(this.storage, this.publicKeysItem, keys); + } + + /** + * Saves the current state of the private keys to HTML5 local storage. + * The key array gets stringified using JSON + * @param {Array} keys array of keys to save in localstore + * @async + */ + async storePrivate(keys) { + await storeKeys(this.storage, this.privateKeysItem, keys); } } @@ -50,24 +91,6 @@ function LocalStore(prefix) { LocalStore.prototype.publicKeysItem = 'public-keys'; LocalStore.prototype.privateKeysItem = 'private-keys'; -/** - * Load the public keys from HTML5 local storage. - * @returns {Array} array of keys retrieved from localstore - * @async - */ -LocalStore.prototype.loadPublic = async function () { - return loadKeys(this.storage, this.publicKeysItem); -}; - -/** - * Load the private keys from HTML5 local storage. - * @returns {Array} array of keys retrieved from localstore - * @async - */ -LocalStore.prototype.loadPrivate = async function () { - return loadKeys(this.storage, this.privateKeysItem); -}; - async function loadKeys(storage, itemname) { const armoredKeys = JSON.parse(storage.getItem(itemname)); const keys = []; @@ -81,26 +104,6 @@ async function loadKeys(storage, itemname) { return keys; } -/** - * Saves the current state of the public keys to HTML5 local storage. - * The key array gets stringified using JSON - * @param {Array} keys array of keys to save in localstore - * @async - */ -LocalStore.prototype.storePublic = async function (keys) { - await storeKeys(this.storage, this.publicKeysItem, keys); -}; - -/** - * Saves the current state of the private keys to HTML5 local storage. - * The key array gets stringified using JSON - * @param {Array} keys array of keys to save in localstore - * @async - */ -LocalStore.prototype.storePrivate = async function (keys) { - await storeKeys(this.storage, this.privateKeysItem, keys); -}; - async function storeKeys(storage, itemname, keys) { if (keys.length) { const armoredKeys = await Promise.all(keys.map(key => stream.readToEnd(key.armor()))); diff --git a/src/message.js b/src/message.js index ca3e84ca..d727a1d8 100644 --- a/src/message.js +++ b/src/message.js @@ -53,474 +53,579 @@ import { getPreferredHashAlgo, getPreferredAlgo, isAeadSupported, createSignatur /** - * @class - * @classdesc Class that represents an OpenPGP message. + * Class that represents an OpenPGP message. * Can be an encrypted message, signed message, compressed message or literal message - * @param {module:PacketList} packetlist The packets that form this message * See {@link https://tools.ietf.org/html/rfc4880#section-11.3} */ - -export function Message(packetlist) { - if (!(this instanceof Message)) { - return new Message(packetlist); +export class Message { + /** + * @param {module:PacketList} packetlist The packets that form this message + */ + constructor(packetlist) { + this.packets = packetlist || new PacketList(); } - this.packets = packetlist || new PacketList(); -} -/** - * Returns the key IDs of the keys to which the session key is encrypted - * @returns {Array} array of keyid objects - */ -Message.prototype.getEncryptionKeyIds = function() { - const keyIds = []; - const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); - pkESKeyPacketlist.forEach(function(packet) { - keyIds.push(packet.publicKeyId); - }); - return keyIds; -}; + /** + * Returns the key IDs of the keys to which the session key is encrypted + * @returns {Array} array of keyid objects + */ + getEncryptionKeyIds() { + const keyIds = []; + const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); + pkESKeyPacketlist.forEach(function(packet) { + keyIds.push(packet.publicKeyId); + }); + return keyIds; + } -/** - * Returns the key IDs of the keys that signed the message - * @returns {Array} array of keyid objects - */ -Message.prototype.getSigningKeyIds = function() { - const keyIds = []; - const msg = this.unwrapCompressed(); - // search for one pass signatures - const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature); - onePassSigList.forEach(function(packet) { - keyIds.push(packet.issuerKeyId); - }); - // if nothing found look for signature packets - if (!keyIds.length) { - const signatureList = msg.packets.filterByTag(enums.packet.signature); - signatureList.forEach(function(packet) { + /** + * Returns the key IDs of the keys that signed the message + * @returns {Array} array of keyid objects + */ + getSigningKeyIds() { + const keyIds = []; + const msg = this.unwrapCompressed(); + // search for one pass signatures + const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature); + onePassSigList.forEach(function(packet) { keyIds.push(packet.issuerKeyId); }); + // if nothing found look for signature packets + if (!keyIds.length) { + const signatureList = msg.packets.filterByTag(enums.packet.signature); + signatureList.forEach(function(packet) { + keyIds.push(packet.issuerKeyId); + }); + } + return keyIds; } - return keyIds; -}; -/** - * Decrypt the message. Either a private key, a session key, or a password must be specified. - * @param {Array} privateKeys (optional) private keys with decrypted secret data - * @param {Array} passwords (optional) passwords used to decrypt - * @param {Array} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } - * @param {Boolean} streaming (optional) whether to process data as a stream - * @returns {Promise} new message with decrypted content - * @async - */ -Message.prototype.decrypt = async function(privateKeys, passwords, sessionKeys, streaming) { - const keyObjs = sessionKeys || await this.decryptSessionKeys(privateKeys, passwords); + /** + * Decrypt the message. Either a private key, a session key, or a password must be specified. + * @param {Array} privateKeys (optional) private keys with decrypted secret data + * @param {Array} passwords (optional) passwords used to decrypt + * @param {Array} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } + * @param {Boolean} streaming (optional) whether to process data as a stream + * @returns {Promise} new message with decrypted content + * @async + */ + async decrypt(privateKeys, passwords, sessionKeys, streaming) { + const keyObjs = sessionKeys || await this.decryptSessionKeys(privateKeys, passwords); + + const symEncryptedPacketlist = this.packets.filterByTag( + enums.packet.symmetricallyEncryptedData, + enums.packet.symEncryptedIntegrityProtectedData, + enums.packet.symEncryptedAEADProtectedData + ); + + if (symEncryptedPacketlist.length === 0) { + return this; + } - const symEncryptedPacketlist = this.packets.filterByTag( - enums.packet.symmetricallyEncryptedData, - enums.packet.symEncryptedIntegrityProtectedData, - enums.packet.symEncryptedAEADProtectedData - ); + const symEncryptedPacket = symEncryptedPacketlist[0]; + let exception = null; + const decryptedPromise = Promise.all(keyObjs.map(async keyObj => { + if (!keyObj || !util.isUint8Array(keyObj.data) || !util.isString(keyObj.algorithm)) { + throw new Error('Invalid session key for decryption.'); + } - if (symEncryptedPacketlist.length === 0) { - return this; - } + try { + await symEncryptedPacket.decrypt(keyObj.algorithm, keyObj.data, streaming); + } catch (e) { + util.printDebugError(e); + exception = e; + } + })); + // We don't await stream.cancel here because it only returns when the other copy is canceled too. + stream.cancel(symEncryptedPacket.encrypted); // Don't keep copy of encrypted data in memory. + symEncryptedPacket.encrypted = null; + await decryptedPromise; - const symEncryptedPacket = symEncryptedPacketlist[0]; - let exception = null; - const decryptedPromise = Promise.all(keyObjs.map(async keyObj => { - if (!keyObj || !util.isUint8Array(keyObj.data) || !util.isString(keyObj.algorithm)) { - throw new Error('Invalid session key for decryption.'); + if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) { + throw exception || new Error('Decryption failed.'); } - try { - await symEncryptedPacket.decrypt(keyObj.algorithm, keyObj.data, streaming); - } catch (e) { - util.printDebugError(e); - exception = e; - } - })); - // We don't await stream.cancel here because it only returns when the other copy is canceled too. - stream.cancel(symEncryptedPacket.encrypted); // Don't keep copy of encrypted data in memory. - symEncryptedPacket.encrypted = null; - await decryptedPromise; + const resultMsg = new Message(symEncryptedPacket.packets); + symEncryptedPacket.packets = new PacketList(); // remove packets after decryption - if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) { - throw exception || new Error('Decryption failed.'); + return resultMsg; } - const resultMsg = new Message(symEncryptedPacket.packets); - symEncryptedPacket.packets = new PacketList(); // remove packets after decryption - - return resultMsg; -}; - -/** - * Decrypt encrypted session keys either with private keys or passwords. - * @param {Array} privateKeys (optional) private keys with decrypted secret data - * @param {Array} passwords (optional) passwords used to decrypt - * @returns {Promise>} array of object with potential sessionKey, algorithm pairs - * @async - */ -Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) { - let keyPackets = []; - - let exception; - if (passwords) { - const symESKeyPacketlist = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); - if (!symESKeyPacketlist) { - throw new Error('No symmetrically encrypted session key packet found.'); - } - await Promise.all(passwords.map(async function(password, i) { - let packets; - if (i) { - packets = new PacketList(); - await packets.read(symESKeyPacketlist.write(), { SymEncryptedSessionKeyPacket }); - } else { - packets = symESKeyPacketlist; + /** + * Decrypt encrypted session keys either with private keys or passwords. + * @param {Array} privateKeys (optional) private keys with decrypted secret data + * @param {Array} passwords (optional) passwords used to decrypt + * @returns {Promise>} array of object with potential sessionKey, algorithm pairs + * @async + */ + async decryptSessionKeys(privateKeys, passwords) { + let keyPackets = []; + + let exception; + if (passwords) { + const symESKeyPacketlist = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); + if (!symESKeyPacketlist) { + throw new Error('No symmetrically encrypted session key packet found.'); } - await Promise.all(packets.map(async function(keyPacket) { - try { - await keyPacket.decrypt(password); - keyPackets.push(keyPacket); - } catch (err) { - util.printDebugError(err); + await Promise.all(passwords.map(async function(password, i) { + let packets; + if (i) { + packets = new PacketList(); + await packets.read(symESKeyPacketlist.write(), { SymEncryptedSessionKeyPacket }); + } else { + packets = symESKeyPacketlist; } - })); - })); - } else if (privateKeys) { - const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); - if (!pkESKeyPacketlist) { - throw new Error('No public key encrypted session key packet found.'); - } - await Promise.all(pkESKeyPacketlist.map(async function(keyPacket) { - await Promise.all(privateKeys.map(async function(privateKey) { - let algos = [ - enums.symmetric.aes256, // Old OpenPGP.js default fallback - enums.symmetric.aes128, // RFC4880bis fallback - enums.symmetric.tripledes, // RFC4880 fallback - enums.symmetric.cast5 // Golang OpenPGP fallback - ]; - try { - const primaryUser = await privateKey.getPrimaryUser(); // TODO: Pass userId from somewhere. - if (primaryUser.selfCertification.preferredSymmetricAlgorithms) { - algos = algos.concat(primaryUser.selfCertification.preferredSymmetricAlgorithms); - } - } catch (e) {} - - // do not check key expiration to allow decryption of old messages - const privateKeyPackets = (await privateKey.getDecryptionKeys(keyPacket.publicKeyId, null)).map(key => key.keyPacket); - await Promise.all(privateKeyPackets.map(async function(privateKeyPacket) { - if (!privateKeyPacket) { - return; - } - if (!privateKeyPacket.isDecrypted()) { - throw new Error('Private key is not decrypted.'); - } + await Promise.all(packets.map(async function(keyPacket) { try { - await keyPacket.decrypt(privateKeyPacket); - if (!algos.includes(enums.write(enums.symmetric, keyPacket.sessionKeyAlgorithm))) { - throw new Error('A non-preferred symmetric algorithm was used.'); - } + await keyPacket.decrypt(password); keyPackets.push(keyPacket); } catch (err) { util.printDebugError(err); - exception = err; } })); })); - stream.cancel(keyPacket.encrypted); // Don't keep copy of encrypted data in memory. - keyPacket.encrypted = null; - })); - } else { - throw new Error('No key or password specified.'); - } + } else if (privateKeys) { + const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); + if (!pkESKeyPacketlist) { + throw new Error('No public key encrypted session key packet found.'); + } + await Promise.all(pkESKeyPacketlist.map(async function(keyPacket) { + await Promise.all(privateKeys.map(async function(privateKey) { + let algos = [ + enums.symmetric.aes256, // Old OpenPGP.js default fallback + enums.symmetric.aes128, // RFC4880bis fallback + enums.symmetric.tripledes, // RFC4880 fallback + enums.symmetric.cast5 // Golang OpenPGP fallback + ]; + try { + const primaryUser = await privateKey.getPrimaryUser(); // TODO: Pass userId from somewhere. + if (primaryUser.selfCertification.preferredSymmetricAlgorithms) { + algos = algos.concat(primaryUser.selfCertification.preferredSymmetricAlgorithms); + } + } catch (e) {} - if (keyPackets.length) { - // Return only unique session keys - if (keyPackets.length > 1) { - const seen = {}; - keyPackets = keyPackets.filter(function(item) { - const k = item.sessionKeyAlgorithm + util.uint8ArrayToStr(item.sessionKey); - if (seen.hasOwnProperty(k)) { - return false; - } - seen[k] = true; - return true; - }); + // do not check key expiration to allow decryption of old messages + const privateKeyPackets = (await privateKey.getDecryptionKeys(keyPacket.publicKeyId, null)).map(key => key.keyPacket); + await Promise.all(privateKeyPackets.map(async function(privateKeyPacket) { + if (!privateKeyPacket) { + return; + } + if (!privateKeyPacket.isDecrypted()) { + throw new Error('Private key is not decrypted.'); + } + try { + await keyPacket.decrypt(privateKeyPacket); + if (!algos.includes(enums.write(enums.symmetric, keyPacket.sessionKeyAlgorithm))) { + throw new Error('A non-preferred symmetric algorithm was used.'); + } + keyPackets.push(keyPacket); + } catch (err) { + util.printDebugError(err); + exception = err; + } + })); + })); + stream.cancel(keyPacket.encrypted); // Don't keep copy of encrypted data in memory. + keyPacket.encrypted = null; + })); + } else { + throw new Error('No key or password specified.'); } - return keyPackets.map(packet => ({ data: packet.sessionKey, algorithm: packet.sessionKeyAlgorithm })); + if (keyPackets.length) { + // Return only unique session keys + if (keyPackets.length > 1) { + const seen = {}; + keyPackets = keyPackets.filter(function(item) { + const k = item.sessionKeyAlgorithm + util.uint8ArrayToStr(item.sessionKey); + if (seen.hasOwnProperty(k)) { + return false; + } + seen[k] = true; + return true; + }); + } + + return keyPackets.map(packet => ({ data: packet.sessionKey, algorithm: packet.sessionKeyAlgorithm })); + } + throw exception || new Error('Session key decryption failed.'); } - throw exception || new Error('Session key decryption failed.'); -}; -/** - * Get literal data that is the body of the message - * @returns {(Uint8Array|null)} literal body of the message as Uint8Array - */ -Message.prototype.getLiteralData = function() { - const msg = this.unwrapCompressed(); - const literal = msg.packets.findPacket(enums.packet.literalData); - return (literal && literal.getBytes()) || null; -}; + /** + * Get literal data that is the body of the message + * @returns {(Uint8Array|null)} literal body of the message as Uint8Array + */ + getLiteralData() { + const msg = this.unwrapCompressed(); + const literal = msg.packets.findPacket(enums.packet.literalData); + return (literal && literal.getBytes()) || null; + } -/** - * Get filename from literal data packet - * @returns {(String|null)} filename of literal data packet as string - */ -Message.prototype.getFilename = function() { - const msg = this.unwrapCompressed(); - const literal = msg.packets.findPacket(enums.packet.literalData); - return (literal && literal.getFilename()) || null; -}; + /** + * Get filename from literal data packet + * @returns {(String|null)} filename of literal data packet as string + */ + getFilename() { + const msg = this.unwrapCompressed(); + const literal = msg.packets.findPacket(enums.packet.literalData); + return (literal && literal.getFilename()) || null; + } -/** - * Get literal data as text - * @returns {(String|null)} literal body of the message interpreted as text - */ -Message.prototype.getText = function() { - const msg = this.unwrapCompressed(); - const literal = msg.packets.findPacket(enums.packet.literalData); - if (literal) { - return literal.getText(); + /** + * Get literal data as text + * @returns {(String|null)} literal body of the message interpreted as text + */ + getText() { + const msg = this.unwrapCompressed(); + const literal = msg.packets.findPacket(enums.packet.literalData); + if (literal) { + return literal.getText(); + } + return null; } - return null; -}; -/** - * Generate a new session key object, taking the algorithm preferences of the passed public keys into account, if any. - * @param {Array} keys (optional) public key(s) to select algorithm preferences for - * @param {Date} date (optional) date to select algorithm preferences at - * @param {Array} userIds (optional) user IDs to select algorithm preferences for - * @returns {Promise<{ data: Uint8Array, algorithm: String }>} object with session key data and algorithm - * @async - */ -export async function generateSessionKey(keys = [], date = new Date(), userIds = []) { - const algorithm = enums.read(enums.symmetric, await getPreferredAlgo('symmetric', keys, date, userIds)); - const aeadAlgorithm = config.aeadProtect && await isAeadSupported(keys, date, userIds) ? - enums.read(enums.aead, await getPreferredAlgo('aead', keys, date, userIds)) : - undefined; - const sessionKeyData = await crypto.generateSessionKey(algorithm); - return { data: sessionKeyData, algorithm, aeadAlgorithm }; -} + /** + * Generate a new session key object, taking the algorithm preferences of the passed public keys into account, if any. + * @param {Array} keys (optional) public key(s) to select algorithm preferences for + * @param {Date} date (optional) date to select algorithm preferences at + * @param {Array} userIds (optional) user IDs to select algorithm preferences for + * @returns {Promise<{ data: Uint8Array, algorithm: String }>} object with session key data and algorithm + * @async + */ + static async generateSessionKey(keys = [], date = new Date(), userIds = []) { + const algorithm = enums.read(enums.symmetric, await getPreferredAlgo('symmetric', keys, date, userIds)); + const aeadAlgorithm = config.aeadProtect && await isAeadSupported(keys, date, userIds) ? + enums.read(enums.aead, await getPreferredAlgo('aead', keys, date, userIds)) : + undefined; + const sessionKeyData = await crypto.generateSessionKey(algorithm); + return { data: sessionKeyData, algorithm, aeadAlgorithm }; + } -/** - * Encrypt the message either with public keys, passwords, or both at once. - * @param {Array} keys (optional) public key(s) for message encryption - * @param {Array} passwords (optional) password(s) for message encryption - * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } - * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs - * @param {Date} date (optional) override the creation date of the literal package - * @param {Array} userIds (optional) user IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] - * @param {Boolean} streaming (optional) whether to process data as a stream - * @returns {Promise} new message with encrypted content - * @async - */ -Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard = false, date = new Date(), userIds = [], streaming) { - if (sessionKey) { - if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { - throw new Error('Invalid session key for encryption.'); + /** + * Encrypt the message either with public keys, passwords, or both at once. + * @param {Array} keys (optional) public key(s) for message encryption + * @param {Array} passwords (optional) password(s) for message encryption + * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } + * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs + * @param {Date} date (optional) override the creation date of the literal package + * @param {Array} userIds (optional) user IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] + * @param {Boolean} streaming (optional) whether to process data as a stream + * @returns {Promise} new message with encrypted content + * @async + */ + async encrypt(keys, passwords, sessionKey, wildcard = false, date = new Date(), userIds = [], streaming) { + if (sessionKey) { + if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { + throw new Error('Invalid session key for encryption.'); + } + } else if (keys && keys.length) { + sessionKey = await Message.generateSessionKey(keys, date, userIds); + } else if (passwords && passwords.length) { + sessionKey = await Message.generateSessionKey(); + } else { + throw new Error('No keys, passwords, or session key provided.'); } - } else if (keys && keys.length) { - sessionKey = await generateSessionKey(keys, date, userIds); - } else if (passwords && passwords.length) { - sessionKey = await generateSessionKey(); - } else { - throw new Error('No keys, passwords, or session key provided.'); - } - const { data: sessionKeyData, algorithm, aeadAlgorithm } = sessionKey; + const { data: sessionKeyData, algorithm, aeadAlgorithm } = sessionKey; + + const msg = await Message.encryptSessionKey(sessionKeyData, algorithm, aeadAlgorithm, keys, passwords, wildcard, date, userIds); - const msg = await encryptSessionKey(sessionKeyData, algorithm, aeadAlgorithm, keys, passwords, wildcard, date, userIds); + let symEncryptedPacket; + if (aeadAlgorithm) { + symEncryptedPacket = new SymEncryptedAEADProtectedDataPacket(); + symEncryptedPacket.aeadAlgorithm = aeadAlgorithm; + } else if (config.integrityProtect) { + symEncryptedPacket = new SymEncryptedIntegrityProtectedDataPacket(); + } else { + symEncryptedPacket = new SymmetricallyEncryptedDataPacket(); + } + symEncryptedPacket.packets = this.packets; + + await symEncryptedPacket.encrypt(algorithm, sessionKeyData, streaming); - let symEncryptedPacket; - if (aeadAlgorithm) { - symEncryptedPacket = new SymEncryptedAEADProtectedDataPacket(); - symEncryptedPacket.aeadAlgorithm = aeadAlgorithm; - } else if (config.integrityProtect) { - symEncryptedPacket = new SymEncryptedIntegrityProtectedDataPacket(); - } else { - symEncryptedPacket = new SymmetricallyEncryptedDataPacket(); + msg.packets.push(symEncryptedPacket); + symEncryptedPacket.packets = new PacketList(); // remove packets after encryption + return msg; } - symEncryptedPacket.packets = this.packets; - await symEncryptedPacket.encrypt(algorithm, sessionKeyData, streaming); + /** + * Encrypt a session key either with public keys, passwords, or both at once. + * @param {Uint8Array} sessionKey session key for encryption + * @param {String} algorithm session key algorithm + * @param {String} aeadAlgorithm (optional) aead algorithm, e.g. 'eax' or 'ocb' + * @param {Array} publicKeys (optional) public key(s) for message encryption + * @param {Array} passwords (optional) for message encryption + * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs + * @param {Date} date (optional) override the date + * @param {Array} userIds (optional) user IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] + * @returns {Promise} new message with encrypted content + * @async + */ + static async encryptSessionKey(sessionKey, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard = false, date = new Date(), userIds = []) { + const packetlist = new PacketList(); + + if (publicKeys) { + const results = await Promise.all(publicKeys.map(async function(publicKey) { + const encryptionKey = await publicKey.getEncryptionKey(undefined, date, userIds); + const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket(); + pkESKeyPacket.publicKeyId = wildcard ? type_keyid.wildcard() : encryptionKey.getKeyId(); + pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm; + pkESKeyPacket.sessionKey = sessionKey; + pkESKeyPacket.sessionKeyAlgorithm = algorithm; + await pkESKeyPacket.encrypt(encryptionKey.keyPacket); + delete pkESKeyPacket.sessionKey; // delete plaintext session key after encryption + return pkESKeyPacket; + })); + packetlist.concat(results); + } + if (passwords) { + const testDecrypt = async function(keyPacket, password) { + try { + await keyPacket.decrypt(password); + return 1; + } catch (e) { + return 0; + } + }; - msg.packets.push(symEncryptedPacket); - symEncryptedPacket.packets = new PacketList(); // remove packets after encryption - return msg; -}; + const sum = (accumulator, currentValue) => accumulator + currentValue; -/** - * Encrypt a session key either with public keys, passwords, or both at once. - * @param {Uint8Array} sessionKey session key for encryption - * @param {String} algorithm session key algorithm - * @param {String} aeadAlgorithm (optional) aead algorithm, e.g. 'eax' or 'ocb' - * @param {Array} publicKeys (optional) public key(s) for message encryption - * @param {Array} passwords (optional) for message encryption - * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs - * @param {Date} date (optional) override the date - * @param {Array} userIds (optional) user IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] - * @returns {Promise} new message with encrypted content - * @async - */ -export async function encryptSessionKey(sessionKey, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard = false, date = new Date(), userIds = []) { - const packetlist = new PacketList(); + const encryptPassword = async function(sessionKey, algorithm, aeadAlgorithm, password) { + const symEncryptedSessionKeyPacket = new SymEncryptedSessionKeyPacket(); + symEncryptedSessionKeyPacket.sessionKey = sessionKey; + symEncryptedSessionKeyPacket.sessionKeyAlgorithm = algorithm; + if (aeadAlgorithm) { + symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgorithm; + } + await symEncryptedSessionKeyPacket.encrypt(password); - if (publicKeys) { - const results = await Promise.all(publicKeys.map(async function(publicKey) { - const encryptionKey = await publicKey.getEncryptionKey(undefined, date, userIds); - const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket(); - pkESKeyPacket.publicKeyId = wildcard ? type_keyid.wildcard() : encryptionKey.getKeyId(); - pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm; - pkESKeyPacket.sessionKey = sessionKey; - pkESKeyPacket.sessionKeyAlgorithm = algorithm; - await pkESKeyPacket.encrypt(encryptionKey.keyPacket); - delete pkESKeyPacket.sessionKey; // delete plaintext session key after encryption - return pkESKeyPacket; - })); - packetlist.concat(results); + if (config.passwordCollisionCheck) { + const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd))); + if (results.reduce(sum) !== 1) { + return encryptPassword(sessionKey, algorithm, password); + } + } + + delete symEncryptedSessionKeyPacket.sessionKey; // delete plaintext session key after encryption + return symEncryptedSessionKeyPacket; + }; + + const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, algorithm, aeadAlgorithm, pwd))); + packetlist.concat(results); + } + + return new Message(packetlist); } - if (passwords) { - const testDecrypt = async function(keyPacket, password) { - try { - await keyPacket.decrypt(password); - return 1; - } catch (e) { - return 0; - } - }; - const sum = (accumulator, currentValue) => accumulator + currentValue; + /** + * Sign the message (the literal data packet of the message) + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @param {Signature} signature (optional) any existing detached signature to add to the message + * @param {Date} date (optional) override the creation time of the signature + * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Boolean} streaming (optional) whether to process data as a stream + * @returns {Promise} new message with signed content + * @async + */ + async sign(privateKeys = [], signature = null, date = new Date(), userIds = [], streaming = false) { + const packetlist = new PacketList(); + + const literalDataPacket = this.packets.findPacket(enums.packet.literalData); + if (!literalDataPacket) { + throw new Error('No literal data packet to sign.'); + } - const encryptPassword = async function(sessionKey, algorithm, aeadAlgorithm, password) { - const symEncryptedSessionKeyPacket = new SymEncryptedSessionKeyPacket(); - symEncryptedSessionKeyPacket.sessionKey = sessionKey; - symEncryptedSessionKeyPacket.sessionKeyAlgorithm = algorithm; - if (aeadAlgorithm) { - symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgorithm; + let i; + let existingSigPacketlist; + // If data packet was created from Uint8Array, use binary, otherwise use text + const signatureType = literalDataPacket.text === null ? + enums.signature.binary : enums.signature.text; + + if (signature) { + existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); + for (i = existingSigPacketlist.length - 1; i >= 0; i--) { + const signaturePacket = existingSigPacketlist[i]; + const onePassSig = new OnePassSignaturePacket(); + onePassSig.signatureType = signaturePacket.signatureType; + onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; + onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; + onePassSig.issuerKeyId = signaturePacket.issuerKeyId; + if (!privateKeys.length && i === 0) { + onePassSig.flags = 1; + } + packetlist.push(onePassSig); } - await symEncryptedSessionKeyPacket.encrypt(password); + } - if (config.passwordCollisionCheck) { - const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd))); - if (results.reduce(sum) !== 1) { - return encryptPassword(sessionKey, algorithm, password); - } + await Promise.all(Array.from(privateKeys).reverse().map(async function (privateKey, i) { + if (privateKey.isPublic()) { + throw new Error('Need private key for signing'); + } + const signingKey = await privateKey.getSigningKey(undefined, date, userIds); + const onePassSig = new OnePassSignaturePacket(); + onePassSig.signatureType = signatureType; + onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKey.keyPacket, date, userIds); + onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm; + onePassSig.issuerKeyId = signingKey.getKeyId(); + if (i === privateKeys.length - 1) { + onePassSig.flags = 1; } + return onePassSig; + })).then(onePassSignatureList => { + onePassSignatureList.forEach(onePassSig => packetlist.push(onePassSig)); + }); - delete symEncryptedSessionKeyPacket.sessionKey; // delete plaintext session key after encryption - return symEncryptedSessionKeyPacket; - }; + packetlist.push(literalDataPacket); + packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, false, streaming)); - const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, algorithm, aeadAlgorithm, pwd))); - packetlist.concat(results); + return new Message(packetlist); } - return new Message(packetlist); -} + /** + * Compresses the message (the literal and -if signed- signature data packets of the message) + * @param {module:enums.compression} compression compression algorithm to be used + * @returns {module:message.Message} new message with compressed content + */ + compress(compression) { + if (compression === enums.compression.uncompressed) { + return this; + } -/** - * Sign the message (the literal data packet of the message) - * @param {Array} privateKeys private keys with decrypted secret key data for signing - * @param {Signature} signature (optional) any existing detached signature to add to the message - * @param {Date} date (optional) override the creation time of the signature - * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] - * @param {Boolean} streaming (optional) whether to process data as a stream - * @returns {Promise} new message with signed content - * @async - */ -Message.prototype.sign = async function(privateKeys = [], signature = null, date = new Date(), userIds = [], streaming = false) { - const packetlist = new PacketList(); + const compressed = new CompressedDataPacket(); + compressed.packets = this.packets; + compressed.algorithm = enums.read(enums.compression, compression); - const literalDataPacket = this.packets.findPacket(enums.packet.literalData); - if (!literalDataPacket) { - throw new Error('No literal data packet to sign.'); - } + const packetList = new PacketList(); + packetList.push(compressed); - let i; - let existingSigPacketlist; - // If data packet was created from Uint8Array, use binary, otherwise use text - const signatureType = literalDataPacket.text === null ? - enums.signature.binary : enums.signature.text; + return new Message(packetList); + } - if (signature) { - existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); - for (i = existingSigPacketlist.length - 1; i >= 0; i--) { - const signaturePacket = existingSigPacketlist[i]; - const onePassSig = new OnePassSignaturePacket(); - onePassSig.signatureType = signaturePacket.signatureType; - onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; - onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; - onePassSig.issuerKeyId = signaturePacket.issuerKeyId; - if (!privateKeys.length && i === 0) { - onePassSig.flags = 1; - } - packetlist.push(onePassSig); + /** + * Create a detached signature for the message (the literal data packet of the message) + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @param {Signature} signature (optional) any existing detached signature + * @param {Date} date (optional) override the creation time of the signature + * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Boolean} streaming (optional) whether to process data as a stream + * @returns {Promise} new detached signature of message content + * @async + */ + async signDetached(privateKeys = [], signature = null, date = new Date(), userIds = [], streaming = false) { + const literalDataPacket = this.packets.findPacket(enums.packet.literalData); + if (!literalDataPacket) { + throw new Error('No literal data packet to sign.'); } + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true, streaming)); } - await Promise.all(Array.from(privateKeys).reverse().map(async function (privateKey, i) { - if (privateKey.isPublic()) { - throw new Error('Need private key for signing'); + /** + * Verify message signatures + * @param {Array} keys array of keys to verify signatures + * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @param {Boolean} streaming (optional) whether to process data as a stream + * @returns {Promise>} list of signer's keyid and validity of signature + * @async + */ + async verify(keys, date = new Date(), streaming) { + const msg = this.unwrapCompressed(); + const literalDataList = msg.packets.filterByTag(enums.packet.literalData); + if (literalDataList.length !== 1) { + throw new Error('Can only verify message with one literal data packet.'); } - const signingKey = await privateKey.getSigningKey(undefined, date, userIds); - const onePassSig = new OnePassSignaturePacket(); - onePassSig.signatureType = signatureType; - onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKey.keyPacket, date, userIds); - onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm; - onePassSig.issuerKeyId = signingKey.getKeyId(); - if (i === privateKeys.length - 1) { - onePassSig.flags = 1; + if (!streaming) { + msg.packets.concat(await stream.readToEnd(msg.packets.stream, _ => _)); } - return onePassSig; - })).then(onePassSignatureList => { - onePassSignatureList.forEach(onePassSig => packetlist.push(onePassSig)); - }); - - packetlist.push(literalDataPacket); - packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, false, streaming)); + const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature).reverse(); + const signatureList = msg.packets.filterByTag(enums.packet.signature); + if (streaming && onePassSigList.length && !signatureList.length && msg.packets.stream) { + await Promise.all(onePassSigList.map(async onePassSig => { + onePassSig.correspondingSig = new Promise((resolve, reject) => { + onePassSig.correspondingSigResolve = resolve; + onePassSig.correspondingSigReject = reject; + }); + onePassSig.signatureData = stream.fromAsync(async () => (await onePassSig.correspondingSig).signatureData); + onePassSig.hashed = stream.readToEnd(await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false, streaming)); + onePassSig.hashed.catch(() => {}); + })); + msg.packets.stream = stream.transformPair(msg.packets.stream, async (readable, writable) => { + const reader = stream.getReader(readable); + const writer = stream.getWriter(writable); + try { + for (let i = 0; i < onePassSigList.length; i++) { + const { value: signature } = await reader.read(); + onePassSigList[i].correspondingSigResolve(signature); + } + await reader.readToEnd(); + await writer.ready; + await writer.close(); + } catch (e) { + onePassSigList.forEach(onePassSig => { + onePassSig.correspondingSigReject(e); + }); + await writer.abort(e); + } + }); + return createVerificationObjects(onePassSigList, literalDataList, keys, date, false, streaming); + } + return createVerificationObjects(signatureList, literalDataList, keys, date, false, streaming); + } - return new Message(packetlist); -}; + /** + * Verify detached message signature + * @param {Array} keys array of keys to verify signatures + * @param {Signature} signature + * @param {Date} date Verify the signature against the given date, i.e. check signature creation time < date < expiration time + * @returns {Promise>} list of signer's keyid and validity of signature + * @async + */ + verifyDetached(signature, keys, date = new Date()) { + const msg = this.unwrapCompressed(); + const literalDataList = msg.packets.filterByTag(enums.packet.literalData); + if (literalDataList.length !== 1) { + throw new Error('Can only verify message with one literal data packet.'); + } + const signatureList = signature.packets; + return createVerificationObjects(signatureList, literalDataList, keys, date, true); + } -/** - * Compresses the message (the literal and -if signed- signature data packets of the message) - * @param {module:enums.compression} compression compression algorithm to be used - * @returns {module:message.Message} new message with compressed content - */ -Message.prototype.compress = function(compression) { - if (compression === enums.compression.uncompressed) { + /** + * Unwrap compressed message + * @returns {module:message.Message} message Content of compressed message + */ + unwrapCompressed() { + const compressed = this.packets.filterByTag(enums.packet.compressedData); + if (compressed.length) { + return new Message(compressed[0].packets); + } return this; } - const compressed = new CompressedDataPacket(); - compressed.packets = this.packets; - compressed.algorithm = enums.read(enums.compression, compression); - - const packetList = new PacketList(); - packetList.push(compressed); + /** + * Append signature to unencrypted message object + * @param {String|Uint8Array} detachedSignature The detached ASCII-armored or Uint8Array PGP signature + */ + async appendSignature(detachedSignature) { + await this.packets.read(util.isUint8Array(detachedSignature) ? detachedSignature : (await armor.decode(detachedSignature)).data, { SignaturePacket }); + } - return new Message(packetList); -}; + /** + * Returns binary encoded message + * @returns {ReadableStream} binary message + */ + write() { + return this.packets.write(); + } -/** - * Create a detached signature for the message (the literal data packet of the message) - * @param {Array} privateKeys private keys with decrypted secret key data for signing - * @param {Signature} signature (optional) any existing detached signature - * @param {Date} date (optional) override the creation time of the signature - * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] - * @param {Boolean} streaming (optional) whether to process data as a stream - * @returns {Promise} new detached signature of message content - * @async - */ -Message.prototype.signDetached = async function(privateKeys = [], signature = null, date = new Date(), userIds = [], streaming = false) { - const literalDataPacket = this.packets.findPacket(enums.packet.literalData); - if (!literalDataPacket) { - throw new Error('No literal data packet to sign.'); + /** + * Returns ASCII armored text of message + * @returns {ReadableStream} ASCII armor + */ + armor() { + return armor.encode(enums.armor.message, this.write()); } - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true, streaming)); -}; +} /** * Create signature packets for the message @@ -559,76 +664,6 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig return packetlist; } -/** - * Verify message signatures - * @param {Array} keys array of keys to verify signatures - * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @param {Boolean} streaming (optional) whether to process data as a stream - * @returns {Promise>} list of signer's keyid and validity of signature - * @async - */ -Message.prototype.verify = async function(keys, date = new Date(), streaming) { - const msg = this.unwrapCompressed(); - const literalDataList = msg.packets.filterByTag(enums.packet.literalData); - if (literalDataList.length !== 1) { - throw new Error('Can only verify message with one literal data packet.'); - } - if (!streaming) { - msg.packets.concat(await stream.readToEnd(msg.packets.stream, _ => _)); - } - const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature).reverse(); - const signatureList = msg.packets.filterByTag(enums.packet.signature); - if (streaming && onePassSigList.length && !signatureList.length && msg.packets.stream) { - await Promise.all(onePassSigList.map(async onePassSig => { - onePassSig.correspondingSig = new Promise((resolve, reject) => { - onePassSig.correspondingSigResolve = resolve; - onePassSig.correspondingSigReject = reject; - }); - onePassSig.signatureData = stream.fromAsync(async () => (await onePassSig.correspondingSig).signatureData); - onePassSig.hashed = stream.readToEnd(await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false, streaming)); - onePassSig.hashed.catch(() => {}); - })); - msg.packets.stream = stream.transformPair(msg.packets.stream, async (readable, writable) => { - const reader = stream.getReader(readable); - const writer = stream.getWriter(writable); - try { - for (let i = 0; i < onePassSigList.length; i++) { - const { value: signature } = await reader.read(); - onePassSigList[i].correspondingSigResolve(signature); - } - await reader.readToEnd(); - await writer.ready; - await writer.close(); - } catch (e) { - onePassSigList.forEach(onePassSig => { - onePassSig.correspondingSigReject(e); - }); - await writer.abort(e); - } - }); - return createVerificationObjects(onePassSigList, literalDataList, keys, date, false, streaming); - } - return createVerificationObjects(signatureList, literalDataList, keys, date, false, streaming); -}; - -/** - * Verify detached message signature - * @param {Array} keys array of keys to verify signatures - * @param {Signature} signature - * @param {Date} date Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @returns {Promise>} list of signer's keyid and validity of signature - * @async - */ -Message.prototype.verifyDetached = function(signature, keys, date = new Date()) { - const msg = this.unwrapCompressed(); - const literalDataList = msg.packets.filterByTag(enums.packet.literalData); - if (literalDataList.length !== 1) { - throw new Error('Can only verify message with one literal data packet.'); - } - const signatureList = signature.packets; - return createVerificationObjects(signatureList, literalDataList, keys, date, true); -}; - /** * Create object containing signer's keyid and validity of signature * @param {SignaturePacket} signature signature packets @@ -710,42 +745,6 @@ export async function createVerificationObjects(signatureList, literalDataList, })); } -/** - * Unwrap compressed message - * @returns {module:message.Message} message Content of compressed message - */ -Message.prototype.unwrapCompressed = function() { - const compressed = this.packets.filterByTag(enums.packet.compressedData); - if (compressed.length) { - return new Message(compressed[0].packets); - } - return this; -}; - -/** - * Append signature to unencrypted message object - * @param {String|Uint8Array} detachedSignature The detached ASCII-armored or Uint8Array PGP signature - */ -Message.prototype.appendSignature = async function(detachedSignature) { - await this.packets.read(util.isUint8Array(detachedSignature) ? detachedSignature : (await armor.decode(detachedSignature)).data, { SignaturePacket }); -}; - -/** - * Returns binary encoded message - * @returns {ReadableStream} binary message - */ -Message.prototype.write = function() { - return this.packets.write(); -}; - -/** - * Returns ASCII armored text of message - * @returns {ReadableStream} ASCII armor - */ -Message.prototype.armor = function() { - return armor.encode(enums.armor.message, this.write()); -}; - /** * reads an OpenPGP armored message and returns a message object * @param {String | ReadableStream} armoredText text to be parsed diff --git a/src/openpgp.js b/src/openpgp.js index 9c255247..ac12a088 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -40,11 +40,7 @@ import stream from 'web-stream-tools'; import { createReadableStreamWrapper } from '@mattiasbuelens/web-streams-adapter'; -import { - Message, - generateSessionKey as messageGenerateSessionKey, - encryptSessionKey as messageEncryptSessionKey -} from './message'; +import { Message } from './message'; import { CleartextMessage } from './cleartext'; import { generate, reformat } from './key'; import config from './config/config'; @@ -411,7 +407,7 @@ export function generateSessionKey({ publicKeys, date = new Date(), toUserIds = return Promise.resolve().then(async function() { - return messageGenerateSessionKey(publicKeys, date, toUserIds); + return Message.generateSessionKey(publicKeys, date, toUserIds); }).catch(onError.bind(null, 'Error generating session key')); } @@ -437,7 +433,7 @@ export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, return Promise.resolve().then(async function() { - const message = await messageEncryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard, date, toUserIds); + const message = await Message.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard, date, toUserIds); return armor ? message.armor() : message.write(); }).catch(onError.bind(null, 'Error encrypting session key')); diff --git a/src/packet/compressed_data.js b/src/packet/compressed_data.js index 3a837a92..f6656407 100644 --- a/src/packet/compressed_data.js +++ b/src/packet/compressed_data.js @@ -47,91 +47,92 @@ import { * this packet is found as the contents of an encrypted packet, or following * a Signature or One-Pass Signature packet, and contains a literal data packet. * @memberof module:packet - * @constructor */ -function CompressedDataPacket() { - /** - * Packet type - * @type {module:enums.packet} - */ - this.tag = enums.packet.compressedData; - /** - * List of packets - * @type {PacketList} - */ - this.packets = null; - /** - * Compression algorithm - * @type {compression} - */ - this.algorithm = 'zip'; +class CompressedDataPacket { + constructor() { + /** + * Packet type + * @type {module:enums.packet} + */ + this.tag = enums.packet.compressedData; + /** + * List of packets + * @type {PacketList} + */ + this.packets = null; + /** + * Compression algorithm + * @type {compression} + */ + this.algorithm = 'zip'; + + /** + * Compressed packet data + * @type {Uint8Array | ReadableStream} + */ + this.compressed = null; + } /** - * Compressed packet data - * @type {Uint8Array | ReadableStream} + * Parsing function for the packet. + * @param {Uint8Array | ReadableStream} bytes Payload of a tag 8 packet */ - this.compressed = null; -} + async read(bytes, streaming) { + await stream.parse(bytes, async reader => { -/** - * Parsing function for the packet. - * @param {Uint8Array | ReadableStream} bytes Payload of a tag 8 packet - */ -CompressedDataPacket.prototype.read = async function (bytes, streaming) { - await stream.parse(bytes, async reader => { + // One octet that gives the algorithm used to compress the packet. + this.algorithm = enums.read(enums.compression, await reader.readByte()); - // One octet that gives the algorithm used to compress the packet. - this.algorithm = enums.read(enums.compression, await reader.readByte()); + // Compressed data, which makes up the remainder of the packet. + this.compressed = reader.remainder(); - // Compressed data, which makes up the remainder of the packet. - this.compressed = reader.remainder(); + await this.decompress(streaming); + }); + } - await this.decompress(streaming); - }); -}; + /** + * Return the compressed packet. + * @returns {Uint8Array | ReadableStream} binary compressed packet + */ + write() { + if (this.compressed === null) { + this.compress(); + } -/** - * Return the compressed packet. - * @returns {Uint8Array | ReadableStream} binary compressed packet - */ -CompressedDataPacket.prototype.write = function () { - if (this.compressed === null) { - this.compress(); + return util.concat([new Uint8Array([enums.write(enums.compression, this.algorithm)]), this.compressed]); } - return util.concat([new Uint8Array([enums.write(enums.compression, this.algorithm)]), this.compressed]); -}; + /** + * Decompression method for decompressing the compressed data + * read by read_packet + */ + async decompress(streaming) { -/** - * Decompression method for decompressing the compressed data - * read by read_packet - */ -CompressedDataPacket.prototype.decompress = async function (streaming) { + if (!decompress_fns[this.algorithm]) { + throw new Error(this.algorithm + ' decompression not supported'); + } - if (!decompress_fns[this.algorithm]) { - throw new Error(this.algorithm + ' decompression not supported'); + await this.packets.read(decompress_fns[this.algorithm](this.compressed), { + LiteralDataPacket, + OnePassSignaturePacket, + SignaturePacket + }, streaming); } - await this.packets.read(decompress_fns[this.algorithm](this.compressed), { - LiteralDataPacket, - OnePassSignaturePacket, - SignaturePacket - }, streaming); -}; + /** + * Compress the packet data (member decompressedData) + */ + compress() { -/** - * Compress the packet data (member decompressedData) - */ -CompressedDataPacket.prototype.compress = function () { + if (!compress_fns[this.algorithm]) { + throw new Error(this.algorithm + ' compression not supported'); + } - if (!compress_fns[this.algorithm]) { - throw new Error(this.algorithm + ' compression not supported'); + this.compressed = compress_fns[this.algorithm](this.packets.write()); } - - this.compressed = compress_fns[this.algorithm](this.packets.write()); -}; +} export default CompressedDataPacket; diff --git a/src/packet/literal_data.js b/src/packet/literal_data.js index de364a0e..a662fd8c 100644 --- a/src/packet/literal_data.js +++ b/src/packet/literal_data.js @@ -31,138 +31,141 @@ import util from '../util'; * {@link https://tools.ietf.org/html/rfc4880#section-5.9|RFC4880 5.9}: * A Literal Data packet contains the body of a message; data that is not to be * further interpreted. - * @param {Date} date the creation date of the literal package * @memberof module:packet - * @constructor */ -function LiteralDataPacket(date = new Date()) { - this.tag = enums.packet.literalData; - this.format = 'utf8'; // default format for literal data packets - this.date = util.normalizeDate(date); - this.text = null; // textual data representation - this.data = null; // literal data representation - this.filename = 'msg.txt'; -} +class LiteralDataPacket { + /** + * @param {Date} date the creation date of the literal package + */ + constructor(date = new Date()) { + this.tag = enums.packet.literalData; + this.format = 'utf8'; // default format for literal data packets + this.date = util.normalizeDate(date); + this.text = null; // textual data representation + this.data = null; // literal data representation + this.filename = 'msg.txt'; + } -/** - * Set the packet data to a javascript native string, end of line - * will be normalized to \r\n and by default text is converted to UTF8 - * @param {String | ReadableStream} text Any native javascript string - * @param {utf8|binary|text|mime} format (optional) The format of the string of bytes - */ -LiteralDataPacket.prototype.setText = function(text, format = 'utf8') { - this.format = format; - this.text = text; - this.data = null; -}; + /** + * Set the packet data to a javascript native string, end of line + * will be normalized to \r\n and by default text is converted to UTF8 + * @param {String | ReadableStream} text Any native javascript string + * @param {utf8|binary|text|mime} format (optional) The format of the string of bytes + */ + setText(text, format = 'utf8') { + this.format = format; + this.text = text; + this.data = null; + } -/** - * Returns literal data packets as native JavaScript string - * with normalized end of line to \n - * @param {Boolean} clone (optional) Whether to return a clone so that getBytes/getText can be called again - * @returns {String | ReadableStream} literal data as text - */ -LiteralDataPacket.prototype.getText = function(clone = false) { - if (this.text === null || util.isStream(this.text)) { // Assume that this.text has been read - this.text = util.decodeUtf8(util.nativeEOL(this.getBytes(clone))); + /** + * Returns literal data packets as native JavaScript string + * with normalized end of line to \n + * @param {Boolean} clone (optional) Whether to return a clone so that getBytes/getText can be called again + * @returns {String | ReadableStream} literal data as text + */ + getText(clone = false) { + if (this.text === null || util.isStream(this.text)) { // Assume that this.text has been read + this.text = util.decodeUtf8(util.nativeEOL(this.getBytes(clone))); + } + return this.text; } - return this.text; -}; -/** - * Set the packet data to value represented by the provided string of bytes. - * @param {Uint8Array | ReadableStream} bytes The string of bytes - * @param {utf8|binary|text|mime} format The format of the string of bytes - */ -LiteralDataPacket.prototype.setBytes = function(bytes, format) { - this.format = format; - this.data = bytes; - this.text = null; -}; + /** + * Set the packet data to value represented by the provided string of bytes. + * @param {Uint8Array | ReadableStream} bytes The string of bytes + * @param {utf8|binary|text|mime} format The format of the string of bytes + */ + setBytes(bytes, format) { + this.format = format; + this.data = bytes; + this.text = null; + } -/** - * Get the byte sequence representing the literal packet data - * @param {Boolean} clone (optional) Whether to return a clone so that getBytes/getText can be called again - * @returns {Uint8Array | ReadableStream} A sequence of bytes - */ -LiteralDataPacket.prototype.getBytes = function(clone = false) { - if (this.data === null) { - // encode UTF8 and normalize EOL to \r\n - this.data = util.canonicalizeEOL(util.encodeUtf8(this.text)); + /** + * Get the byte sequence representing the literal packet data + * @param {Boolean} clone (optional) Whether to return a clone so that getBytes/getText can be called again + * @returns {Uint8Array | ReadableStream} A sequence of bytes + */ + getBytes(clone = false) { + if (this.data === null) { + // encode UTF8 and normalize EOL to \r\n + this.data = util.canonicalizeEOL(util.encodeUtf8(this.text)); + } + if (clone) { + return stream.passiveClone(this.data); + } + return this.data; } - if (clone) { - return stream.passiveClone(this.data); - } - return this.data; -}; -/** - * Sets the filename of the literal packet data - * @param {String} filename Any native javascript string - */ -LiteralDataPacket.prototype.setFilename = function(filename) { - this.filename = filename; -}; + /** + * Sets the filename of the literal packet data + * @param {String} filename Any native javascript string + */ + setFilename(filename) { + this.filename = filename; + } -/** - * Get the filename of the literal packet data - * @returns {String} filename - */ -LiteralDataPacket.prototype.getFilename = function() { - return this.filename; -}; + /** + * Get the filename of the literal packet data + * @returns {String} filename + */ + getFilename() { + return this.filename; + } -/** - * Parsing function for a literal data packet (tag 11). - * - * @param {Uint8Array | ReadableStream} input Payload of a tag 11 packet - * @returns {LiteralDataPacket} object representation - */ -LiteralDataPacket.prototype.read = async function(bytes) { - await stream.parse(bytes, async reader => { - // - A one-octet field that describes how the data is formatted. - const format = enums.read(enums.literal, await reader.readByte()); + /** + * Parsing function for a literal data packet (tag 11). + * + * @param {Uint8Array | ReadableStream} input Payload of a tag 11 packet + * @returns {LiteralDataPacket} object representation + */ + async read(bytes) { + await stream.parse(bytes, async reader => { + // - A one-octet field that describes how the data is formatted. + const format = enums.read(enums.literal, await reader.readByte()); - const filename_len = await reader.readByte(); - this.filename = util.decodeUtf8(await reader.readBytes(filename_len)); + const filename_len = await reader.readByte(); + this.filename = util.decodeUtf8(await reader.readBytes(filename_len)); - this.date = util.readDate(await reader.readBytes(4)); + this.date = util.readDate(await reader.readBytes(4)); - const data = reader.remainder(); + const data = reader.remainder(); - this.setBytes(data, format); - }); -}; + this.setBytes(data, format); + }); + } -/** - * Creates a Uint8Array representation of the packet, excluding the data - * - * @returns {Uint8Array} Uint8Array representation of the packet - */ -LiteralDataPacket.prototype.writeHeader = function() { - const filename = util.encodeUtf8(this.filename); - const filename_length = new Uint8Array([filename.length]); + /** + * Creates a Uint8Array representation of the packet, excluding the data + * + * @returns {Uint8Array} Uint8Array representation of the packet + */ + writeHeader() { + const filename = util.encodeUtf8(this.filename); + const filename_length = new Uint8Array([filename.length]); - const format = new Uint8Array([enums.write(enums.literal, this.format)]); - const date = util.writeDate(this.date); + const format = new Uint8Array([enums.write(enums.literal, this.format)]); + const date = util.writeDate(this.date); - return util.concatUint8Array([format, filename_length, filename, date]); -}; + return util.concatUint8Array([format, filename_length, filename, date]); + } -/** - * Creates a Uint8Array representation of the packet - * - * @returns {Uint8Array | ReadableStream} Uint8Array representation of the packet - */ -LiteralDataPacket.prototype.write = function() { - const header = this.writeHeader(); - const data = this.getBytes(); + /** + * Creates a Uint8Array representation of the packet + * + * @returns {Uint8Array | ReadableStream} Uint8Array representation of the packet + */ + write() { + const header = this.writeHeader(); + const data = this.getBytes(); - return util.concat([header, data]); -}; + return util.concat([header, data]); + } +} export default LiteralDataPacket; diff --git a/src/packet/marker.js b/src/packet/marker.js index baee17e5..d0e44f58 100644 --- a/src/packet/marker.js +++ b/src/packet/marker.js @@ -32,31 +32,32 @@ import enums from '../enums'; * * Such a packet MUST be ignored when received. * @memberof module:packet - * @constructor */ -function MarkerPacket() { - this.tag = enums.packet.marker; -} +class MarkerPacket { + constructor() { + this.tag = enums.packet.marker; + } -/** - * Parsing function for a literal data packet (tag 10). - * - * @param {String} input Payload of a tag 10 packet - * @param {Integer} position - * Position to start reading from the input string - * @param {Integer} len - * Length of the packet or the remaining length of - * input at position - * @returns {MarkerPacket} Object representation - */ -MarkerPacket.prototype.read = function (bytes) { - if (bytes[0] === 0x50 && // P - bytes[1] === 0x47 && // G - bytes[2] === 0x50) { // P - return true; + /** + * Parsing function for a literal data packet (tag 10). + * + * @param {String} input Payload of a tag 10 packet + * @param {Integer} position + * Position to start reading from the input string + * @param {Integer} len + * Length of the packet or the remaining length of + * input at position + * @returns {MarkerPacket} Object representation + */ + read(bytes) { + if (bytes[0] === 0x50 && // P + bytes[1] === 0x47 && // G + bytes[2] === 0x50) { // P + return true; + } + // marker packet does not contain "PGP" + return false; } - // marker packet does not contain "PGP" - return false; -}; +} export default MarkerPacket; diff --git a/src/packet/one_pass_signature.js b/src/packet/one_pass_signature.js index 45c7ad38..e8c0de91 100644 --- a/src/packet/one_pass_signature.js +++ b/src/packet/one_pass_signature.js @@ -39,111 +39,113 @@ import util from '../util'; * packet to be placed at the end of the message, so that the signer * can compute the entire signed message in one pass. * @memberof module:packet - * @constructor */ -function OnePassSignaturePacket() { - /** - * Packet type - * @type {module:enums.packet} - */ - this.tag = enums.packet.onePassSignature; - /** A one-octet version number. The current version is 3. */ - this.version = null; - /** - * A one-octet signature type. - * Signature types are described in - * {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1}. - */ - this.signatureType = null; - /** - * A one-octet number describing the hash algorithm used. - * @see {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC4880 9.4} - */ - this.hashAlgorithm = null; +class OnePassSignaturePacket { + constructor() { + /** + * Packet type + * @type {module:enums.packet} + */ + this.tag = enums.packet.onePassSignature; + /** A one-octet version number. The current version is 3. */ + this.version = null; + /** + * A one-octet signature type. + * Signature types are described in + * {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1}. + */ + this.signatureType = null; + /** + * A one-octet number describing the hash algorithm used. + * @see {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC4880 9.4} + */ + this.hashAlgorithm = null; + /** + * A one-octet number describing the public-key algorithm used. + * @see {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC4880 9.1} + */ + this.publicKeyAlgorithm = null; + /** An eight-octet number holding the Key ID of the signing key. */ + this.issuerKeyId = null; + /** + * A one-octet number holding a flag showing whether the signature is nested. + * A zero value indicates that the next packet is another One-Pass Signature packet + * that describes another signature to be applied to the same message data. + */ + this.flags = null; + } + /** - * A one-octet number describing the public-key algorithm used. - * @see {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC4880 9.1} + * parsing function for a one-pass signature packet (tag 4). + * @param {Uint8Array} bytes payload of a tag 4 packet + * @returns {OnePassSignaturePacket} object representation */ - this.publicKeyAlgorithm = null; - /** An eight-octet number holding the Key ID of the signing key. */ - this.issuerKeyId = null; + read(bytes) { + let mypos = 0; + // A one-octet version number. The current version is 3. + this.version = bytes[mypos++]; + + // A one-octet signature type. Signature types are described in + // Section 5.2.1. + this.signatureType = bytes[mypos++]; + + // A one-octet number describing the hash algorithm used. + this.hashAlgorithm = bytes[mypos++]; + + // A one-octet number describing the public-key algorithm used. + this.publicKeyAlgorithm = bytes[mypos++]; + + // An eight-octet number holding the Key ID of the signing key. + this.issuerKeyId = new type_keyid(); + this.issuerKeyId.read(bytes.subarray(mypos, mypos + 8)); + mypos += 8; + + // A one-octet number holding a flag showing whether the signature + // is nested. A zero value indicates that the next packet is + // another One-Pass Signature packet that describes another + // signature to be applied to the same message data. + this.flags = bytes[mypos++]; + return this; + } + /** - * A one-octet number holding a flag showing whether the signature is nested. - * A zero value indicates that the next packet is another One-Pass Signature packet - * that describes another signature to be applied to the same message data. + * creates a string representation of a one-pass signature packet + * @returns {Uint8Array} a Uint8Array representation of a one-pass signature packet */ - this.flags = null; -} + write() { + const start = new Uint8Array([3, enums.write(enums.signature, this.signatureType), + enums.write(enums.hash, this.hashAlgorithm), + enums.write(enums.publicKey, this.publicKeyAlgorithm)]); -/** - * parsing function for a one-pass signature packet (tag 4). - * @param {Uint8Array} bytes payload of a tag 4 packet - * @returns {OnePassSignaturePacket} object representation - */ -OnePassSignaturePacket.prototype.read = function (bytes) { - let mypos = 0; - // A one-octet version number. The current version is 3. - this.version = bytes[mypos++]; - - // A one-octet signature type. Signature types are described in - // Section 5.2.1. - this.signatureType = bytes[mypos++]; - - // A one-octet number describing the hash algorithm used. - this.hashAlgorithm = bytes[mypos++]; - - // A one-octet number describing the public-key algorithm used. - this.publicKeyAlgorithm = bytes[mypos++]; - - // An eight-octet number holding the Key ID of the signing key. - this.issuerKeyId = new type_keyid(); - this.issuerKeyId.read(bytes.subarray(mypos, mypos + 8)); - mypos += 8; - - // A one-octet number holding a flag showing whether the signature - // is nested. A zero value indicates that the next packet is - // another One-Pass Signature packet that describes another - // signature to be applied to the same message data. - this.flags = bytes[mypos++]; - return this; -}; + const end = new Uint8Array([this.flags]); -/** - * creates a string representation of a one-pass signature packet - * @returns {Uint8Array} a Uint8Array representation of a one-pass signature packet - */ -OnePassSignaturePacket.prototype.write = function () { - const start = new Uint8Array([3, enums.write(enums.signature, this.signatureType), - enums.write(enums.hash, this.hashAlgorithm), - enums.write(enums.publicKey, this.publicKeyAlgorithm)]); + return util.concatUint8Array([start, this.issuerKeyId.write(), end]); + } - const end = new Uint8Array([this.flags]); + calculateTrailer(...args) { + return stream.fromAsync(async () => SignaturePacket.prototype.calculateTrailer.apply(await this.correspondingSig, args)); + } - return util.concatUint8Array([start, this.issuerKeyId.write(), end]); -}; + async verify() { + const correspondingSig = await this.correspondingSig; + if (!correspondingSig || correspondingSig.tag !== enums.packet.signature) { + throw new Error('Corresponding signature packet missing'); + } + if ( + correspondingSig.signatureType !== this.signatureType || + correspondingSig.hashAlgorithm !== this.hashAlgorithm || + correspondingSig.publicKeyAlgorithm !== this.publicKeyAlgorithm || + !correspondingSig.issuerKeyId.equals(this.issuerKeyId) + ) { + throw new Error('Corresponding signature packet does not match one-pass signature packet'); + } + correspondingSig.hashed = this.hashed; + return correspondingSig.verify.apply(correspondingSig, arguments); + } +} OnePassSignaturePacket.prototype.hash = SignaturePacket.prototype.hash; OnePassSignaturePacket.prototype.toHash = SignaturePacket.prototype.toHash; OnePassSignaturePacket.prototype.toSign = SignaturePacket.prototype.toSign; -OnePassSignaturePacket.prototype.calculateTrailer = function(...args) { - return stream.fromAsync(async () => SignaturePacket.prototype.calculateTrailer.apply(await this.correspondingSig, args)); -}; - -OnePassSignaturePacket.prototype.verify = async function() { - const correspondingSig = await this.correspondingSig; - if (!correspondingSig || correspondingSig.tag !== enums.packet.signature) { - throw new Error('Corresponding signature packet missing'); - } - if ( - correspondingSig.signatureType !== this.signatureType || - correspondingSig.hashAlgorithm !== this.hashAlgorithm || - correspondingSig.publicKeyAlgorithm !== this.publicKeyAlgorithm || - !correspondingSig.issuerKeyId.equals(this.issuerKeyId) - ) { - throw new Error('Corresponding signature packet does not match one-pass signature packet'); - } - correspondingSig.hashed = this.hashed; - return correspondingSig.verify.apply(correspondingSig, arguments); -}; export default OnePassSignaturePacket; diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index f054cf50..3b02caee 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -20,187 +20,176 @@ import util from '../util'; * Take care when iterating over it - the packets themselves * are stored as numerical indices. * @memberof module:packet - * @constructor * @extends Array */ -function PacketList() { +class PacketList extends Array { /** - * The number of packets contained within the list. - * @readonly - * @type {Integer} + * Reads a stream of binary data and interprents it as a list of packets. + * @param {Uint8Array | ReadableStream} A Uint8Array of bytes. */ - this.length = 0; -} - -PacketList.prototype = []; - -/** - * Reads a stream of binary data and interprents it as a list of packets. - * @param {Uint8Array | ReadableStream} A Uint8Array of bytes. - */ -PacketList.prototype.read = async function (bytes, allowedPackets, streaming) { - this.stream = stream.transformPair(bytes, async (readable, writable) => { - const writer = stream.getWriter(writable); - try { - while (true) { - await writer.ready; - const done = await packetParser.read(readable, streaming, async parsed => { - try { - const tag = enums.read(enums.packet, parsed.tag); - const packet = packets.newPacketFromTag(tag, allowedPackets); - packet.packets = new PacketList(); - packet.fromStream = util.isStream(parsed.packet); - await packet.read(parsed.packet, streaming); - await writer.write(packet); - } catch (e) { - if (!config.tolerant || packetParser.supportsStreaming(parsed.tag)) { - // The packets that support streaming are the ones that contain - // message data. Those are also the ones we want to be more strict - // about and throw on parse errors for. - await writer.abort(e); + async read(bytes, allowedPackets, streaming) { + this.stream = stream.transformPair(bytes, async (readable, writable) => { + const writer = stream.getWriter(writable); + try { + while (true) { + await writer.ready; + const done = await packetParser.read(readable, streaming, async parsed => { + try { + const tag = enums.read(enums.packet, parsed.tag); + const packet = packets.newPacketFromTag(tag, allowedPackets); + packet.packets = new PacketList(); + packet.fromStream = util.isStream(parsed.packet); + await packet.read(parsed.packet, streaming); + await writer.write(packet); + } catch (e) { + if (!config.tolerant || packetParser.supportsStreaming(parsed.tag)) { + // The packets that support streaming are the ones that contain + // message data. Those are also the ones we want to be more strict + // about and throw on parse errors for. + await writer.abort(e); + } + util.printDebugError(e); } - util.printDebugError(e); + }); + if (done) { + await writer.ready; + await writer.close(); + return; } - }); - if (done) { - await writer.ready; - await writer.close(); - return; } + } catch (e) { + await writer.abort(e); + } + }); + + // Wait until first few packets have been read + const reader = stream.getReader(this.stream); + while (true) { + const { done, value } = await reader.read(); + if (!done) { + this.push(value); + } else { + this.stream = null; + } + if (done || packetParser.supportsStreaming(value.tag)) { + break; } - } catch (e) { - await writer.abort(e); - } - }); - - // Wait until first few packets have been read - const reader = stream.getReader(this.stream); - while (true) { - const { done, value } = await reader.read(); - if (!done) { - this.push(value); - } else { - this.stream = null; - } - if (done || packetParser.supportsStreaming(value.tag)) { - break; } + reader.releaseLock(); } - reader.releaseLock(); -}; -/** - * Creates a binary representation of openpgp objects contained within the - * class instance. - * @returns {Uint8Array} A Uint8Array containing valid openpgp packets. - */ -PacketList.prototype.write = function () { - const arr = []; - - for (let i = 0; i < this.length; i++) { - const packetbytes = this[i].write(); - if (util.isStream(packetbytes) && packetParser.supportsStreaming(this[i].tag)) { - let buffer = []; - let bufferLength = 0; - const minLength = 512; - arr.push(packetParser.writeTag(this[i].tag)); - arr.push(stream.transform(packetbytes, value => { - buffer.push(value); - bufferLength += value.length; - if (bufferLength >= minLength) { - const powerOf2 = Math.min(Math.log(bufferLength) / Math.LN2 | 0, 30); - const chunkSize = 2 ** powerOf2; - const bufferConcat = util.concat([packetParser.writePartialLength(powerOf2)].concat(buffer)); - buffer = [bufferConcat.subarray(1 + chunkSize)]; - bufferLength = buffer[0].length; - return bufferConcat.subarray(0, 1 + chunkSize); - } - }, () => util.concat([packetParser.writeSimpleLength(bufferLength)].concat(buffer)))); - } else { - if (util.isStream(packetbytes)) { - let length = 0; - arr.push(stream.transform(stream.clone(packetbytes), value => { - length += value.length; - }, () => packetParser.writeHeader(this[i].tag, length))); + /** + * Creates a binary representation of openpgp objects contained within the + * class instance. + * @returns {Uint8Array} A Uint8Array containing valid openpgp packets. + */ + write() { + const arr = []; + + for (let i = 0; i < this.length; i++) { + const packetbytes = this[i].write(); + if (util.isStream(packetbytes) && packetParser.supportsStreaming(this[i].tag)) { + let buffer = []; + let bufferLength = 0; + const minLength = 512; + arr.push(packetParser.writeTag(this[i].tag)); + arr.push(stream.transform(packetbytes, value => { + buffer.push(value); + bufferLength += value.length; + if (bufferLength >= minLength) { + const powerOf2 = Math.min(Math.log(bufferLength) / Math.LN2 | 0, 30); + const chunkSize = 2 ** powerOf2; + const bufferConcat = util.concat([packetParser.writePartialLength(powerOf2)].concat(buffer)); + buffer = [bufferConcat.subarray(1 + chunkSize)]; + bufferLength = buffer[0].length; + return bufferConcat.subarray(0, 1 + chunkSize); + } + }, () => util.concat([packetParser.writeSimpleLength(bufferLength)].concat(buffer)))); } else { - arr.push(packetParser.writeHeader(this[i].tag, packetbytes.length)); + if (util.isStream(packetbytes)) { + let length = 0; + arr.push(stream.transform(stream.clone(packetbytes), value => { + length += value.length; + }, () => packetParser.writeHeader(this[i].tag, length))); + } else { + arr.push(packetParser.writeHeader(this[i].tag, packetbytes.length)); + } + arr.push(packetbytes); } - arr.push(packetbytes); } - } - return util.concat(arr); -}; - -/** - * Adds a packet to the list. This is the only supported method of doing so; - * writing to packetlist[i] directly will result in an error. - * @param {Object} packet Packet to push - */ -PacketList.prototype.push = function (packet) { - if (!packet) { - return; + return util.concat(arr); } - packet.packets = packet.packets || new PacketList(); + /** + * Adds a packet to the list. This is the only supported method of doing so; + * writing to packetlist[i] directly will result in an error. + * @param {Object} packet Packet to push + */ + push(packet) { + if (!packet) { + return; + } - this[this.length] = packet; - this.length++; -}; + packet.packets = packet.packets || new PacketList(); -/** - * Creates a new PacketList with all packets from the given types - */ -PacketList.prototype.filterByTag = function (...args) { - const filtered = new PacketList(); + super.push(packet); + } - const handle = tag => packetType => tag === packetType; + /** + * Creates a new PacketList with all packets from the given types + */ + filterByTag(...args) { + const filtered = new PacketList(); + + const handle = tag => packetType => tag === packetType; - for (let i = 0; i < this.length; i++) { - if (args.some(handle(this[i].tag))) { - filtered.push(this[i]); + for (let i = 0; i < this.length; i++) { + if (args.some(handle(this[i].tag))) { + filtered.push(this[i]); + } } - } - return filtered; -}; + return filtered; + } -/** - * Traverses packet tree and returns first matching packet - * @param {module:enums.packet} type The packet type - * @returns {module:packet/packet|undefined} - */ -PacketList.prototype.findPacket = function (type) { - return this.find(packet => packet.tag === type); -}; + /** + * Traverses packet tree and returns first matching packet + * @param {module:enums.packet} type The packet type + * @returns {module:packet/packet|undefined} + */ + findPacket(type) { + return this.find(packet => packet.tag === type); + } -/** - * Returns array of found indices by tag - */ -PacketList.prototype.indexOfTag = function (...args) { - const tagIndex = []; - const that = this; + /** + * Returns array of found indices by tag + */ + indexOfTag(...args) { + const tagIndex = []; + const that = this; - const handle = tag => packetType => tag === packetType; + const handle = tag => packetType => tag === packetType; - for (let i = 0; i < this.length; i++) { - if (args.some(handle(that[i].tag))) { - tagIndex.push(i); + for (let i = 0; i < this.length; i++) { + if (args.some(handle(that[i].tag))) { + tagIndex.push(i); + } } + return tagIndex; } - return tagIndex; -}; -/** - * Concatenates packetlist or array of packets - */ -PacketList.prototype.concat = function (packetlist) { - if (packetlist) { - for (let i = 0; i < packetlist.length; i++) { - this.push(packetlist[i]); + /** + * Concatenates packetlist or array of packets + */ + concat(packetlist) { + if (packetlist) { + for (let i = 0; i < packetlist.length; i++) { + this.push(packetlist[i]); + } } + return this; } - return this; -}; +} export default PacketList; diff --git a/src/packet/public_key.js b/src/packet/public_key.js index a74c28cb..497e78b5 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -44,222 +44,223 @@ import util from '../util'; * A Public-Key packet starts a series of packets that forms an OpenPGP * key (sometimes called an OpenPGP certificate). * @memberof module:packet - * @constructor */ -function PublicKeyPacket(date = new Date()) { - /** - * Packet type - * @type {module:enums.packet} - */ - this.tag = enums.packet.publicKey; - /** - * Packet version - * @type {Integer} - */ - this.version = config.v5Keys ? 5 : 4; - /** - * Key creation date. - * @type {Date} - */ - this.created = util.normalizeDate(date); - /** - * Public key algorithm. - * @type {String} - */ - this.algorithm = null; - /** - * Algorithm specific params - * @type {Array} - */ - this.params = []; - /** - * Time until expiration in days (V3 only) - * @type {Integer} - */ - this.expirationTimeV3 = 0; - /** - * Fingerprint in lowercase hex - * @type {String} - */ - this.fingerprint = null; +class PublicKeyPacket { + constructor(date = new Date()) { + /** + * Packet type + * @type {module:enums.packet} + */ + this.tag = enums.packet.publicKey; + /** + * Packet version + * @type {Integer} + */ + this.version = config.v5Keys ? 5 : 4; + /** + * Key creation date. + * @type {Date} + */ + this.created = util.normalizeDate(date); + /** + * Public key algorithm. + * @type {String} + */ + this.algorithm = null; + /** + * Algorithm specific params + * @type {Array} + */ + this.params = []; + /** + * Time until expiration in days (V3 only) + * @type {Integer} + */ + this.expirationTimeV3 = 0; + /** + * Fingerprint in lowercase hex + * @type {String} + */ + this.fingerprint = null; + /** + * Keyid + * @type {module:type/keyid} + */ + this.keyid = null; + } + /** - * Keyid - * @type {module:type/keyid} + * Internal Parser for public keys as specified in {@link https://tools.ietf.org/html/rfc4880#section-5.5.2|RFC 4880 section 5.5.2 Public-Key Packet Formats} + * called by read_tag<num> + * @param {Uint8Array} bytes Input array to read the packet from + * @returns {Object} This object with attributes set by the parser */ - this.keyid = null; -} - -/** - * Internal Parser for public keys as specified in {@link https://tools.ietf.org/html/rfc4880#section-5.5.2|RFC 4880 section 5.5.2 Public-Key Packet Formats} - * called by read_tag<num> - * @param {Uint8Array} bytes Input array to read the packet from - * @returns {Object} This object with attributes set by the parser - */ -PublicKeyPacket.prototype.read = function (bytes) { - let pos = 0; - // A one-octet version number (3, 4 or 5). - this.version = bytes[pos++]; + read(bytes) { + let pos = 0; + // A one-octet version number (3, 4 or 5). + this.version = bytes[pos++]; - if (this.version === 4 || this.version === 5) { - // - A four-octet number denoting the time that the key was created. - this.created = util.readDate(bytes.subarray(pos, pos + 4)); - pos += 4; + if (this.version === 4 || this.version === 5) { + // - A four-octet number denoting the time that the key was created. + this.created = util.readDate(bytes.subarray(pos, pos + 4)); + pos += 4; - // - A one-octet number denoting the public-key algorithm of this key. - this.algorithm = enums.read(enums.publicKey, bytes[pos++]); - const algo = enums.write(enums.publicKey, this.algorithm); + // - A one-octet number denoting the public-key algorithm of this key. + this.algorithm = enums.read(enums.publicKey, bytes[pos++]); + const algo = enums.write(enums.publicKey, this.algorithm); - if (this.version === 5) { - // - A four-octet scalar octet count for the following key material. - pos += 4; - } + if (this.version === 5) { + // - A four-octet scalar octet count for the following key material. + pos += 4; + } - // - A series of values comprising the key material. This is - // algorithm-specific and described in section XXXX. - const types = crypto.getPubKeyParamTypes(algo); - this.params = crypto.constructParams(types); + // - A series of values comprising the key material. This is + // algorithm-specific and described in section XXXX. + const types = crypto.getPubKeyParamTypes(algo); + this.params = crypto.constructParams(types); - for (let i = 0; i < types.length && pos < bytes.length; i++) { - pos += this.params[i].read(bytes.subarray(pos, bytes.length)); - if (pos > bytes.length) { - throw new Error('Error reading MPI @:' + pos); + for (let i = 0; i < types.length && pos < bytes.length; i++) { + pos += this.params[i].read(bytes.subarray(pos, bytes.length)); + if (pos > bytes.length) { + throw new Error('Error reading MPI @:' + pos); + } } - } - return pos; + return pos; + } + throw new Error('Version ' + this.version + ' of the key packet is unsupported.'); } - throw new Error('Version ' + this.version + ' of the key packet is unsupported.'); -}; - -/** - * Alias of read() - * @see PublicKeyPacket#read - */ -PublicKeyPacket.prototype.readPublicKey = PublicKeyPacket.prototype.read; -/** - * Same as write_private_key, but has less information because of - * public key. - * @returns {Uint8Array} OpenPGP packet body contents, - */ -PublicKeyPacket.prototype.write = function () { - const arr = []; - // Version - arr.push(new Uint8Array([this.version])); - arr.push(util.writeDate(this.created)); - // A one-octet number denoting the public-key algorithm of this key - const algo = enums.write(enums.publicKey, this.algorithm); - arr.push(new Uint8Array([algo])); + /** + * Same as write_private_key, but has less information because of + * public key. + * @returns {Uint8Array} OpenPGP packet body contents, + */ + write() { + const arr = []; + // Version + arr.push(new Uint8Array([this.version])); + arr.push(util.writeDate(this.created)); + // A one-octet number denoting the public-key algorithm of this key + const algo = enums.write(enums.publicKey, this.algorithm); + arr.push(new Uint8Array([algo])); - const paramCount = crypto.getPubKeyParamTypes(algo).length; - const params = util.concatUint8Array(this.params.slice(0, paramCount).map(param => param.write())); - if (this.version === 5) { - // A four-octet scalar octet count for the following key material - arr.push(util.writeNumber(params.length, 4)); + const paramCount = crypto.getPubKeyParamTypes(algo).length; + const params = util.concatUint8Array(this.params.slice(0, paramCount).map(param => param.write())); + if (this.version === 5) { + // A four-octet scalar octet count for the following key material + arr.push(util.writeNumber(params.length, 4)); + } + // Algorithm-specific params + arr.push(params); + return util.concatUint8Array(arr); } - // Algorithm-specific params - arr.push(params); - return util.concatUint8Array(arr); -}; - -/** - * Alias of write() - * @see PublicKeyPacket#write - */ -PublicKeyPacket.prototype.writePublicKey = PublicKeyPacket.prototype.write; -/** - * Write packet in order to be hashed; either for a signature or a fingerprint. - */ -PublicKeyPacket.prototype.writeForHash = function (version) { - const bytes = this.writePublicKey(); + /** + * Write packet in order to be hashed; either for a signature or a fingerprint. + */ + writeForHash(version) { + const bytes = this.writePublicKey(); - if (version === 5) { - return util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]); + if (version === 5) { + return util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]); + } + return util.concatUint8Array([new Uint8Array([0x99]), util.writeNumber(bytes.length, 2), bytes]); } - return util.concatUint8Array([new Uint8Array([0x99]), util.writeNumber(bytes.length, 2), bytes]); -}; -/** - * Check whether secret-key data is available in decrypted form. Returns null for public keys. - * @returns {Boolean|null} - */ -PublicKeyPacket.prototype.isDecrypted = function() { - return null; -}; + /** + * Check whether secret-key data is available in decrypted form. Returns null for public keys. + * @returns {Boolean|null} + */ + isDecrypted() { + return null; + } -/** - * Returns the creation time of the key - * @returns {Date} - */ -PublicKeyPacket.prototype.getCreationTime = function() { - return this.created; -}; + /** + * Returns the creation time of the key + * @returns {Date} + */ + getCreationTime() { + return this.created; + } -/** - * Calculates the key id of the key - * @returns {module:type/keyid} A 8 byte key id - */ -PublicKeyPacket.prototype.getKeyId = function () { - if (this.keyid) { + /** + * Calculates the key id of the key + * @returns {module:type/keyid} A 8 byte key id + */ + getKeyId() { + if (this.keyid) { + return this.keyid; + } + this.keyid = new type_keyid(); + if (this.version === 5) { + this.keyid.read(util.hexToUint8Array(this.getFingerprint()).subarray(0, 8)); + } else if (this.version === 4) { + this.keyid.read(util.hexToUint8Array(this.getFingerprint()).subarray(12, 20)); + } return this.keyid; } - this.keyid = new type_keyid(); - if (this.version === 5) { - this.keyid.read(util.hexToUint8Array(this.getFingerprint()).subarray(0, 8)); - } else if (this.version === 4) { - this.keyid.read(util.hexToUint8Array(this.getFingerprint()).subarray(12, 20)); - } - return this.keyid; -}; -/** - * Calculates the fingerprint of the key - * @returns {Uint8Array} A Uint8Array containing the fingerprint - */ -PublicKeyPacket.prototype.getFingerprintBytes = function () { - if (this.fingerprint) { + /** + * Calculates the fingerprint of the key + * @returns {Uint8Array} A Uint8Array containing the fingerprint + */ + getFingerprintBytes() { + if (this.fingerprint) { + return this.fingerprint; + } + const toHash = this.writeForHash(this.version); + if (this.version === 5) { + this.fingerprint = Sha256.bytes(toHash); + } else if (this.version === 4) { + this.fingerprint = Sha1.bytes(toHash); + } return this.fingerprint; } - const toHash = this.writeForHash(this.version); - if (this.version === 5) { - this.fingerprint = Sha256.bytes(toHash); - } else if (this.version === 4) { - this.fingerprint = Sha1.bytes(toHash); + + /** + * Calculates the fingerprint of the key + * @returns {String} A string containing the fingerprint in lowercase hex + */ + getFingerprint() { + return util.uint8ArrayToHex(this.getFingerprintBytes()); + } + + /** + * Calculates whether two keys have the same fingerprint without actually calculating the fingerprint + * @returns {Boolean} Whether the two keys have the same version and public key data + */ + hasSameFingerprintAs(other) { + return this.version === other.version && util.equalsUint8Array(this.writePublicKey(), other.writePublicKey()); } - return this.fingerprint; -}; -/** - * Calculates the fingerprint of the key - * @returns {String} A string containing the fingerprint in lowercase hex - */ -PublicKeyPacket.prototype.getFingerprint = function() { - return util.uint8ArrayToHex(this.getFingerprintBytes()); -}; + /** + * Returns algorithm information + * @returns {Object} An object of the form {algorithm: String, rsaBits:int, curve:String} + */ + getAlgorithmInfo() { + const result = {}; + result.algorithm = this.algorithm; + if (this.params[0] instanceof type_mpi) { + result.rsaBits = this.params[0].byteLength() * 8; + result.bits = result.rsaBits; // Deprecated. + } else { + result.curve = this.params[0].getName(); + } + return result; + } +} /** - * Calculates whether two keys have the same fingerprint without actually calculating the fingerprint - * @returns {Boolean} Whether the two keys have the same version and public key data + * Alias of read() + * @see PublicKeyPacket#read */ -PublicKeyPacket.prototype.hasSameFingerprintAs = function(other) { - return this.version === other.version && util.equalsUint8Array(this.writePublicKey(), other.writePublicKey()); -}; +PublicKeyPacket.prototype.readPublicKey = PublicKeyPacket.prototype.read; /** - * Returns algorithm information - * @returns {Object} An object of the form {algorithm: String, rsaBits:int, curve:String} + * Alias of write() + * @see PublicKeyPacket#write */ -PublicKeyPacket.prototype.getAlgorithmInfo = function () { - const result = {}; - result.algorithm = this.algorithm; - if (this.params[0] instanceof type_mpi) { - result.rsaBits = this.params[0].byteLength() * 8; - result.bits = result.rsaBits; // Deprecated. - } else { - result.curve = this.params[0].getName(); - } - return result; -}; +PublicKeyPacket.prototype.writePublicKey = PublicKeyPacket.prototype.write; export default PublicKeyPacket; diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 696bddc8..92ed20e6 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -44,106 +44,107 @@ import util from '../util'; * public key, decrypts the session key, and then uses the session key to * decrypt the message. * @memberof module:packet - * @constructor */ -function PublicKeyEncryptedSessionKeyPacket() { - this.tag = enums.packet.publicKeyEncryptedSessionKey; - this.version = 3; +class PublicKeyEncryptedSessionKeyPacket { + constructor() { + this.tag = enums.packet.publicKeyEncryptedSessionKey; + this.version = 3; - this.publicKeyId = new type_keyid(); - this.publicKeyAlgorithm = null; + this.publicKeyId = new type_keyid(); + this.publicKeyAlgorithm = null; - this.sessionKey = null; - this.sessionKeyAlgorithm = null; + this.sessionKey = null; + this.sessionKeyAlgorithm = null; - /** @type {Array} */ - this.encrypted = []; -} - -/** - * Parsing function for a publickey encrypted session key packet (tag 1). - * - * @param {Uint8Array} input Payload of a tag 1 packet - * @param {Integer} position Position to start reading from the input string - * @param {Integer} len Length of the packet or the remaining length of - * input at position - * @returns {PublicKeyEncryptedSessionKeyPacket} Object representation - */ -PublicKeyEncryptedSessionKeyPacket.prototype.read = function (bytes) { - this.version = bytes[0]; - this.publicKeyId.read(bytes.subarray(1, bytes.length)); - this.publicKeyAlgorithm = enums.read(enums.publicKey, bytes[9]); - - let i = 10; - - const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); - const types = crypto.getEncSessionKeyParamTypes(algo); - this.encrypted = crypto.constructParams(types); - - for (let j = 0; j < types.length; j++) { - i += this.encrypted[j].read(bytes.subarray(i, bytes.length)); + /** @type {Array} */ + this.encrypted = []; } -}; - -/** - * Create a string representation of a tag 1 packet - * - * @returns {Uint8Array} The Uint8Array representation - */ -PublicKeyEncryptedSessionKeyPacket.prototype.write = function () { - const arr = [new Uint8Array([this.version]), this.publicKeyId.write(), new Uint8Array([enums.write(enums.publicKey, this.publicKeyAlgorithm)])]; - for (let i = 0; i < this.encrypted.length; i++) { - arr.push(this.encrypted[i].write()); + /** + * Parsing function for a publickey encrypted session key packet (tag 1). + * + * @param {Uint8Array} input Payload of a tag 1 packet + * @param {Integer} position Position to start reading from the input string + * @param {Integer} len Length of the packet or the remaining length of + * input at position + * @returns {PublicKeyEncryptedSessionKeyPacket} Object representation + */ + read(bytes) { + this.version = bytes[0]; + this.publicKeyId.read(bytes.subarray(1, bytes.length)); + this.publicKeyAlgorithm = enums.read(enums.publicKey, bytes[9]); + + let i = 10; + + const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); + const types = crypto.getEncSessionKeyParamTypes(algo); + this.encrypted = crypto.constructParams(types); + + for (let j = 0; j < types.length; j++) { + i += this.encrypted[j].read(bytes.subarray(i, bytes.length)); + } } - return util.concatUint8Array(arr); -}; + /** + * Create a string representation of a tag 1 packet + * + * @returns {Uint8Array} The Uint8Array representation + */ + write() { + const arr = [new Uint8Array([this.version]), this.publicKeyId.write(), new Uint8Array([enums.write(enums.publicKey, this.publicKeyAlgorithm)])]; -/** - * Encrypt session key packet - * @param {PublicKeyPacket} key Public key - * @returns {Promise} - * @async - */ -PublicKeyEncryptedSessionKeyPacket.prototype.encrypt = async function (key) { - let data = String.fromCharCode(enums.write(enums.symmetric, this.sessionKeyAlgorithm)); + for (let i = 0; i < this.encrypted.length; i++) { + arr.push(this.encrypted[i].write()); + } - data += util.uint8ArrayToStr(this.sessionKey); - data += util.uint8ArrayToStr(util.writeChecksum(this.sessionKey)); - const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); - this.encrypted = await crypto.publicKeyEncrypt( - algo, key.params, data, key.getFingerprintBytes()); - return true; -}; + return util.concatUint8Array(arr); + } -/** - * Decrypts the session key (only for public key encrypted session key - * packets (tag 1) - * - * @param {SecretKeyPacket} key - * Private key with secret params unlocked - * @returns {Promise} - * @async - */ -PublicKeyEncryptedSessionKeyPacket.prototype.decrypt = async function (key) { - const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); - const keyAlgo = enums.write(enums.publicKey, key.algorithm); - // check that session key algo matches the secret key algo - if (algo !== keyAlgo) { - throw new Error('Decryption error'); + /** + * Encrypt session key packet + * @param {PublicKeyPacket} key Public key + * @returns {Promise} + * @async + */ + async encrypt(key) { + let data = String.fromCharCode(enums.write(enums.symmetric, this.sessionKeyAlgorithm)); + + data += util.uint8ArrayToStr(this.sessionKey); + data += util.uint8ArrayToStr(util.writeChecksum(this.sessionKey)); + const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); + this.encrypted = await crypto.publicKeyEncrypt( + algo, key.params, data, key.getFingerprintBytes()); + return true; } - const decoded = await crypto.publicKeyDecrypt(algo, key.params, this.encrypted, key.getFingerprintBytes()); - const checksum = util.strToUint8Array(decoded.substr(decoded.length - 2)); - key = util.strToUint8Array(decoded.substring(1, decoded.length - 2)); - - if (!util.equalsUint8Array(checksum, util.writeChecksum(key))) { - throw new Error('Decryption error'); - } else { - this.sessionKey = key; - this.sessionKeyAlgorithm = enums.read(enums.symmetric, decoded.charCodeAt(0)); + + /** + * Decrypts the session key (only for public key encrypted session key + * packets (tag 1) + * + * @param {SecretKeyPacket} key + * Private key with secret params unlocked + * @returns {Promise} + * @async + */ + async decrypt(key) { + const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); + const keyAlgo = enums.write(enums.publicKey, key.algorithm); + // check that session key algo matches the secret key algo + if (algo !== keyAlgo) { + throw new Error('Decryption error'); + } + const decoded = await crypto.publicKeyDecrypt(algo, key.params, this.encrypted, key.getFingerprintBytes()); + const checksum = util.strToUint8Array(decoded.substr(decoded.length - 2)); + key = util.strToUint8Array(decoded.substring(1, decoded.length - 2)); + + if (!util.equalsUint8Array(checksum, util.writeChecksum(key))) { + throw new Error('Decryption error'); + } else { + this.sessionKey = key; + this.sessionKeyAlgorithm = enums.read(enums.symmetric, decoded.charCodeAt(0)); + } + return true; } - return true; -}; +} export default PublicKeyEncryptedSessionKeyPacket; diff --git a/src/packet/public_subkey.js b/src/packet/public_subkey.js index 5903d410..dec082f0 100644 --- a/src/packet/public_subkey.js +++ b/src/packet/public_subkey.js @@ -30,15 +30,13 @@ import enums from '../enums'; * provides signature services, and the subkeys provide encryption * services. * @memberof module:packet - * @constructor * @extends PublicKeyPacket */ -function PublicSubkeyPacket() { - PublicKeyPacket.call(this); - this.tag = enums.packet.publicSubkey; +class PublicSubkeyPacket extends PublicKeyPacket { + constructor() { + super(); + this.tag = enums.packet.publicSubkey; + } } -PublicSubkeyPacket.prototype = new PublicKeyPacket(); -PublicSubkeyPacket.prototype.constructor = PublicSubkeyPacket; - export default PublicSubkeyPacket; diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 6ee2f7bf..376d8ae3 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -35,418 +35,417 @@ import util from '../util'; * Public-Key packet, including the public-key material, but also * includes the secret-key material after all the public-key fields. * @memberof module:packet - * @constructor * @extends PublicKeyPacket */ -function SecretKeyPacket(date = new Date()) { - PublicKeyPacket.call(this, date); - /** - * Packet type - * @type {module:enums.packet} - */ - this.tag = enums.packet.secretKey; - /** - * Secret-key data - */ - this.keyMaterial = null; - /** - * Indicates whether secret-key data is encrypted. `this.isEncrypted === false` means data is available in decrypted form. - */ - this.isEncrypted = null; - /** - * S2K usage - * @type {Integer} - */ - this.s2k_usage = 0; - /** - * S2K object - * @type {type/s2k} - */ - this.s2k = null; - /** - * Symmetric algorithm - * @type {String} - */ - this.symmetric = null; +class SecretKeyPacket extends PublicKeyPacket { + constructor(date = new Date()) { + super(date); + /** + * Packet type + * @type {module:enums.packet} + */ + this.tag = enums.packet.secretKey; + /** + * Secret-key data + */ + this.keyMaterial = null; + /** + * Indicates whether secret-key data is encrypted. `this.isEncrypted === false` means data is available in decrypted form. + */ + this.isEncrypted = null; + /** + * S2K usage + * @type {Integer} + */ + this.s2k_usage = 0; + /** + * S2K object + * @type {type/s2k} + */ + this.s2k = null; + /** + * Symmetric algorithm + * @type {String} + */ + this.symmetric = null; + /** + * AEAD algorithm + * @type {String} + */ + this.aead = null; + } + + // 5.5.3. Secret-Key Packet Formats + /** - * AEAD algorithm - * @type {String} + * Internal parser for private keys as specified in + * {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3} + * @param {String} bytes Input string to read the packet from */ - this.aead = null; -} + read(bytes) { + // - A Public-Key or Public-Subkey packet, as described above. + let i = this.readPublicKey(bytes); + + // - One octet indicating string-to-key usage conventions. Zero + // indicates that the secret-key data is not encrypted. 255 or 254 + // indicates that a string-to-key specifier is being given. Any + // other value is a symmetric-key encryption algorithm identifier. + this.s2k_usage = bytes[i++]; + + // - Only for a version 5 packet, a one-octet scalar octet count of + // the next 4 optional fields. + if (this.version === 5) { + i++; + } -SecretKeyPacket.prototype = new PublicKeyPacket(); -SecretKeyPacket.prototype.constructor = SecretKeyPacket; + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // one-octet symmetric encryption algorithm. + if (this.s2k_usage === 255 || this.s2k_usage === 254 || this.s2k_usage === 253) { + this.symmetric = bytes[i++]; + this.symmetric = enums.read(enums.symmetric, this.symmetric); + + // - [Optional] If string-to-key usage octet was 253, a one-octet + // AEAD algorithm. + if (this.s2k_usage === 253) { + this.aead = bytes[i++]; + this.aead = enums.read(enums.aead, this.aead); + } -// Helper function + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // string-to-key specifier. The length of the string-to-key + // specifier is implied by its type, as described above. + this.s2k = new type_s2k(); + i += this.s2k.read(bytes.subarray(i, bytes.length)); -function parse_cleartext_params(cleartext, algorithm) { - const algo = enums.write(enums.publicKey, algorithm); - const types = crypto.getPrivKeyParamTypes(algo); - const params = crypto.constructParams(types); - let p = 0; - - for (let i = 0; i < types.length && p < cleartext.length; i++) { - p += params[i].read(cleartext.subarray(p, cleartext.length)); - if (p > cleartext.length) { - throw new Error('Error reading param @:' + p); + if (this.s2k.type === 'gnu-dummy') { + return; + } + } else if (this.s2k_usage) { + this.symmetric = this.s2k_usage; + this.symmetric = enums.read(enums.symmetric, this.symmetric); } - } - return params; -} + // - [Optional] If secret data is encrypted (string-to-key usage octet + // not zero), an Initial Vector (IV) of the same length as the + // cipher's block size. + if (this.s2k_usage) { + this.iv = bytes.subarray( + i, + i + crypto.cipher[this.symmetric].blockSize + ); -function write_cleartext_params(params, algorithm) { - const arr = []; - const algo = enums.write(enums.publicKey, algorithm); - const numPublicParams = crypto.getPubKeyParamTypes(algo).length; + i += this.iv.length; + } - for (let i = numPublicParams; i < params.length; i++) { - arr.push(params[i].write()); - } + // - Only for a version 5 packet, a four-octet scalar octet count for + // the following key material. + if (this.version === 5) { + i += 4; + } - return util.concatUint8Array(arr); -} + // - Plain or encrypted multiprecision integers comprising the secret + // key data. These algorithm-specific fields are as described + // below. + this.keyMaterial = bytes.subarray(i); + this.isEncrypted = !!this.s2k_usage; + if (!this.isEncrypted) { + const cleartext = this.keyMaterial.subarray(0, -2); + if (!util.equalsUint8Array(util.writeChecksum(cleartext), this.keyMaterial.subarray(-2))) { + throw new Error('Key checksum mismatch'); + } + const privParams = parse_cleartext_params(cleartext, this.algorithm); + this.params = this.params.concat(privParams); + } + } -// 5.5.3. Secret-Key Packet Formats + /** + * Creates an OpenPGP key packet for the given key. + * @returns {String} A string of bytes containing the secret key OpenPGP packet + */ + write() { + const arr = [this.writePublicKey()]; -/** - * Internal parser for private keys as specified in - * {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3} - * @param {String} bytes Input string to read the packet from - */ -SecretKeyPacket.prototype.read = function (bytes) { - // - A Public-Key or Public-Subkey packet, as described above. - let i = this.readPublicKey(bytes); - - // - One octet indicating string-to-key usage conventions. Zero - // indicates that the secret-key data is not encrypted. 255 or 254 - // indicates that a string-to-key specifier is being given. Any - // other value is a symmetric-key encryption algorithm identifier. - this.s2k_usage = bytes[i++]; - - // - Only for a version 5 packet, a one-octet scalar octet count of - // the next 4 optional fields. - if (this.version === 5) { - i++; - } + arr.push(new Uint8Array([this.s2k_usage])); - // - [Optional] If string-to-key usage octet was 255, 254, or 253, a - // one-octet symmetric encryption algorithm. - if (this.s2k_usage === 255 || this.s2k_usage === 254 || this.s2k_usage === 253) { - this.symmetric = bytes[i++]; - this.symmetric = enums.read(enums.symmetric, this.symmetric); + const optionalFieldsArr = []; + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // one- octet symmetric encryption algorithm. + if (this.s2k_usage === 255 || this.s2k_usage === 254 || this.s2k_usage === 253) { + optionalFieldsArr.push(enums.write(enums.symmetric, this.symmetric)); + + // - [Optional] If string-to-key usage octet was 253, a one-octet + // AEAD algorithm. + if (this.s2k_usage === 253) { + optionalFieldsArr.push(enums.write(enums.aead, this.aead)); + } - // - [Optional] If string-to-key usage octet was 253, a one-octet - // AEAD algorithm. - if (this.s2k_usage === 253) { - this.aead = bytes[i++]; - this.aead = enums.read(enums.aead, this.aead); + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // string-to-key specifier. The length of the string-to-key + // specifier is implied by its type, as described above. + optionalFieldsArr.push(...this.s2k.write()); } - // - [Optional] If string-to-key usage octet was 255, 254, or 253, a - // string-to-key specifier. The length of the string-to-key - // specifier is implied by its type, as described above. - this.s2k = new type_s2k(); - i += this.s2k.read(bytes.subarray(i, bytes.length)); + // - [Optional] If secret data is encrypted (string-to-key usage octet + // not zero), an Initial Vector (IV) of the same length as the + // cipher's block size. + if (this.s2k_usage && this.s2k.type !== 'gnu-dummy') { + optionalFieldsArr.push(...this.iv); + } - if (this.s2k.type === 'gnu-dummy') { - return; + if (this.version === 5) { + arr.push(new Uint8Array([optionalFieldsArr.length])); } - } else if (this.s2k_usage) { - this.symmetric = this.s2k_usage; - this.symmetric = enums.read(enums.symmetric, this.symmetric); - } + arr.push(new Uint8Array(optionalFieldsArr)); + + if (!this.s2k || this.s2k.type !== 'gnu-dummy') { + if (!this.s2k_usage) { + const cleartextParams = write_cleartext_params(this.params, this.algorithm); + this.keyMaterial = util.concatUint8Array([ + cleartextParams, + util.writeChecksum(cleartextParams) + ]); + } - // - [Optional] If secret data is encrypted (string-to-key usage octet - // not zero), an Initial Vector (IV) of the same length as the - // cipher's block size. - if (this.s2k_usage) { - this.iv = bytes.subarray( - i, - i + crypto.cipher[this.symmetric].blockSize - ); + if (this.version === 5) { + arr.push(util.writeNumber(this.keyMaterial.length, 4)); + } + arr.push(this.keyMaterial); + } - i += this.iv.length; + return util.concatUint8Array(arr); } - // - Only for a version 5 packet, a four-octet scalar octet count for - // the following key material. - if (this.version === 5) { - i += 4; + /** + * Check whether secret-key data is available in decrypted form. Returns null for public keys. + * @returns {Boolean|null} + */ + isDecrypted() { + return this.isEncrypted === false; } - // - Plain or encrypted multiprecision integers comprising the secret - // key data. These algorithm-specific fields are as described - // below. - this.keyMaterial = bytes.subarray(i); - this.isEncrypted = !!this.s2k_usage; + /** + * Check whether this is a gnu-dummy key + * @returns {Boolean} + */ + isDummy() { + return !!(this.s2k && this.s2k.type === 'gnu-dummy'); + } - if (!this.isEncrypted) { - const cleartext = this.keyMaterial.subarray(0, -2); - if (!util.equalsUint8Array(util.writeChecksum(cleartext), this.keyMaterial.subarray(-2))) { - throw new Error('Key checksum mismatch'); + /** + * Remove private key material, converting the key to a dummy one + * The resulting key cannot be used for signing/decrypting but can still verify signatures + */ + makeDummy() { + if (this.isDummy()) { + return; } - const privParams = parse_cleartext_params(cleartext, this.algorithm); - this.params = this.params.concat(privParams); + if (!this.isDecrypted()) { + // this is technically not needed, but makes the conversion simpler + throw new Error("Key is not decrypted"); + } + this.clearPrivateParams(); + this.keyMaterial = null; + this.isEncrypted = false; + this.s2k = new type_s2k(); + this.s2k.algorithm = 0; + this.s2k.c = 0; + this.s2k.type = 'gnu-dummy'; + this.s2k_usage = 254; + this.symmetric = 'aes256'; } -}; -/** - * Creates an OpenPGP key packet for the given key. - * @returns {String} A string of bytes containing the secret key OpenPGP packet - */ -SecretKeyPacket.prototype.write = function () { - const arr = [this.writePublicKey()]; - - arr.push(new Uint8Array([this.s2k_usage])); + /** + * Encrypt the payload. By default, we use aes256 and iterated, salted string + * to key specifier. If the key is in a decrypted state (isEncrypted === false) + * and the passphrase is empty or undefined, the key will be set as not encrypted. + * This can be used to remove passphrase protection after calling decrypt(). + * @param {String} passphrase + * @returns {Promise} + * @async + */ + async encrypt(passphrase) { + if (this.isDummy()) { + return false; + } - const optionalFieldsArr = []; - // - [Optional] If string-to-key usage octet was 255, 254, or 253, a - // one- octet symmetric encryption algorithm. - if (this.s2k_usage === 255 || this.s2k_usage === 254 || this.s2k_usage === 253) { - optionalFieldsArr.push(enums.write(enums.symmetric, this.symmetric)); + if (!this.isDecrypted()) { + throw new Error('Key packet is already encrypted'); + } - // - [Optional] If string-to-key usage octet was 253, a one-octet - // AEAD algorithm. - if (this.s2k_usage === 253) { - optionalFieldsArr.push(enums.write(enums.aead, this.aead)); + if (this.isDecrypted() && !passphrase) { + this.s2k_usage = 0; + return false; + } else if (!passphrase) { + throw new Error('The key must be decrypted before removing passphrase protection.'); } - // - [Optional] If string-to-key usage octet was 255, 254, or 253, a - // string-to-key specifier. The length of the string-to-key - // specifier is implied by its type, as described above. - optionalFieldsArr.push(...this.s2k.write()); - } + this.s2k = new type_s2k(); + this.s2k.salt = await crypto.random.getRandomBytes(8); + const cleartext = write_cleartext_params(this.params, this.algorithm); + this.symmetric = 'aes256'; + const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); + const blockLen = crypto.cipher[this.symmetric].blockSize; + this.iv = await crypto.random.getRandomBytes(blockLen); - // - [Optional] If secret data is encrypted (string-to-key usage octet - // not zero), an Initial Vector (IV) of the same length as the - // cipher's block size. - if (this.s2k_usage && this.s2k.type !== 'gnu-dummy') { - optionalFieldsArr.push(...this.iv); + if (this.version === 5) { + this.s2k_usage = 253; + this.aead = 'eax'; + const mode = crypto[this.aead]; + const modeInstance = await mode(this.symmetric, key); + this.keyMaterial = await modeInstance.encrypt(cleartext, this.iv.subarray(0, mode.ivLength), new Uint8Array()); + } else { + this.s2k_usage = 254; + this.keyMaterial = await crypto.cfb.encrypt(this.symmetric, key, util.concatUint8Array([ + cleartext, + await crypto.hash.sha1(cleartext) + ]), this.iv); + } + return true; } - if (this.version === 5) { - arr.push(new Uint8Array([optionalFieldsArr.length])); - } - arr.push(new Uint8Array(optionalFieldsArr)); - - if (!this.isDummy()) { - if (!this.s2k_usage) { - const cleartextParams = write_cleartext_params(this.params, this.algorithm); - this.keyMaterial = util.concatUint8Array([ - cleartextParams, - util.writeChecksum(cleartextParams) - ]); + /** + * Decrypts the private key params which are needed to use the key. + * {@link SecretKeyPacket.isDecrypted} should be false, as + * otherwise calls to this function will throw an error. + * @param {String} passphrase The passphrase for this private key as string + * @returns {Promise} + * @async + */ + async decrypt(passphrase) { + if (this.isDummy()) { + this.isEncrypted = false; + return false; } - if (this.version === 5) { - arr.push(util.writeNumber(this.keyMaterial.length, 4)); + if (this.isDecrypted()) { + throw new Error('Key packet is already decrypted.'); } - arr.push(this.keyMaterial); - } - return util.concatUint8Array(arr); -}; + let key; + if (this.s2k_usage === 254 || this.s2k_usage === 253) { + key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); + } else if (this.s2k_usage === 255) { + throw new Error('Encrypted private key is authenticated using an insecure two-byte hash'); + } else { + throw new Error('Private key is encrypted using an insecure S2K function: unsalted MD5'); + } -/** - * Check whether secret-key data is available in decrypted form. Returns null for public keys. - * @returns {Boolean|null} - */ -SecretKeyPacket.prototype.isDecrypted = function() { - return this.isEncrypted === false; -}; + let cleartext; + if (this.s2k_usage === 253) { + const mode = crypto[this.aead]; + try { + const modeInstance = await mode(this.symmetric, key); + cleartext = await modeInstance.decrypt(this.keyMaterial, this.iv.subarray(0, mode.ivLength), new Uint8Array()); + } catch (err) { + if (err.message === 'Authentication tag mismatch') { + throw new Error('Incorrect key passphrase: ' + err.message); + } + throw err; + } + } else { + const cleartextWithHash = await crypto.cfb.decrypt(this.symmetric, key, this.keyMaterial, this.iv); -/** - * Check whether this is a gnu-dummy key - * @returns {Boolean} - */ -SecretKeyPacket.prototype.isDummy = function() { - return !!(this.s2k && this.s2k.type === 'gnu-dummy'); -}; + cleartext = cleartextWithHash.subarray(0, -20); + const hash = await crypto.hash.sha1(cleartext); -/** - * Remove private key material, converting the key to a dummy one - * The resulting key cannot be used for signing/decrypting but can still verify signatures - */ -SecretKey.prototype.makeDummy = function () { - if (this.isDummy()) { - return; - } - if (!this.isDecrypted()) { - // this is technically not needed, but makes the conversion simpler - throw new Error("Key is not decrypted"); - } - this.clearPrivateParams(); - this.keyMaterial = null; - this.isEncrypted = false; - this.s2k = new type_s2k(); - this.s2k.algorithm = 0; - this.s2k.c = 0; - this.s2k.type = 'gnu-dummy'; - this.s2k_usage = 254; - this.symmetric = 'aes256'; -}; + if (!util.equalsUint8Array(hash, cleartextWithHash.subarray(-20))) { + throw new Error('Incorrect key passphrase'); + } + } -/** - * Encrypt the payload. By default, we use aes256 and iterated, salted string - * to key specifier. If the key is in a decrypted state (isEncrypted === false) - * and the passphrase is empty or undefined, the key will be set as not encrypted. - * This can be used to remove passphrase protection after calling decrypt(). - * @param {String} passphrase - * @returns {Promise} - * @async - */ -SecretKeyPacket.prototype.encrypt = async function (passphrase) { - if (this.isDummy()) { - return false; - } + const privParams = parse_cleartext_params(cleartext, this.algorithm); + this.params = this.params.concat(privParams); + this.isEncrypted = false; + this.keyMaterial = null; + this.s2k_usage = 0; - if (!this.isDecrypted()) { - throw new Error('Key packet is already encrypted'); + return true; } - if (this.isDecrypted() && !passphrase) { - this.s2k_usage = 0; - return false; - } else if (!passphrase) { - throw new Error('The key must be decrypted before removing passphrase protection.'); - } + /** + * Checks that the key parameters are consistent + * @throws {Error} if validation was not successful + * @async + */ + async validate() { + if (this.isDummy()) { + return; + } - this.s2k = new type_s2k(); - this.s2k.salt = await crypto.random.getRandomBytes(8); - const cleartext = write_cleartext_params(this.params, this.algorithm); - this.symmetric = 'aes256'; - const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); - const blockLen = crypto.cipher[this.symmetric].blockSize; - this.iv = await crypto.random.getRandomBytes(blockLen); - - if (this.version === 5) { - this.s2k_usage = 253; - this.aead = 'eax'; - const mode = crypto[this.aead]; - const modeInstance = await mode(this.symmetric, key); - this.keyMaterial = await modeInstance.encrypt(cleartext, this.iv.subarray(0, mode.ivLength), new Uint8Array()); - } else { - this.s2k_usage = 254; - this.keyMaterial = await crypto.cfb.encrypt(this.symmetric, key, util.concatUint8Array([ - cleartext, - await crypto.hash.sha1(cleartext) - ]), this.iv); + if (!this.isDecrypted()) { + throw new Error('Key is not decrypted'); + } + + const algo = enums.write(enums.publicKey, this.algorithm); + const validParams = await crypto.validateParams(algo, this.params); + if (!validParams) { + throw new Error('Key is invalid'); + } } - return true; -}; -async function produceEncryptionKey(s2k, passphrase, algorithm) { - return s2k.produce_key( - passphrase, - crypto.cipher[algorithm].keySize - ); -} -/** - * Decrypts the private key params which are needed to use the key. - * {@link SecretKeyPacket.isDecrypted} should be false, as - * otherwise calls to this function will throw an error. - * @param {String} passphrase The passphrase for this private key as string - * @returns {Promise} - * @async - */ -SecretKeyPacket.prototype.decrypt = async function (passphrase) { - if (this.isDummy()) { + async generate(bits, curve) { + const algo = enums.write(enums.publicKey, this.algorithm); + this.params = await crypto.generateParams(algo, bits, curve); this.isEncrypted = false; - return false; } - if (this.isDecrypted()) { - throw new Error('Key packet is already decrypted.'); - } + /** + * Clear private key parameters + */ + clearPrivateParams() { + if (this.s2k && this.s2k.type === 'gnu-dummy') { + this.isEncrypted = true; + return; + } - let key; - if (this.s2k_usage === 254 || this.s2k_usage === 253) { - key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); - } else if (this.s2k_usage === 255) { - throw new Error('Encrypted private key is authenticated using an insecure two-byte hash'); - } else { - throw new Error('Private key is encrypted using an insecure S2K function: unsalted MD5'); + const algo = enums.write(enums.publicKey, this.algorithm); + const publicParamCount = crypto.getPubKeyParamTypes(algo).length; + this.params.slice(publicParamCount).forEach(param => { + param.data.fill(0); + }); + this.params.length = publicParamCount; + this.isEncrypted = true; } +} - let cleartext; - if (this.s2k_usage === 253) { - const mode = crypto[this.aead]; - try { - const modeInstance = await mode(this.symmetric, key); - cleartext = await modeInstance.decrypt(this.keyMaterial, this.iv.subarray(0, mode.ivLength), new Uint8Array()); - } catch (err) { - if (err.message === 'Authentication tag mismatch') { - throw new Error('Incorrect key passphrase: ' + err.message); - } - throw err; - } - } else { - const cleartextWithHash = await crypto.cfb.decrypt(this.symmetric, key, this.keyMaterial, this.iv); +// Helper function - cleartext = cleartextWithHash.subarray(0, -20); - const hash = await crypto.hash.sha1(cleartext); +function parse_cleartext_params(cleartext, algorithm) { + const algo = enums.write(enums.publicKey, algorithm); + const types = crypto.getPrivKeyParamTypes(algo); + const params = crypto.constructParams(types); + let p = 0; - if (!util.equalsUint8Array(hash, cleartextWithHash.subarray(-20))) { - throw new Error('Incorrect key passphrase'); + for (let i = 0; i < types.length && p < cleartext.length; i++) { + p += params[i].read(cleartext.subarray(p, cleartext.length)); + if (p > cleartext.length) { + throw new Error('Error reading param @:' + p); } } - const privParams = parse_cleartext_params(cleartext, this.algorithm); - this.params = this.params.concat(privParams); - this.isEncrypted = false; - this.keyMaterial = null; - this.s2k_usage = 0; - - return true; -}; - -SecretKeyPacket.prototype.generate = async function (bits, curve) { - const algo = enums.write(enums.publicKey, this.algorithm); - this.params = await crypto.generateParams(algo, bits, curve); - this.isEncrypted = false; -}; + return params; +} -/** - * Checks that the key parameters are consistent - * @throws {Error} if validation was not successful - * @async - */ -SecretKeyPacket.prototype.validate = async function () { - if (this.isDummy()) { - return; - } +function write_cleartext_params(params, algorithm) { + const arr = []; + const algo = enums.write(enums.publicKey, algorithm); + const numPublicParams = crypto.getPubKeyParamTypes(algo).length; - if (!this.isDecrypted()) { - throw new Error('Key is not decrypted'); + for (let i = numPublicParams; i < params.length; i++) { + arr.push(params[i].write()); } - const algo = enums.write(enums.publicKey, this.algorithm); - const validParams = await crypto.validateParams(algo, this.params); - if (!validParams) { - throw new Error('Key is invalid'); - } -}; + return util.concatUint8Array(arr); +} -/** - * Clear private key parameters - */ -SecretKeyPacket.prototype.clearPrivateParams = function () { - if (this.s2k && this.s2k.type === 'gnu-dummy') { - this.isEncrypted = true; - return; - } - const algo = enums.write(enums.publicKey, this.algorithm); - const publicParamCount = crypto.getPubKeyParamTypes(algo).length; - this.params.slice(publicParamCount).forEach(param => { - param.data.fill(0); - }); - this.params.length = publicParamCount; - this.isEncrypted = true; -}; +async function produceEncryptionKey(s2k, passphrase, algorithm) { + return s2k.produce_key( + passphrase, + crypto.cipher[algorithm].keySize + ); +} export default SecretKeyPacket; diff --git a/src/packet/secret_subkey.js b/src/packet/secret_subkey.js index 2d2932dc..86904734 100644 --- a/src/packet/secret_subkey.js +++ b/src/packet/secret_subkey.js @@ -27,15 +27,13 @@ import enums from '../enums'; * A Secret-Subkey packet (tag 7) is the subkey analog of the Secret * Key packet and has exactly the same format. * @memberof module:packet - * @constructor * @extends SecretKeyPacket */ -function SecretSubkeyPacket(date = new Date()) { - SecretKeyPacket.call(this, date); - this.tag = enums.packet.secretSubkey; +class SecretSubkeyPacket extends SecretKeyPacket { + constructor(date = new Date()) { + super(date); + this.tag = enums.packet.secretSubkey; + } } -SecretSubkeyPacket.prototype = new SecretKeyPacket(); -SecretSubkeyPacket.prototype.constructor = SecretSubkeyPacket; - export default SecretSubkeyPacket; diff --git a/src/packet/signature.js b/src/packet/signature.js index f05dae96..68196cc9 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -42,736 +42,736 @@ import config from '../config'; * some data. The most common signatures are a signature of a file or a * block of text, and a signature that is a certification of a User ID. * @memberof module:packet - * @constructor - * @param {Date} date the creation date of the signature */ -function SignaturePacket(date = new Date()) { - this.tag = enums.packet.signature; - this.version = 4; // This is set to 5 below if we sign with a V5 key. - this.signatureType = null; - this.hashAlgorithm = null; - this.publicKeyAlgorithm = null; - - this.signatureData = null; - this.unhashedSubpackets = []; - this.signedHashValue = null; - - this.created = util.normalizeDate(date); - this.signatureExpirationTime = null; - this.signatureNeverExpires = true; - this.exportable = null; - this.trustLevel = null; - this.trustAmount = null; - this.regularExpression = null; - this.revocable = null; - this.keyExpirationTime = null; - this.keyNeverExpires = null; - this.preferredSymmetricAlgorithms = null; - this.revocationKeyClass = null; - this.revocationKeyAlgorithm = null; - this.revocationKeyFingerprint = null; - this.issuerKeyId = new type_keyid(); - this.rawNotations = []; - this.notations = {}; - this.preferredHashAlgorithms = null; - this.preferredCompressionAlgorithms = null; - this.keyServerPreferences = null; - this.preferredKeyServer = null; - this.isPrimaryUserID = null; - this.policyURI = null; - this.keyFlags = null; - this.signersUserId = null; - this.reasonForRevocationFlag = null; - this.reasonForRevocationString = null; - this.features = null; - this.signatureTargetPublicKeyAlgorithm = null; - this.signatureTargetHashAlgorithm = null; - this.signatureTargetHash = null; - this.embeddedSignature = null; - this.issuerKeyVersion = null; - this.issuerFingerprint = null; - this.preferredAeadAlgorithms = null; - - this.verified = null; - this.revoked = null; -} - -/** - * parsing function for a signature packet (tag 2). - * @param {String} bytes payload of a tag 2 packet - * @returns {SignaturePacket} object representation - */ -SignaturePacket.prototype.read = function (bytes) { - let i = 0; - this.version = bytes[i++]; - - if (this.version !== 4 && this.version !== 5) { - throw new Error('Version ' + this.version + ' of the signature is unsupported.'); - } - - this.signatureType = bytes[i++]; - this.publicKeyAlgorithm = bytes[i++]; - this.hashAlgorithm = bytes[i++]; +class SignaturePacket { + /** + * @param {Date} date the creation date of the signature + */ + constructor(date = new Date()) { + this.tag = enums.packet.signature; + this.version = 4; // This is set to 5 below if we sign with a V5 key. + this.signatureType = null; + this.hashAlgorithm = null; + this.publicKeyAlgorithm = null; + + this.signatureData = null; + this.unhashedSubpackets = []; + this.signedHashValue = null; + + this.created = util.normalizeDate(date); + this.signatureExpirationTime = null; + this.signatureNeverExpires = true; + this.exportable = null; + this.trustLevel = null; + this.trustAmount = null; + this.regularExpression = null; + this.revocable = null; + this.keyExpirationTime = null; + this.keyNeverExpires = null; + this.preferredSymmetricAlgorithms = null; + this.revocationKeyClass = null; + this.revocationKeyAlgorithm = null; + this.revocationKeyFingerprint = null; + this.issuerKeyId = new type_keyid(); + this.rawNotations = []; + this.notations = {}; + this.preferredHashAlgorithms = null; + this.preferredCompressionAlgorithms = null; + this.keyServerPreferences = null; + this.preferredKeyServer = null; + this.isPrimaryUserID = null; + this.policyURI = null; + this.keyFlags = null; + this.signersUserId = null; + this.reasonForRevocationFlag = null; + this.reasonForRevocationString = null; + this.features = null; + this.signatureTargetPublicKeyAlgorithm = null; + this.signatureTargetHashAlgorithm = null; + this.signatureTargetHash = null; + this.embeddedSignature = null; + this.issuerKeyVersion = null; + this.issuerFingerprint = null; + this.preferredAeadAlgorithms = null; + + this.verified = null; + this.revoked = null; + } + + /** + * parsing function for a signature packet (tag 2). + * @param {String} bytes payload of a tag 2 packet + * @returns {SignaturePacket} object representation + */ + read(bytes) { + let i = 0; + this.version = bytes[i++]; + + if (this.version !== 4 && this.version !== 5) { + throw new Error('Version ' + this.version + ' of the signature is unsupported.'); + } - // hashed subpackets - i += this.read_sub_packets(bytes.subarray(i, bytes.length), true); + this.signatureType = bytes[i++]; + this.publicKeyAlgorithm = bytes[i++]; + this.hashAlgorithm = bytes[i++]; - // A V4 signature hashes the packet body - // starting from its first field, the version number, through the end - // of the hashed subpacket data. Thus, the fields hashed are the - // signature version, the signature type, the public-key algorithm, the - // hash algorithm, the hashed subpacket length, and the hashed - // subpacket body. - this.signatureData = bytes.subarray(0, i); + // hashed subpackets + i += this.read_sub_packets(bytes.subarray(i, bytes.length), true); - // unhashed subpackets - i += this.read_sub_packets(bytes.subarray(i, bytes.length), false); + // A V4 signature hashes the packet body + // starting from its first field, the version number, through the end + // of the hashed subpacket data. Thus, the fields hashed are the + // signature version, the signature type, the public-key algorithm, the + // hash algorithm, the hashed subpacket length, and the hashed + // subpacket body. + this.signatureData = bytes.subarray(0, i); - // Two-octet field holding left 16 bits of signed hash value. - this.signedHashValue = bytes.subarray(i, i + 2); - i += 2; + // unhashed subpackets + i += this.read_sub_packets(bytes.subarray(i, bytes.length), false); - this.signature = bytes.subarray(i, bytes.length); -}; + // Two-octet field holding left 16 bits of signed hash value. + this.signedHashValue = bytes.subarray(i, i + 2); + i += 2; -SignaturePacket.prototype.write = function () { - const arr = []; - arr.push(this.signatureData); - arr.push(this.write_unhashed_sub_packets()); - arr.push(this.signedHashValue); - arr.push(stream.clone(this.signature)); - return util.concat(arr); -}; - -/** - * Signs provided data. This needs to be done prior to serialization. - * @param {SecretKeyPacket} key private key used to sign the message. - * @param {Object} data Contains packets to be signed. - * @param {Boolean} detached (optional) whether to create a detached signature - * @param {Boolean} streaming (optional) whether to process data as a stream - * @returns {Promise} - * @async - */ -SignaturePacket.prototype.sign = async function (key, data, detached = false, streaming = false) { - const signatureType = enums.write(enums.signature, this.signatureType); - const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); - const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); - - if (key.version === 5) { - this.version = 5; + this.signature = bytes.subarray(i, bytes.length); } - const arr = [new Uint8Array([this.version, signatureType, publicKeyAlgorithm, hashAlgorithm])]; - - this.issuerKeyVersion = key.version; - this.issuerFingerprint = key.getFingerprintBytes(); - this.issuerKeyId = key.getKeyId(); - - // Add hashed subpackets - arr.push(this.write_hashed_sub_packets()); - - this.signatureData = util.concat(arr); - - const toHash = this.toHash(signatureType, data, detached); - const hash = await this.hash(signatureType, data, toHash, detached); - - this.signedHashValue = stream.slice(stream.clone(hash), 0, 2); - const params = key.params; - const signed = async () => crypto.signature.sign( - publicKeyAlgorithm, hashAlgorithm, params, toHash, await stream.readToEnd(hash) - ); - if (streaming) { - this.signature = stream.fromAsync(signed); - } else { - this.signature = await signed(); - - // Store the fact that this signature is valid, e.g. for when we call `await - // getLatestValidSignature(this.revocationSignatures, key, data)` later. - // Note that this only holds up if the key and data passed to verify are the - // same as the ones passed to sign. - this.verified = true; - } - return true; -}; -/** - * Creates Uint8Array of bytes of all subpacket data except Issuer and Embedded Signature subpackets - * @returns {Uint8Array} subpacket data - */ -SignaturePacket.prototype.write_hashed_sub_packets = function () { - const sub = enums.signatureSubpacket; - const arr = []; - let bytes; - if (this.created !== null) { - arr.push(write_sub_packet(sub.signatureCreationTime, util.writeDate(this.created))); - } - if (this.signatureExpirationTime !== null) { - arr.push(write_sub_packet(sub.signatureExpirationTime, util.writeNumber(this.signatureExpirationTime, 4))); - } - if (this.exportable !== null) { - arr.push(write_sub_packet(sub.exportableCertification, new Uint8Array([this.exportable ? 1 : 0]))); - } - if (this.trustLevel !== null) { - bytes = new Uint8Array([this.trustLevel, this.trustAmount]); - arr.push(write_sub_packet(sub.trustSignature, bytes)); - } - if (this.regularExpression !== null) { - arr.push(write_sub_packet(sub.regularExpression, this.regularExpression)); - } - if (this.revocable !== null) { - arr.push(write_sub_packet(sub.revocable, new Uint8Array([this.revocable ? 1 : 0]))); - } - if (this.keyExpirationTime !== null) { - arr.push(write_sub_packet(sub.keyExpirationTime, util.writeNumber(this.keyExpirationTime, 4))); - } - if (this.preferredSymmetricAlgorithms !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredSymmetricAlgorithms)); - arr.push(write_sub_packet(sub.preferredSymmetricAlgorithms, bytes)); - } - if (this.revocationKeyClass !== null) { - bytes = new Uint8Array([this.revocationKeyClass, this.revocationKeyAlgorithm]); - bytes = util.concat([bytes, this.revocationKeyFingerprint]); - arr.push(write_sub_packet(sub.revocationKey, bytes)); - } - this.rawNotations.forEach(([{ name, value, humanReadable }]) => { - bytes = [new Uint8Array([humanReadable ? 0x80 : 0, 0, 0, 0])]; - // 2 octets of name length - bytes.push(util.writeNumber(name.length, 2)); - // 2 octets of value length - bytes.push(util.writeNumber(value.length, 2)); - bytes.push(util.strToUint8Array(name)); - bytes.push(value); - bytes = util.concat(bytes); - arr.push(write_sub_packet(sub.notationData, bytes)); - }); - if (this.preferredHashAlgorithms !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredHashAlgorithms)); - arr.push(write_sub_packet(sub.preferredHashAlgorithms, bytes)); - } - if (this.preferredCompressionAlgorithms !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredCompressionAlgorithms)); - arr.push(write_sub_packet(sub.preferredCompressionAlgorithms, bytes)); - } - if (this.keyServerPreferences !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.keyServerPreferences)); - arr.push(write_sub_packet(sub.keyServerPreferences, bytes)); - } - if (this.preferredKeyServer !== null) { - arr.push(write_sub_packet(sub.preferredKeyServer, util.strToUint8Array(this.preferredKeyServer))); - } - if (this.isPrimaryUserID !== null) { - arr.push(write_sub_packet(sub.primaryUserId, new Uint8Array([this.isPrimaryUserID ? 1 : 0]))); - } - if (this.policyURI !== null) { - arr.push(write_sub_packet(sub.policyUri, util.strToUint8Array(this.policyURI))); - } - if (this.keyFlags !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.keyFlags)); - arr.push(write_sub_packet(sub.keyFlags, bytes)); - } - if (this.signersUserId !== null) { - arr.push(write_sub_packet(sub.signersUserId, util.strToUint8Array(this.signersUserId))); - } - if (this.reasonForRevocationFlag !== null) { - bytes = util.strToUint8Array(String.fromCharCode(this.reasonForRevocationFlag) + this.reasonForRevocationString); - arr.push(write_sub_packet(sub.reasonForRevocation, bytes)); - } - if (this.features !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.features)); - arr.push(write_sub_packet(sub.features, bytes)); - } - if (this.signatureTargetPublicKeyAlgorithm !== null) { - bytes = [new Uint8Array([this.signatureTargetPublicKeyAlgorithm, this.signatureTargetHashAlgorithm])]; - bytes.push(util.strToUint8Array(this.signatureTargetHash)); - bytes = util.concat(bytes); - arr.push(write_sub_packet(sub.signatureTarget, bytes)); + write() { + const arr = []; + arr.push(this.signatureData); + arr.push(this.write_unhashed_sub_packets()); + arr.push(this.signedHashValue); + arr.push(stream.clone(this.signature)); + return util.concat(arr); } - if (this.preferredAeadAlgorithms !== null) { - bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredAeadAlgorithms)); - arr.push(write_sub_packet(sub.preferredAeadAlgorithms, bytes)); + + /** + * Signs provided data. This needs to be done prior to serialization. + * @param {SecretKeyPacket} key private key used to sign the message. + * @param {Object} data Contains packets to be signed. + * @param {Boolean} detached (optional) whether to create a detached signature + * @param {Boolean} streaming (optional) whether to process data as a stream + * @returns {Promise} + * @async + */ + async sign(key, data, detached = false, streaming = false) { + const signatureType = enums.write(enums.signature, this.signatureType); + const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); + const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); + + if (key.version === 5) { + this.version = 5; + } + const arr = [new Uint8Array([this.version, signatureType, publicKeyAlgorithm, hashAlgorithm])]; + + this.issuerKeyVersion = key.version; + this.issuerFingerprint = key.getFingerprintBytes(); + this.issuerKeyId = key.getKeyId(); + + // Add hashed subpackets + arr.push(this.write_hashed_sub_packets()); + + this.signatureData = util.concat(arr); + + const toHash = this.toHash(signatureType, data, detached); + const hash = await this.hash(signatureType, data, toHash, detached); + + this.signedHashValue = stream.slice(stream.clone(hash), 0, 2); + const params = key.params; + const signed = async () => crypto.signature.sign( + publicKeyAlgorithm, hashAlgorithm, params, toHash, await stream.readToEnd(hash) + ); + if (streaming) { + this.signature = stream.fromAsync(signed); + } else { + this.signature = await signed(); + + // Store the fact that this signature is valid, e.g. for when we call `await + // getLatestValidSignature(this.revocationSignatures, key, data)` later. + // Note that this only holds up if the key and data passed to verify are the + // same as the ones passed to sign. + this.verified = true; + } + return true; } - const result = util.concat(arr); - const length = util.writeNumber(result.length, 2); + /** + * Creates Uint8Array of bytes of all subpacket data except Issuer and Embedded Signature subpackets + * @returns {Uint8Array} subpacket data + */ + write_hashed_sub_packets() { + const sub = enums.signatureSubpacket; + const arr = []; + let bytes; + if (this.created !== null) { + arr.push(write_sub_packet(sub.signatureCreationTime, util.writeDate(this.created))); + } + if (this.signatureExpirationTime !== null) { + arr.push(write_sub_packet(sub.signatureExpirationTime, util.writeNumber(this.signatureExpirationTime, 4))); + } + if (this.exportable !== null) { + arr.push(write_sub_packet(sub.exportableCertification, new Uint8Array([this.exportable ? 1 : 0]))); + } + if (this.trustLevel !== null) { + bytes = new Uint8Array([this.trustLevel, this.trustAmount]); + arr.push(write_sub_packet(sub.trustSignature, bytes)); + } + if (this.regularExpression !== null) { + arr.push(write_sub_packet(sub.regularExpression, this.regularExpression)); + } + if (this.revocable !== null) { + arr.push(write_sub_packet(sub.revocable, new Uint8Array([this.revocable ? 1 : 0]))); + } + if (this.keyExpirationTime !== null) { + arr.push(write_sub_packet(sub.keyExpirationTime, util.writeNumber(this.keyExpirationTime, 4))); + } + if (this.preferredSymmetricAlgorithms !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredSymmetricAlgorithms)); + arr.push(write_sub_packet(sub.preferredSymmetricAlgorithms, bytes)); + } + if (this.revocationKeyClass !== null) { + bytes = new Uint8Array([this.revocationKeyClass, this.revocationKeyAlgorithm]); + bytes = util.concat([bytes, this.revocationKeyFingerprint]); + arr.push(write_sub_packet(sub.revocationKey, bytes)); + } + this.rawNotations.forEach(([{ name, value, humanReadable }]) => { + bytes = [new Uint8Array([humanReadable ? 0x80 : 0, 0, 0, 0])]; + // 2 octets of name length + bytes.push(util.writeNumber(name.length, 2)); + // 2 octets of value length + bytes.push(util.writeNumber(value.length, 2)); + bytes.push(util.strToUint8Array(name)); + bytes.push(value); + bytes = util.concat(bytes); + arr.push(write_sub_packet(sub.notationData, bytes)); + }); + if (this.preferredHashAlgorithms !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredHashAlgorithms)); + arr.push(write_sub_packet(sub.preferredHashAlgorithms, bytes)); + } + if (this.preferredCompressionAlgorithms !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredCompressionAlgorithms)); + arr.push(write_sub_packet(sub.preferredCompressionAlgorithms, bytes)); + } + if (this.keyServerPreferences !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.keyServerPreferences)); + arr.push(write_sub_packet(sub.keyServerPreferences, bytes)); + } + if (this.preferredKeyServer !== null) { + arr.push(write_sub_packet(sub.preferredKeyServer, util.strToUint8Array(this.preferredKeyServer))); + } + if (this.isPrimaryUserID !== null) { + arr.push(write_sub_packet(sub.primaryUserId, new Uint8Array([this.isPrimaryUserID ? 1 : 0]))); + } + if (this.policyURI !== null) { + arr.push(write_sub_packet(sub.policyUri, util.strToUint8Array(this.policyURI))); + } + if (this.keyFlags !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.keyFlags)); + arr.push(write_sub_packet(sub.keyFlags, bytes)); + } + if (this.signersUserId !== null) { + arr.push(write_sub_packet(sub.signersUserId, util.strToUint8Array(this.signersUserId))); + } + if (this.reasonForRevocationFlag !== null) { + bytes = util.strToUint8Array(String.fromCharCode(this.reasonForRevocationFlag) + this.reasonForRevocationString); + arr.push(write_sub_packet(sub.reasonForRevocation, bytes)); + } + if (this.features !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.features)); + arr.push(write_sub_packet(sub.features, bytes)); + } + if (this.signatureTargetPublicKeyAlgorithm !== null) { + bytes = [new Uint8Array([this.signatureTargetPublicKeyAlgorithm, this.signatureTargetHashAlgorithm])]; + bytes.push(util.strToUint8Array(this.signatureTargetHash)); + bytes = util.concat(bytes); + arr.push(write_sub_packet(sub.signatureTarget, bytes)); + } + if (this.preferredAeadAlgorithms !== null) { + bytes = util.strToUint8Array(util.uint8ArrayToStr(this.preferredAeadAlgorithms)); + arr.push(write_sub_packet(sub.preferredAeadAlgorithms, bytes)); + } - return util.concat([length, result]); -}; + const result = util.concat(arr); + const length = util.writeNumber(result.length, 2); -/** - * Creates Uint8Array of bytes of Issuer and Embedded Signature subpackets - * @returns {Uint8Array} subpacket data - */ -SignaturePacket.prototype.write_unhashed_sub_packets = function() { - const sub = enums.signatureSubpacket; - const arr = []; - let bytes; - if (!this.issuerKeyId.isNull() && this.issuerKeyVersion !== 5) { - // If the version of [the] key is greater than 4, this subpacket - // MUST NOT be included in the signature. - arr.push(write_sub_packet(sub.issuer, this.issuerKeyId.write())); - } - if (this.embeddedSignature !== null) { - arr.push(write_sub_packet(sub.embeddedSignature, this.embeddedSignature.write())); - } - if (this.issuerFingerprint !== null) { - bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint]; - bytes = util.concat(bytes); - arr.push(write_sub_packet(sub.issuerFingerprint, bytes)); + return util.concat([length, result]); } - this.unhashedSubpackets.forEach(data => { - arr.push(packet.writeSimpleLength(data.length)); - arr.push(data); - }); - const result = util.concat(arr); - const length = util.writeNumber(result.length, 2); + /** + * Creates Uint8Array of bytes of Issuer and Embedded Signature subpackets + * @returns {Uint8Array} subpacket data + */ + write_unhashed_sub_packets() { + const sub = enums.signatureSubpacket; + const arr = []; + let bytes; + if (!this.issuerKeyId.isNull() && this.issuerKeyVersion !== 5) { + // If the version of [the] key is greater than 4, this subpacket + // MUST NOT be included in the signature. + arr.push(write_sub_packet(sub.issuer, this.issuerKeyId.write())); + } + if (this.embeddedSignature !== null) { + arr.push(write_sub_packet(sub.embeddedSignature, this.embeddedSignature.write())); + } + if (this.issuerFingerprint !== null) { + bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint]; + bytes = util.concat(bytes); + arr.push(write_sub_packet(sub.issuerFingerprint, bytes)); + } + this.unhashedSubpackets.forEach(data => { + arr.push(packet.writeSimpleLength(data.length)); + arr.push(data); + }); - return util.concat([length, result]); -}; + const result = util.concat(arr); + const length = util.writeNumber(result.length, 2); -/** - * Creates a string representation of a sub signature packet - * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.1|RFC4880 5.2.3.1} - * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.2|RFC4880 5.2.3.2} - * @param {Integer} type subpacket signature type. - * @param {String} data data to be included - * @returns {String} a string-representation of a sub signature packet - * @private - */ -function write_sub_packet(type, data) { - const arr = []; - arr.push(packet.writeSimpleLength(data.length + 1)); - arr.push(new Uint8Array([type])); - arr.push(data); - return util.concat(arr); -} + return util.concat([length, result]); + } -// V4 signature sub packets + // V4 signature sub packets -SignaturePacket.prototype.read_sub_packet = function (bytes, trusted = true) { - let mypos = 0; + read_sub_packet(bytes, trusted = true) { + let mypos = 0; - const read_array = (prop, bytes) => { - this[prop] = []; + const read_array = (prop, bytes) => { + this[prop] = []; - for (let i = 0; i < bytes.length; i++) { - this[prop].push(bytes[i]); + for (let i = 0; i < bytes.length; i++) { + this[prop].push(bytes[i]); + } + }; + + // The leftmost bit denotes a "critical" packet + const critical = bytes[mypos] & 0x80; + const type = bytes[mypos] & 0x7F; + + // GPG puts the Issuer and Signature subpackets in the unhashed area. + // Tampering with those invalidates the signature, so we can trust them. + // Ignore all other unhashed subpackets. + if (!trusted && ![ + enums.signatureSubpacket.issuer, + enums.signatureSubpacket.issuerFingerprint, + enums.signatureSubpacket.embeddedSignature + ].includes(type)) { + this.unhashedSubpackets.push(bytes.subarray(mypos, bytes.length)); + return; } - }; - // The leftmost bit denotes a "critical" packet - const critical = bytes[mypos] & 0x80; - const type = bytes[mypos] & 0x7F; + mypos++; - // GPG puts the Issuer and Signature subpackets in the unhashed area. - // Tampering with those invalidates the signature, so we can trust them. - // Ignore all other unhashed subpackets. - if (!trusted && ![ - enums.signatureSubpacket.issuer, - enums.signatureSubpacket.issuerFingerprint, - enums.signatureSubpacket.embeddedSignature - ].includes(type)) { - this.unhashedSubpackets.push(bytes.subarray(mypos, bytes.length)); - return; - } + // subpacket type + switch (type) { + case 2: + // Signature Creation Time + this.created = util.readDate(bytes.subarray(mypos, bytes.length)); + break; + case 3: { + // Signature Expiration Time in seconds + const seconds = util.readNumber(bytes.subarray(mypos, bytes.length)); - mypos++; - - // subpacket type - switch (type) { - case 2: - // Signature Creation Time - this.created = util.readDate(bytes.subarray(mypos, bytes.length)); - break; - case 3: { - // Signature Expiration Time in seconds - const seconds = util.readNumber(bytes.subarray(mypos, bytes.length)); - - this.signatureNeverExpires = seconds === 0; - this.signatureExpirationTime = seconds; - - break; - } - case 4: - // Exportable Certification - this.exportable = bytes[mypos++] === 1; - break; - case 5: - // Trust Signature - this.trustLevel = bytes[mypos++]; - this.trustAmount = bytes[mypos++]; - break; - case 6: - // Regular Expression - this.regularExpression = bytes[mypos]; - break; - case 7: - // Revocable - this.revocable = bytes[mypos++] === 1; - break; - case 9: { - // Key Expiration Time in seconds - const seconds = util.readNumber(bytes.subarray(mypos, bytes.length)); - - this.keyExpirationTime = seconds; - this.keyNeverExpires = seconds === 0; - - break; - } - case 11: - // Preferred Symmetric Algorithms - read_array('preferredSymmetricAlgorithms', bytes.subarray(mypos, bytes.length)); - break; - case 12: - // Revocation Key - // (1 octet of class, 1 octet of public-key algorithm ID, 20 - // octets of - // fingerprint) - this.revocationKeyClass = bytes[mypos++]; - this.revocationKeyAlgorithm = bytes[mypos++]; - this.revocationKeyFingerprint = bytes.subarray(mypos, mypos + 20); - break; - - case 16: - // Issuer - this.issuerKeyId.read(bytes.subarray(mypos, bytes.length)); - break; - - case 20: { - // Notation Data - const humanReadable = !!(bytes[mypos] & 0x80); - - // We extract key/value tuple from the byte stream. - mypos += 4; - const m = util.readNumber(bytes.subarray(mypos, mypos + 2)); - mypos += 2; - const n = util.readNumber(bytes.subarray(mypos, mypos + 2)); - mypos += 2; - - const name = util.uint8ArrayToStr(bytes.subarray(mypos, mypos + m)); - const value = bytes.subarray(mypos + m, mypos + m + n); - - this.rawNotations.push({ name, humanReadable, value }); - - if (humanReadable) { - this.notations[name] = util.uint8ArrayToStr(value); - } + this.signatureNeverExpires = seconds === 0; + this.signatureExpirationTime = seconds; - if (critical && (config.knownNotations.indexOf(name) === -1)) { - throw new Error("Unknown critical notation: " + name); + break; + } + case 4: + // Exportable Certification + this.exportable = bytes[mypos++] === 1; + break; + case 5: + // Trust Signature + this.trustLevel = bytes[mypos++]; + this.trustAmount = bytes[mypos++]; + break; + case 6: + // Regular Expression + this.regularExpression = bytes[mypos]; + break; + case 7: + // Revocable + this.revocable = bytes[mypos++] === 1; + break; + case 9: { + // Key Expiration Time in seconds + const seconds = util.readNumber(bytes.subarray(mypos, bytes.length)); + + this.keyExpirationTime = seconds; + this.keyNeverExpires = seconds === 0; + + break; } - break; - } - case 21: - // Preferred Hash Algorithms - read_array('preferredHashAlgorithms', bytes.subarray(mypos, bytes.length)); - break; - case 22: - // Preferred Compression Algorithms - read_array('preferredCompressionAlgorithms', bytes.subarray(mypos, bytes.length)); - break; - case 23: - // Key Server Preferences - read_array('keyServerPreferences', bytes.subarray(mypos, bytes.length)); - break; - case 24: - // Preferred Key Server - this.preferredKeyServer = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); - break; - case 25: - // Primary User ID - this.isPrimaryUserID = bytes[mypos++] !== 0; - break; - case 26: - // Policy URI - this.policyURI = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); - break; - case 27: - // Key Flags - read_array('keyFlags', bytes.subarray(mypos, bytes.length)); - break; - case 28: - // Signer's User ID - this.signersUserId = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); - break; - case 29: - // Reason for Revocation - this.reasonForRevocationFlag = bytes[mypos++]; - this.reasonForRevocationString = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); - break; - case 30: - // Features - read_array('features', bytes.subarray(mypos, bytes.length)); - break; - case 31: { - // Signature Target - // (1 octet public-key algorithm, 1 octet hash algorithm, N octets hash) - this.signatureTargetPublicKeyAlgorithm = bytes[mypos++]; - this.signatureTargetHashAlgorithm = bytes[mypos++]; - - const len = crypto.getHashByteLength(this.signatureTargetHashAlgorithm); - - this.signatureTargetHash = util.uint8ArrayToStr(bytes.subarray(mypos, mypos + len)); - break; - } - case 32: - // Embedded Signature - this.embeddedSignature = new SignaturePacket(); - this.embeddedSignature.read(bytes.subarray(mypos, bytes.length)); - break; - case 33: - // Issuer Fingerprint - this.issuerKeyVersion = bytes[mypos++]; - this.issuerFingerprint = bytes.subarray(mypos, bytes.length); - if (this.issuerKeyVersion === 5) { - this.issuerKeyId.read(this.issuerFingerprint); - } else { - this.issuerKeyId.read(this.issuerFingerprint.subarray(-8)); + case 11: + // Preferred Symmetric Algorithms + read_array('preferredSymmetricAlgorithms', bytes.subarray(mypos, bytes.length)); + break; + case 12: + // Revocation Key + // (1 octet of class, 1 octet of public-key algorithm ID, 20 + // octets of + // fingerprint) + this.revocationKeyClass = bytes[mypos++]; + this.revocationKeyAlgorithm = bytes[mypos++]; + this.revocationKeyFingerprint = bytes.subarray(mypos, mypos + 20); + break; + + case 16: + // Issuer + this.issuerKeyId.read(bytes.subarray(mypos, bytes.length)); + break; + + case 20: { + // Notation Data + const humanReadable = !!(bytes[mypos] & 0x80); + + // We extract key/value tuple from the byte stream. + mypos += 4; + const m = util.readNumber(bytes.subarray(mypos, mypos + 2)); + mypos += 2; + const n = util.readNumber(bytes.subarray(mypos, mypos + 2)); + mypos += 2; + + const name = util.uint8ArrayToStr(bytes.subarray(mypos, mypos + m)); + const value = bytes.subarray(mypos + m, mypos + m + n); + + this.rawNotations.push({ name, humanReadable, value }); + + if (humanReadable) { + this.notations[name] = util.uint8ArrayToStr(value); + } + + if (critical && (config.knownNotations.indexOf(name) === -1)) { + throw new Error("Unknown critical notation: " + name); + } + break; } - break; - case 34: - // Preferred AEAD Algorithms - read_array.call(this, 'preferredAeadAlgorithms', bytes.subarray(mypos, bytes.length)); - break; - default: { - const err = new Error("Unknown signature subpacket type " + type + " @:" + mypos); - if (critical) { - throw err; - } else { - util.printDebug(err); + case 21: + // Preferred Hash Algorithms + read_array('preferredHashAlgorithms', bytes.subarray(mypos, bytes.length)); + break; + case 22: + // Preferred Compression Algorithms + read_array('preferredCompressionAlgorithms', bytes.subarray(mypos, bytes.length)); + break; + case 23: + // Key Server Preferences + read_array('keyServerPreferences', bytes.subarray(mypos, bytes.length)); + break; + case 24: + // Preferred Key Server + this.preferredKeyServer = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); + break; + case 25: + // Primary User ID + this.isPrimaryUserID = bytes[mypos++] !== 0; + break; + case 26: + // Policy URI + this.policyURI = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); + break; + case 27: + // Key Flags + read_array('keyFlags', bytes.subarray(mypos, bytes.length)); + break; + case 28: + // Signer's User ID + this.signersUserId = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); + break; + case 29: + // Reason for Revocation + this.reasonForRevocationFlag = bytes[mypos++]; + this.reasonForRevocationString = util.uint8ArrayToStr(bytes.subarray(mypos, bytes.length)); + break; + case 30: + // Features + read_array('features', bytes.subarray(mypos, bytes.length)); + break; + case 31: { + // Signature Target + // (1 octet public-key algorithm, 1 octet hash algorithm, N octets hash) + this.signatureTargetPublicKeyAlgorithm = bytes[mypos++]; + this.signatureTargetHashAlgorithm = bytes[mypos++]; + + const len = crypto.getHashByteLength(this.signatureTargetHashAlgorithm); + + this.signatureTargetHash = util.uint8ArrayToStr(bytes.subarray(mypos, mypos + len)); + break; + } + case 32: + // Embedded Signature + this.embeddedSignature = new SignaturePacket(); + this.embeddedSignature.read(bytes.subarray(mypos, bytes.length)); + break; + case 33: + // Issuer Fingerprint + this.issuerKeyVersion = bytes[mypos++]; + this.issuerFingerprint = bytes.subarray(mypos, bytes.length); + if (this.issuerKeyVersion === 5) { + this.issuerKeyId.read(this.issuerFingerprint); + } else { + this.issuerKeyId.read(this.issuerFingerprint.subarray(-8)); + } + break; + case 34: + // Preferred AEAD Algorithms + read_array.call(this, 'preferredAeadAlgorithms', bytes.subarray(mypos, bytes.length)); + break; + default: { + const err = new Error("Unknown signature subpacket type " + type + " @:" + mypos); + if (critical) { + throw err; + } else { + util.printDebug(err); + } } } } -}; -SignaturePacket.prototype.read_sub_packets = function(bytes, trusted = true) { - // Two-octet scalar octet count for following subpacket data. - const subpacket_length = util.readNumber(bytes.subarray(0, 2)); + read_sub_packets(bytes, trusted = true) { + // Two-octet scalar octet count for following subpacket data. + const subpacket_length = util.readNumber(bytes.subarray(0, 2)); + + let i = 2; - let i = 2; + // subpacket data set (zero or more subpackets) + while (i < 2 + subpacket_length) { + const len = packet.readSimpleLength(bytes.subarray(i, bytes.length)); + i += len.offset; - // subpacket data set (zero or more subpackets) - while (i < 2 + subpacket_length) { - const len = packet.readSimpleLength(bytes.subarray(i, bytes.length)); - i += len.offset; + this.read_sub_packet(bytes.subarray(i, i + len.len), trusted); - this.read_sub_packet(bytes.subarray(i, i + len.len), trusted); + i += len.len; + } - i += len.len; + return i; } - return i; -}; + // Produces data to produce signature on + toSign(type, data) { + const t = enums.signature; -// Produces data to produce signature on -SignaturePacket.prototype.toSign = function (type, data) { - const t = enums.signature; + switch (type) { + case t.binary: + if (data.text !== null) { + return util.encodeUtf8(data.getText(true)); + } + return data.getBytes(true); - switch (type) { - case t.binary: - if (data.text !== null) { - return util.encodeUtf8(data.getText(true)); + case t.text: { + const bytes = data.getBytes(true); + // normalize EOL to \r\n + return util.canonicalizeEOL(bytes); } - return data.getBytes(true); - - case t.text: { - const bytes = data.getBytes(true); - // normalize EOL to \r\n - return util.canonicalizeEOL(bytes); - } - case t.standalone: - return new Uint8Array(0); - - case t.certGeneric: - case t.certPersona: - case t.certCasual: - case t.certPositive: - case t.certRevocation: { - let packet; - let tag; - - if (data.userId) { - tag = 0xB4; - packet = data.userId; - } else if (data.userAttribute) { - tag = 0xD1; - packet = data.userAttribute; - } else { - throw new Error('Either a userId or userAttribute packet needs to be ' + - 'supplied for certification.'); + case t.standalone: + return new Uint8Array(0); + + case t.certGeneric: + case t.certPersona: + case t.certCasual: + case t.certPositive: + case t.certRevocation: { + let packet; + let tag; + + if (data.userId) { + tag = 0xB4; + packet = data.userId; + } else if (data.userAttribute) { + tag = 0xD1; + packet = data.userAttribute; + } else { + throw new Error('Either a userId or userAttribute packet needs to be ' + + 'supplied for certification.'); + } + + const bytes = packet.write(); + + return util.concat([this.toSign(t.key, data), + new Uint8Array([tag]), + util.writeNumber(bytes.length, 4), + bytes]); } - - const bytes = packet.write(); - - return util.concat([this.toSign(t.key, data), - new Uint8Array([tag]), - util.writeNumber(bytes.length, 4), - bytes]); + case t.subkeyBinding: + case t.subkeyRevocation: + case t.keyBinding: + return util.concat([this.toSign(t.key, data), this.toSign(t.key, { + key: data.bind + })]); + + case t.key: + if (data.key === undefined) { + throw new Error('Key packet is required for this signature.'); + } + return data.key.writeForHash(this.version); + + case t.keyRevocation: + return this.toSign(t.key, data); + case t.timestamp: + return new Uint8Array(0); + case t.thirdParty: + throw new Error('Not implemented'); + default: + throw new Error('Unknown signature type.'); } - case t.subkeyBinding: - case t.subkeyRevocation: - case t.keyBinding: - return util.concat([this.toSign(t.key, data), this.toSign(t.key, { - key: data.bind - })]); - - case t.key: - if (data.key === undefined) { - throw new Error('Key packet is required for this signature.'); - } - return data.key.writeForHash(this.version); - - case t.keyRevocation: - return this.toSign(t.key, data); - case t.timestamp: - return new Uint8Array(0); - case t.thirdParty: - throw new Error('Not implemented'); - default: - throw new Error('Unknown signature type.'); } -}; - -SignaturePacket.prototype.calculateTrailer = function (data, detached) { - let length = 0; - return stream.transform(stream.clone(this.signatureData), value => { - length += value.length; - }, () => { - const arr = []; - if (this.version === 5 && (this.signatureType === enums.signature.binary || this.signatureType === enums.signature.text)) { - if (detached) { - arr.push(new Uint8Array(6)); - } else { - arr.push(data.writeHeader()); + calculateTrailer(data, detached) { + let length = 0; + return stream.transform(stream.clone(this.signatureData), value => { + length += value.length; + }, () => { + const arr = []; + if (this.version === 5 && (this.signatureType === enums.signature.binary || this.signatureType === enums.signature.text)) { + if (detached) { + arr.push(new Uint8Array(6)); + } else { + arr.push(data.writeHeader()); + } } - } - arr.push(new Uint8Array([this.version, 0xFF])); - if (this.version === 5) { - arr.push(new Uint8Array(4)); - } - arr.push(util.writeNumber(length, 4)); - // For v5, this should really be writeNumber(length, 8) rather than the - // hardcoded 4 zero bytes above - return util.concat(arr); - }); -}; - - -SignaturePacket.prototype.toHash = function(signatureType, data, detached = false) { - const bytes = this.toSign(signatureType, data); - - return util.concat([bytes, this.signatureData, this.calculateTrailer(data, detached)]); -}; - -SignaturePacket.prototype.hash = async function(signatureType, data, toHash, detached = false, streaming = true) { - const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); - if (!toHash) toHash = this.toHash(signatureType, data, detached); - if (!streaming && util.isStream(toHash)) { - return stream.fromAsync(async () => this.hash(signatureType, data, await stream.readToEnd(toHash), detached)); + arr.push(new Uint8Array([this.version, 0xFF])); + if (this.version === 5) { + arr.push(new Uint8Array(4)); + } + arr.push(util.writeNumber(length, 4)); + // For v5, this should really be writeNumber(length, 8) rather than the + // hardcoded 4 zero bytes above + return util.concat(arr); + }); } - return crypto.hash.digest(hashAlgorithm, toHash); -}; + toHash(signatureType, data, detached = false) { + const bytes = this.toSign(signatureType, data); -/** - * verifies the signature packet. Note: not all signature types are implemented - * @param {PublicSubkeyPacket|PublicKeyPacket| - * SecretSubkeyPacket|SecretKeyPacket} key the public key to verify the signature - * @param {module:enums.signature} signatureType expected signature type - * @param {String|Object} data data which on the signature applies - * @param {Boolean} detached (optional) whether to verify a detached signature - * @returns {Promise} True if message is verified, else false. - * @async - */ -SignaturePacket.prototype.verify = async function (key, signatureType, data, detached = false, streaming = false) { - const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); - const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); - - if (publicKeyAlgorithm !== enums.write(enums.publicKey, key.algorithm)) { - throw new Error('Public key algorithm used to sign signature does not match issuer key algorithm.'); + return util.concat([bytes, this.signatureData, this.calculateTrailer(data, detached)]); } - let toHash; - let hash; - if (this.hashed) { - hash = await this.hashed; - } else { - toHash = this.toHash(signatureType, data, detached); - if (!streaming) toHash = await stream.readToEnd(toHash); - hash = await this.hash(signatureType, data, toHash); - } - hash = await stream.readToEnd(hash); - if (this.signedHashValue[0] !== hash[0] || - this.signedHashValue[1] !== hash[1]) { - throw new Error('Message digest did not match'); - } + async hash(signatureType, data, toHash, detached = false, streaming = true) { + const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); + if (!toHash) toHash = this.toHash(signatureType, data, detached); + if (!streaming && util.isStream(toHash)) { + return stream.fromAsync(async () => this.hash(signatureType, data, await stream.readToEnd(toHash), detached)); + } + return crypto.hash.digest(hashAlgorithm, toHash); + } + + /** + * verifies the signature packet. Note: not all signature types are implemented + * @param {PublicSubkeyPacket|PublicKeyPacket| + * SecretSubkeyPacket|SecretKeyPacket} key the public key to verify the signature + * @param {module:enums.signature} signatureType expected signature type + * @param {String|Object} data data which on the signature applies + * @param {Boolean} detached (optional) whether to verify a detached signature + * @returns {Promise} True if message is verified, else false. + * @async + */ + async verify(key, signatureType, data, detached = false, streaming = false) { + const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); + const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); + + if (publicKeyAlgorithm !== enums.write(enums.publicKey, key.algorithm)) { + throw new Error('Public key algorithm used to sign signature does not match issuer key algorithm.'); + } - let mpicount = 0; - // Algorithm-Specific Fields for RSA signatures: - // - multiprecision number (MPI) of RSA signature value m**d mod n. - if (publicKeyAlgorithm > 0 && publicKeyAlgorithm < 4) { - mpicount = 1; - - // Algorithm-Specific Fields for DSA, ECDSA, and EdDSA signatures: - // - MPI of DSA value r. - // - MPI of DSA value s. - } else if (publicKeyAlgorithm === enums.publicKey.dsa || - publicKeyAlgorithm === enums.publicKey.ecdsa || - publicKeyAlgorithm === enums.publicKey.eddsa) { - mpicount = 2; - } + let toHash; + let hash; + if (this.hashed) { + hash = await this.hashed; + } else { + toHash = this.toHash(signatureType, data, detached); + if (!streaming) toHash = await stream.readToEnd(toHash); + hash = await this.hash(signatureType, data, toHash); + } + hash = await stream.readToEnd(hash); + if (this.signedHashValue[0] !== hash[0] || + this.signedHashValue[1] !== hash[1]) { + throw new Error('Message digest did not match'); + } - // EdDSA signature parameters are encoded in little-endian format - // https://tools.ietf.org/html/rfc8032#section-5.1.2 - const endian = publicKeyAlgorithm === enums.publicKey.eddsa ? 'le' : 'be'; - const mpi = []; - let i = 0; - this.signature = await stream.readToEnd(this.signature); - for (let j = 0; j < mpicount; j++) { - mpi[j] = new type_mpi(); - i += mpi[j].read(this.signature.subarray(i, this.signature.length), endian); - } - const verified = await crypto.signature.verify( - publicKeyAlgorithm, hashAlgorithm, mpi, key.params, - toHash, hash - ); - if (!verified) { - throw new Error('Signature verification failed'); - } - if (config.rejectHashAlgorithms.has(hashAlgorithm)) { - throw new Error('Insecure hash algorithm: ' + enums.read(enums.hash, hashAlgorithm).toUpperCase()); - } - if (config.rejectMessageHashAlgorithms.has(hashAlgorithm) && - [enums.signature.binary, enums.signature.text].includes(this.signatureType)) { - throw new Error('Insecure message hash algorithm: ' + enums.read(enums.hash, hashAlgorithm).toUpperCase()); - } - if (this.revocationKeyClass !== null) { - throw new Error('This key is intended to be revoked with an authorized key, which OpenPGP.js does not support.'); + let mpicount = 0; + // Algorithm-Specific Fields for RSA signatures: + // - multiprecision number (MPI) of RSA signature value m**d mod n. + if (publicKeyAlgorithm > 0 && publicKeyAlgorithm < 4) { + mpicount = 1; + + // Algorithm-Specific Fields for DSA, ECDSA, and EdDSA signatures: + // - MPI of DSA value r. + // - MPI of DSA value s. + } else if (publicKeyAlgorithm === enums.publicKey.dsa || + publicKeyAlgorithm === enums.publicKey.ecdsa || + publicKeyAlgorithm === enums.publicKey.eddsa) { + mpicount = 2; + } + + // EdDSA signature parameters are encoded in little-endian format + // https://tools.ietf.org/html/rfc8032#section-5.1.2 + const endian = publicKeyAlgorithm === enums.publicKey.eddsa ? 'le' : 'be'; + const mpi = []; + let i = 0; + this.signature = await stream.readToEnd(this.signature); + for (let j = 0; j < mpicount; j++) { + mpi[j] = new type_mpi(); + i += mpi[j].read(this.signature.subarray(i, this.signature.length), endian); + } + const verified = await crypto.signature.verify( + publicKeyAlgorithm, hashAlgorithm, mpi, key.params, + toHash, hash + ); + if (!verified) { + throw new Error('Signature verification failed'); + } + if (config.rejectHashAlgorithms.has(hashAlgorithm)) { + throw new Error('Insecure hash algorithm: ' + enums.read(enums.hash, hashAlgorithm).toUpperCase()); + } + if (config.rejectMessageHashAlgorithms.has(hashAlgorithm) && + [enums.signature.binary, enums.signature.text].includes(this.signatureType)) { + throw new Error('Insecure message hash algorithm: ' + enums.read(enums.hash, hashAlgorithm).toUpperCase()); + } + if (this.revocationKeyClass !== null) { + throw new Error('This key is intended to be revoked with an authorized key, which OpenPGP.js does not support.'); + } + this.verified = true; + return true; + } + + /** + * Verifies signature expiration date + * @param {Date} date (optional) use the given date for verification instead of the current time + * @returns {Boolean} true if expired + */ + isExpired(date = new Date()) { + const normDate = util.normalizeDate(date); + if (normDate !== null) { + const expirationTime = this.getExpirationTime(); + return !(this.created <= normDate && normDate <= expirationTime); + } + return false; } - this.verified = true; - return true; -}; -/** - * Verifies signature expiration date - * @param {Date} date (optional) use the given date for verification instead of the current time - * @returns {Boolean} true if expired - */ -SignaturePacket.prototype.isExpired = function (date = new Date()) { - const normDate = util.normalizeDate(date); - if (normDate !== null) { - const expirationTime = this.getExpirationTime(); - return !(this.created <= normDate && normDate <= expirationTime); + /** + * Returns the expiration time of the signature or Infinity if signature does not expire + * @returns {Date} expiration time + */ + getExpirationTime() { + return !this.signatureNeverExpires ? new Date(this.created.getTime() + this.signatureExpirationTime * 1000) : Infinity; } - return false; -}; +} /** - * Returns the expiration time of the signature or Infinity if signature does not expire - * @returns {Date} expiration time + * Creates a string representation of a sub signature packet + * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.1|RFC4880 5.2.3.1} + * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.3.2|RFC4880 5.2.3.2} + * @param {Integer} type subpacket signature type. + * @param {String} data data to be included + * @returns {String} a string-representation of a sub signature packet + * @private */ -SignaturePacket.prototype.getExpirationTime = function () { - return !this.signatureNeverExpires ? new Date(this.created.getTime() + this.signatureExpirationTime * 1000) : Infinity; -}; +function write_sub_packet(type, data) { + const arr = []; + arr.push(packet.writeSimpleLength(data.length + 1)); + arr.push(new Uint8Array([type])); + arr.push(data); + return util.concat(arr); +} export default SignaturePacket; diff --git a/src/packet/sym_encrypted_aead_protected_data.js b/src/packet/sym_encrypted_aead_protected_data.js index 071f942e..8fff667e 100644 --- a/src/packet/sym_encrypted_aead_protected_data.js +++ b/src/packet/sym_encrypted_aead_protected_data.js @@ -45,157 +45,158 @@ const VERSION = 1; // A one-octet version number of the data packet. * {@link https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1}: * AEAD Protected Data Packet * @memberof module:packet - * @constructor */ -function SymEncryptedAEADProtectedDataPacket() { - this.tag = enums.packet.symEncryptedAEADProtectedData; - this.version = VERSION; - this.cipherAlgo = null; - this.aeadAlgorithm = 'eax'; - this.aeadAlgo = null; - this.chunkSizeByte = null; - this.iv = null; - this.encrypted = null; - this.packets = null; -} - -export default SymEncryptedAEADProtectedDataPacket; +class SymEncryptedAEADProtectedDataPacket { + constructor() { + this.tag = enums.packet.symEncryptedAEADProtectedData; + this.version = VERSION; + this.cipherAlgo = null; + this.aeadAlgorithm = 'eax'; + this.aeadAlgo = null; + this.chunkSizeByte = null; + this.iv = null; + this.encrypted = null; + this.packets = null; + } -/** - * Parse an encrypted payload of bytes in the order: version, IV, ciphertext (see specification) - * @param {Uint8Array | ReadableStream} bytes - */ -SymEncryptedAEADProtectedDataPacket.prototype.read = async function (bytes) { - await stream.parse(bytes, async reader => { - if (await reader.readByte() !== VERSION) { // The only currently defined value is 1. - throw new Error('Invalid packet version.'); - } - this.cipherAlgo = await reader.readByte(); - this.aeadAlgo = await reader.readByte(); - this.chunkSizeByte = await reader.readByte(); - const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; - this.iv = await reader.readBytes(mode.ivLength); - this.encrypted = reader.remainder(); - }); -}; + /** + * Parse an encrypted payload of bytes in the order: version, IV, ciphertext (see specification) + * @param {Uint8Array | ReadableStream} bytes + */ + async read(bytes) { + await stream.parse(bytes, async reader => { + if (await reader.readByte() !== VERSION) { // The only currently defined value is 1. + throw new Error('Invalid packet version.'); + } + this.cipherAlgo = await reader.readByte(); + this.aeadAlgo = await reader.readByte(); + this.chunkSizeByte = await reader.readByte(); + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + this.iv = await reader.readBytes(mode.ivLength); + this.encrypted = reader.remainder(); + }); + } -/** - * Write the encrypted payload of bytes in the order: version, IV, ciphertext (see specification) - * @returns {Uint8Array | ReadableStream} The encrypted payload - */ -SymEncryptedAEADProtectedDataPacket.prototype.write = function () { - return util.concat([new Uint8Array([this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte]), this.iv, this.encrypted]); -}; + /** + * Write the encrypted payload of bytes in the order: version, IV, ciphertext (see specification) + * @returns {Uint8Array | ReadableStream} The encrypted payload + */ + write() { + return util.concat([new Uint8Array([this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte]), this.iv, this.encrypted]); + } -/** - * Decrypt the encrypted payload. - * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' - * @param {Uint8Array} key The session key used to encrypt the payload - * @param {Boolean} streaming Whether the top-level function will return a stream - * @returns {Boolean} - * @async - */ -SymEncryptedAEADProtectedDataPacket.prototype.decrypt = async function (sessionKeyAlgorithm, key, streaming) { - await this.packets.read(await this.crypt('decrypt', key, stream.clone(this.encrypted), streaming), { - LiteralDataPacket, - CompressedDataPacket, - OnePassSignaturePacket, - SignaturePacket - }, streaming); - return true; -}; + /** + * Decrypt the encrypted payload. + * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' + * @param {Uint8Array} key The session key used to encrypt the payload + * @param {Boolean} streaming Whether the top-level function will return a stream + * @returns {Boolean} + * @async + */ + async decrypt(sessionKeyAlgorithm, key, streaming) { + await this.packets.read(await this.crypt('decrypt', key, stream.clone(this.encrypted), streaming), { + LiteralDataPacket, + CompressedDataPacket, + OnePassSignaturePacket, + SignaturePacket + }, streaming); + return true; + } -/** - * Encrypt the packet list payload. - * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' - * @param {Uint8Array} key The session key used to encrypt the payload - * @param {Boolean} streaming Whether the top-level function will return a stream - * @async - */ -SymEncryptedAEADProtectedDataPacket.prototype.encrypt = async function (sessionKeyAlgorithm, key, streaming) { - this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); - this.aeadAlgo = enums.write(enums.aead, this.aeadAlgorithm); - const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; - this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV - this.chunkSizeByte = config.aeadChunkSizeByte; - const data = this.packets.write(); - this.encrypted = await this.crypt('encrypt', key, data, streaming); -}; + /** + * Encrypt the packet list payload. + * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' + * @param {Uint8Array} key The session key used to encrypt the payload + * @param {Boolean} streaming Whether the top-level function will return a stream + * @async + */ + async encrypt(sessionKeyAlgorithm, key, streaming) { + this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); + this.aeadAlgo = enums.write(enums.aead, this.aeadAlgorithm); + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV + this.chunkSizeByte = config.aeadChunkSizeByte; + const data = this.packets.write(); + this.encrypted = await this.crypt('encrypt', key, data, streaming); + } -/** - * En/decrypt the payload. - * @param {encrypt|decrypt} fn Whether to encrypt or decrypt - * @param {Uint8Array} key The session key used to en/decrypt the payload - * @param {Uint8Array | ReadableStream} data The data to en/decrypt - * @param {Boolean} streaming Whether the top-level function will return a stream - * @returns {Uint8Array | ReadableStream} - * @async - */ -SymEncryptedAEADProtectedDataPacket.prototype.crypt = async function (fn, key, data, streaming) { - const cipher = enums.read(enums.symmetric, this.cipherAlgo); - const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; - const modeInstance = await mode(cipher, key); - const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0; - const tagLengthIfEncrypting = fn === 'encrypt' ? mode.tagLength : 0; - const chunkSize = 2 ** (this.chunkSizeByte + 6) + tagLengthIfDecrypting; // ((uint64_t)1 << (c + 6)) - const adataBuffer = new ArrayBuffer(21); - const adataArray = new Uint8Array(adataBuffer, 0, 13); - const adataTagArray = new Uint8Array(adataBuffer); - const adataView = new DataView(adataBuffer); - const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8); - adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0); - let chunkIndex = 0; - let latestPromise = Promise.resolve(); - let cryptedBytes = 0; - let queuedBytes = 0; - const iv = this.iv; - return stream.transformPair(data, async (readable, writable) => { - const reader = stream.getReader(readable); - const buffer = new stream.TransformStream({}, { - highWaterMark: streaming ? util.getHardwareConcurrency() * 2 ** (this.chunkSizeByte + 6) : Infinity, - size: array => array.length - }); - stream.pipe(buffer.readable, writable); - const writer = stream.getWriter(buffer.writable); - try { - while (true) { - let chunk = await reader.readBytes(chunkSize + tagLengthIfDecrypting) || new Uint8Array(); - const finalChunk = chunk.subarray(chunk.length - tagLengthIfDecrypting); - chunk = chunk.subarray(0, chunk.length - tagLengthIfDecrypting); - let cryptedPromise; - let done; - if (!chunkIndex || chunk.length) { - reader.unshift(finalChunk); - cryptedPromise = modeInstance[fn](chunk, mode.getNonce(iv, chunkIndexArray), adataArray); - queuedBytes += chunk.length - tagLengthIfDecrypting + tagLengthIfEncrypting; - } else { - // After the last chunk, we either encrypt a final, empty - // data chunk to get the final authentication tag or - // validate that final authentication tag. - adataView.setInt32(13 + 4, cryptedBytes); // Should be setInt64(13, ...) - cryptedPromise = modeInstance[fn](finalChunk, mode.getNonce(iv, chunkIndexArray), adataTagArray); - queuedBytes += tagLengthIfEncrypting; - done = true; - } - cryptedBytes += chunk.length - tagLengthIfDecrypting; - // eslint-disable-next-line no-loop-func - latestPromise = latestPromise.then(() => cryptedPromise).then(async crypted => { - await writer.ready; - await writer.write(crypted); - queuedBytes -= crypted.length; - }).catch(err => writer.abort(err)); - if (done || queuedBytes > writer.desiredSize) { - await latestPromise; // Respect backpressure - } - if (!done) { - adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) - } else { - await writer.close(); - break; + /** + * En/decrypt the payload. + * @param {encrypt|decrypt} fn Whether to encrypt or decrypt + * @param {Uint8Array} key The session key used to en/decrypt the payload + * @param {Uint8Array | ReadableStream} data The data to en/decrypt + * @param {Boolean} streaming Whether the top-level function will return a stream + * @returns {Uint8Array | ReadableStream} + * @async + */ + async crypt(fn, key, data, streaming) { + const cipher = enums.read(enums.symmetric, this.cipherAlgo); + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + const modeInstance = await mode(cipher, key); + const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0; + const tagLengthIfEncrypting = fn === 'encrypt' ? mode.tagLength : 0; + const chunkSize = 2 ** (this.chunkSizeByte + 6) + tagLengthIfDecrypting; // ((uint64_t)1 << (c + 6)) + const adataBuffer = new ArrayBuffer(21); + const adataArray = new Uint8Array(adataBuffer, 0, 13); + const adataTagArray = new Uint8Array(adataBuffer); + const adataView = new DataView(adataBuffer); + const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8); + adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0); + let chunkIndex = 0; + let latestPromise = Promise.resolve(); + let cryptedBytes = 0; + let queuedBytes = 0; + const iv = this.iv; + return stream.transformPair(data, async (readable, writable) => { + const reader = stream.getReader(readable); + const buffer = new stream.TransformStream({}, { + highWaterMark: streaming ? util.getHardwareConcurrency() * 2 ** (this.chunkSizeByte + 6) : Infinity, + size: array => array.length + }); + stream.pipe(buffer.readable, writable); + const writer = stream.getWriter(buffer.writable); + try { + while (true) { + let chunk = await reader.readBytes(chunkSize + tagLengthIfDecrypting) || new Uint8Array(); + const finalChunk = chunk.subarray(chunk.length - tagLengthIfDecrypting); + chunk = chunk.subarray(0, chunk.length - tagLengthIfDecrypting); + let cryptedPromise; + let done; + if (!chunkIndex || chunk.length) { + reader.unshift(finalChunk); + cryptedPromise = modeInstance[fn](chunk, mode.getNonce(iv, chunkIndexArray), adataArray); + queuedBytes += chunk.length - tagLengthIfDecrypting + tagLengthIfEncrypting; + } else { + // After the last chunk, we either encrypt a final, empty + // data chunk to get the final authentication tag or + // validate that final authentication tag. + adataView.setInt32(13 + 4, cryptedBytes); // Should be setInt64(13, ...) + cryptedPromise = modeInstance[fn](finalChunk, mode.getNonce(iv, chunkIndexArray), adataTagArray); + queuedBytes += tagLengthIfEncrypting; + done = true; + } + cryptedBytes += chunk.length - tagLengthIfDecrypting; + // eslint-disable-next-line no-loop-func + latestPromise = latestPromise.then(() => cryptedPromise).then(async crypted => { + await writer.ready; + await writer.write(crypted); + queuedBytes -= crypted.length; + }).catch(err => writer.abort(err)); + if (done || queuedBytes > writer.desiredSize) { + await latestPromise; // Respect backpressure + } + if (!done) { + adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) + } else { + await writer.close(); + break; + } } + } catch (e) { + await writer.abort(e); } - } catch (e) { - await writer.abort(e); - } - }); -}; + }); + } +} + +export default SymEncryptedAEADProtectedDataPacket; diff --git a/src/packet/sym_encrypted_integrity_protected_data.js b/src/packet/sym_encrypted_integrity_protected_data.js index 95039339..e31e9f94 100644 --- a/src/packet/sym_encrypted_integrity_protected_data.js +++ b/src/packet/sym_encrypted_integrity_protected_data.js @@ -49,103 +49,104 @@ const VERSION = 1; // A one-octet version number of the data packet. * encrypted data. It is used in combination with a Modification Detection Code * packet. * @memberof module:packet - * @constructor */ -function SymEncryptedIntegrityProtectedDataPacket() { - this.tag = enums.packet.symEncryptedIntegrityProtectedData; - this.version = VERSION; - /** The encrypted payload. */ - this.encrypted = null; // string - /** - * If after decrypting the packet this is set to true, - * a modification has been detected and thus the contents - * should be discarded. - * @type {Boolean} - */ - this.modification = false; - this.packets = null; -} +class SymEncryptedIntegrityProtectedDataPacket { + constructor() { + this.tag = enums.packet.symEncryptedIntegrityProtectedData; + this.version = VERSION; + /** The encrypted payload. */ + this.encrypted = null; // string + /** + * If after decrypting the packet this is set to true, + * a modification has been detected and thus the contents + * should be discarded. + * @type {Boolean} + */ + this.modification = false; + this.packets = null; + } -SymEncryptedIntegrityProtectedDataPacket.prototype.read = async function (bytes) { - await stream.parse(bytes, async reader => { + async read(bytes) { + await stream.parse(bytes, async reader => { - // - A one-octet version number. The only currently defined value is 1. - if (await reader.readByte() !== VERSION) { - throw new Error('Invalid packet version.'); - } + // - A one-octet version number. The only currently defined value is 1. + if (await reader.readByte() !== VERSION) { + throw new Error('Invalid packet version.'); + } - // - Encrypted data, the output of the selected symmetric-key cipher - // operating in Cipher Feedback mode with shift amount equal to the - // block size of the cipher (CFB-n where n is the block size). - this.encrypted = reader.remainder(); - }); -}; + // - Encrypted data, the output of the selected symmetric-key cipher + // operating in Cipher Feedback mode with shift amount equal to the + // block size of the cipher (CFB-n where n is the block size). + this.encrypted = reader.remainder(); + }); + } -SymEncryptedIntegrityProtectedDataPacket.prototype.write = function () { - return util.concat([new Uint8Array([VERSION]), this.encrypted]); -}; + write() { + return util.concat([new Uint8Array([VERSION]), this.encrypted]); + } -/** - * Encrypt the payload in the packet. - * @param {String} sessionKeyAlgorithm The selected symmetric encryption algorithm to be used e.g. 'aes128' - * @param {Uint8Array} key The key of cipher blocksize length to be used - * @param {Boolean} streaming Whether to set this.encrypted to a stream - * @returns {Promise} - * @async - */ -SymEncryptedIntegrityProtectedDataPacket.prototype.encrypt = async function (sessionKeyAlgorithm, key, streaming) { - let bytes = this.packets.write(); - if (!streaming) bytes = await stream.readToEnd(bytes); - const prefix = await crypto.getPrefixRandom(sessionKeyAlgorithm); - const mdc = new Uint8Array([0xD3, 0x14]); // modification detection code packet + /** + * Encrypt the payload in the packet. + * @param {String} sessionKeyAlgorithm The selected symmetric encryption algorithm to be used e.g. 'aes128' + * @param {Uint8Array} key The key of cipher blocksize length to be used + * @param {Boolean} streaming Whether to set this.encrypted to a stream + * @returns {Promise} + * @async + */ + async encrypt(sessionKeyAlgorithm, key, streaming) { + let bytes = this.packets.write(); + if (!streaming) bytes = await stream.readToEnd(bytes); + const prefix = await crypto.getPrefixRandom(sessionKeyAlgorithm); + const mdc = new Uint8Array([0xD3, 0x14]); // modification detection code packet - const tohash = util.concat([prefix, bytes, mdc]); - const hash = await crypto.hash.sha1(stream.passiveClone(tohash)); - const plaintext = util.concat([tohash, hash]); + const tohash = util.concat([prefix, bytes, mdc]); + const hash = await crypto.hash.sha1(stream.passiveClone(tohash)); + const plaintext = util.concat([tohash, hash]); - this.encrypted = await crypto.cfb.encrypt(sessionKeyAlgorithm, key, plaintext, new Uint8Array(crypto.cipher[sessionKeyAlgorithm].blockSize)); - return true; -}; + this.encrypted = await crypto.cfb.encrypt(sessionKeyAlgorithm, key, plaintext, new Uint8Array(crypto.cipher[sessionKeyAlgorithm].blockSize)); + return true; + } -/** - * Decrypts the encrypted data contained in the packet. - * @param {String} sessionKeyAlgorithm The selected symmetric encryption algorithm to be used e.g. 'aes128' - * @param {Uint8Array} key The key of cipher blocksize length to be used - * @param {Boolean} streaming Whether to read this.encrypted as a stream - * @returns {Promise} - * @async - */ -SymEncryptedIntegrityProtectedDataPacket.prototype.decrypt = async function (sessionKeyAlgorithm, key, streaming) { - let encrypted = stream.clone(this.encrypted); - if (!streaming) encrypted = await stream.readToEnd(encrypted); - const decrypted = await crypto.cfb.decrypt(sessionKeyAlgorithm, key, encrypted, new Uint8Array(crypto.cipher[sessionKeyAlgorithm].blockSize)); + /** + * Decrypts the encrypted data contained in the packet. + * @param {String} sessionKeyAlgorithm The selected symmetric encryption algorithm to be used e.g. 'aes128' + * @param {Uint8Array} key The key of cipher blocksize length to be used + * @param {Boolean} streaming Whether to read this.encrypted as a stream + * @returns {Promise} + * @async + */ + async decrypt(sessionKeyAlgorithm, key, streaming) { + let encrypted = stream.clone(this.encrypted); + if (!streaming) encrypted = await stream.readToEnd(encrypted); + const decrypted = await crypto.cfb.decrypt(sessionKeyAlgorithm, key, encrypted, new Uint8Array(crypto.cipher[sessionKeyAlgorithm].blockSize)); - // there must be a modification detection code packet as the - // last packet and everything gets hashed except the hash itself - const realHash = stream.slice(stream.passiveClone(decrypted), -20); - const tohash = stream.slice(decrypted, 0, -20); - const verifyHash = Promise.all([ - stream.readToEnd(await crypto.hash.sha1(stream.passiveClone(tohash))), - stream.readToEnd(realHash) - ]).then(([hash, mdc]) => { - if (!util.equalsUint8Array(hash, mdc)) { - throw new Error('Modification detected.'); + // there must be a modification detection code packet as the + // last packet and everything gets hashed except the hash itself + const realHash = stream.slice(stream.passiveClone(decrypted), -20); + const tohash = stream.slice(decrypted, 0, -20); + const verifyHash = Promise.all([ + stream.readToEnd(await crypto.hash.sha1(stream.passiveClone(tohash))), + stream.readToEnd(realHash) + ]).then(([hash, mdc]) => { + if (!util.equalsUint8Array(hash, mdc)) { + throw new Error('Modification detected.'); + } + return new Uint8Array(); + }); + const bytes = stream.slice(tohash, crypto.cipher[sessionKeyAlgorithm].blockSize + 2); // Remove random prefix + let packetbytes = stream.slice(bytes, 0, -2); // Remove MDC packet + packetbytes = stream.concat([packetbytes, stream.fromAsync(() => verifyHash)]); + if (!util.isStream(encrypted) || !config.allowUnauthenticatedStream) { + packetbytes = await stream.readToEnd(packetbytes); } - return new Uint8Array(); - }); - const bytes = stream.slice(tohash, crypto.cipher[sessionKeyAlgorithm].blockSize + 2); // Remove random prefix - let packetbytes = stream.slice(bytes, 0, -2); // Remove MDC packet - packetbytes = stream.concat([packetbytes, stream.fromAsync(() => verifyHash)]); - if (!util.isStream(encrypted) || !config.allowUnauthenticatedStream) { - packetbytes = await stream.readToEnd(packetbytes); + await this.packets.read(packetbytes, { + LiteralDataPacket, + CompressedDataPacket, + OnePassSignaturePacket, + SignaturePacket + }, streaming); + return true; } - await this.packets.read(packetbytes, { - LiteralDataPacket, - CompressedDataPacket, - OnePassSignaturePacket, - SignaturePacket - }, streaming); - return true; -}; +} export default SymEncryptedIntegrityProtectedDataPacket; diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index 8f333f9c..1d8dc248 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -45,153 +45,154 @@ import util from '../util'; * public key, decrypts the session key, and then uses the session key to * decrypt the message. * @memberof module:packet - * @constructor */ -function SymEncryptedSessionKeyPacket() { - this.tag = enums.packet.symEncryptedSessionKey; - this.version = config.aeadProtect ? 5 : 4; - this.sessionKey = null; - this.sessionKeyEncryptionAlgorithm = null; - this.sessionKeyAlgorithm = 'aes256'; - this.aeadAlgorithm = enums.read(enums.aead, config.aeadMode); - this.encrypted = null; - this.s2k = null; - this.iv = null; -} - -/** - * Parsing function for a symmetric encrypted session key packet (tag 3). - * - * @param {Uint8Array} input Payload of a tag 1 packet - * @param {Integer} position Position to start reading from the input string - * @param {Integer} len - * Length of the packet or the remaining length of - * input at position - * @returns {SymEncryptedSessionKeyPacket} Object representation - */ -SymEncryptedSessionKeyPacket.prototype.read = function(bytes) { - let offset = 0; - - // A one-octet version number. The only currently defined version is 4. - this.version = bytes[offset++]; - - // A one-octet number describing the symmetric algorithm used. - const algo = enums.read(enums.symmetric, bytes[offset++]); - - if (this.version === 5) { - // A one-octet AEAD algorithm. - this.aeadAlgorithm = enums.read(enums.aead, bytes[offset++]); +class SymEncryptedSessionKeyPacket { + constructor() { + this.tag = enums.packet.symEncryptedSessionKey; + this.version = config.aeadProtect ? 5 : 4; + this.sessionKey = null; + this.sessionKeyEncryptionAlgorithm = null; + this.sessionKeyAlgorithm = 'aes256'; + this.aeadAlgorithm = enums.read(enums.aead, config.aeadMode); + this.encrypted = null; + this.s2k = null; + this.iv = null; } - // A string-to-key (S2K) specifier, length as defined above. - this.s2k = new type_s2k(); - offset += this.s2k.read(bytes.subarray(offset, bytes.length)); + /** + * Parsing function for a symmetric encrypted session key packet (tag 3). + * + * @param {Uint8Array} input Payload of a tag 1 packet + * @param {Integer} position Position to start reading from the input string + * @param {Integer} len + * Length of the packet or the remaining length of + * input at position + * @returns {SymEncryptedSessionKeyPacket} Object representation + */ + read(bytes) { + let offset = 0; + + // A one-octet version number. The only currently defined version is 4. + this.version = bytes[offset++]; + + // A one-octet number describing the symmetric algorithm used. + const algo = enums.read(enums.symmetric, bytes[offset++]); + + if (this.version === 5) { + // A one-octet AEAD algorithm. + this.aeadAlgorithm = enums.read(enums.aead, bytes[offset++]); + } - if (this.version === 5) { - const mode = crypto[this.aeadAlgorithm]; + // A string-to-key (S2K) specifier, length as defined above. + this.s2k = new type_s2k(); + offset += this.s2k.read(bytes.subarray(offset, bytes.length)); - // A starting initialization vector of size specified by the AEAD - // algorithm. - this.iv = bytes.subarray(offset, offset += mode.ivLength); - } + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; - // The encrypted session key itself, which is decrypted with the - // string-to-key object. This is optional in version 4. - if (this.version === 5 || offset < bytes.length) { - this.encrypted = bytes.subarray(offset, bytes.length); - this.sessionKeyEncryptionAlgorithm = algo; - } else { - this.sessionKeyAlgorithm = algo; + // A starting initialization vector of size specified by the AEAD + // algorithm. + this.iv = bytes.subarray(offset, offset += mode.ivLength); + } + + // The encrypted session key itself, which is decrypted with the + // string-to-key object. This is optional in version 4. + if (this.version === 5 || offset < bytes.length) { + this.encrypted = bytes.subarray(offset, bytes.length); + this.sessionKeyEncryptionAlgorithm = algo; + } else { + this.sessionKeyAlgorithm = algo; + } } -}; -SymEncryptedSessionKeyPacket.prototype.write = function() { - const algo = this.encrypted === null ? - this.sessionKeyAlgorithm : - this.sessionKeyEncryptionAlgorithm; + write() { + const algo = this.encrypted === null ? + this.sessionKeyAlgorithm : + this.sessionKeyEncryptionAlgorithm; - let bytes; + let bytes; - if (this.version === 5) { - bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo), enums.write(enums.aead, this.aeadAlgorithm)]), this.s2k.write(), this.iv, this.encrypted]); - } else { - bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]); + if (this.version === 5) { + bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo), enums.write(enums.aead, this.aeadAlgorithm)]), this.s2k.write(), this.iv, this.encrypted]); + } else { + bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]); - if (this.encrypted !== null) { - bytes = util.concatUint8Array([bytes, this.encrypted]); + if (this.encrypted !== null) { + bytes = util.concatUint8Array([bytes, this.encrypted]); + } } + + return bytes; } - return bytes; -}; + /** + * Decrypts the session key + * @param {String} passphrase The passphrase in string form + * @returns {Promise} + * @async + */ + async decrypt(passphrase) { + const algo = this.sessionKeyEncryptionAlgorithm !== null ? + this.sessionKeyEncryptionAlgorithm : + this.sessionKeyAlgorithm; + + const length = crypto.cipher[algo].keySize; + const key = await this.s2k.produce_key(passphrase, length); + + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; + const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); + const modeInstance = await mode(algo, key); + this.sessionKey = await modeInstance.decrypt(this.encrypted, this.iv, adata); + } else if (this.encrypted !== null) { + const decrypted = await crypto.cfb.decrypt(algo, key, this.encrypted, new Uint8Array(crypto.cipher[algo].blockSize)); + + this.sessionKeyAlgorithm = enums.read(enums.symmetric, decrypted[0]); + this.sessionKey = decrypted.subarray(1, decrypted.length); + } else { + this.sessionKey = key; + } -/** - * Decrypts the session key - * @param {String} passphrase The passphrase in string form - * @returns {Promise} - * @async - */ -SymEncryptedSessionKeyPacket.prototype.decrypt = async function(passphrase) { - const algo = this.sessionKeyEncryptionAlgorithm !== null ? - this.sessionKeyEncryptionAlgorithm : - this.sessionKeyAlgorithm; - - const length = crypto.cipher[algo].keySize; - const key = await this.s2k.produce_key(passphrase, length); - - if (this.version === 5) { - const mode = crypto[this.aeadAlgorithm]; - const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); - const modeInstance = await mode(algo, key); - this.sessionKey = await modeInstance.decrypt(this.encrypted, this.iv, adata); - } else if (this.encrypted !== null) { - const decrypted = await crypto.cfb.decrypt(algo, key, this.encrypted, new Uint8Array(crypto.cipher[algo].blockSize)); - - this.sessionKeyAlgorithm = enums.read(enums.symmetric, decrypted[0]); - this.sessionKey = decrypted.subarray(1, decrypted.length); - } else { - this.sessionKey = key; + return true; } - return true; -}; + /** + * Encrypts the session key + * @param {String} passphrase The passphrase in string form + * @returns {Promise} + * @async + */ + async encrypt(passphrase) { + const algo = this.sessionKeyEncryptionAlgorithm !== null ? + this.sessionKeyEncryptionAlgorithm : + this.sessionKeyAlgorithm; -/** - * Encrypts the session key - * @param {String} passphrase The passphrase in string form - * @returns {Promise} - * @async - */ -SymEncryptedSessionKeyPacket.prototype.encrypt = async function(passphrase) { - const algo = this.sessionKeyEncryptionAlgorithm !== null ? - this.sessionKeyEncryptionAlgorithm : - this.sessionKeyAlgorithm; + this.sessionKeyEncryptionAlgorithm = algo; - this.sessionKeyEncryptionAlgorithm = algo; + this.s2k = new type_s2k(); + this.s2k.salt = await crypto.random.getRandomBytes(8); - this.s2k = new type_s2k(); - this.s2k.salt = await crypto.random.getRandomBytes(8); + const length = crypto.cipher[algo].keySize; + const key = await this.s2k.produce_key(passphrase, length); - const length = crypto.cipher[algo].keySize; - const key = await this.s2k.produce_key(passphrase, length); + if (this.sessionKey === null) { + this.sessionKey = await crypto.generateSessionKey(this.sessionKeyAlgorithm); + } - if (this.sessionKey === null) { - this.sessionKey = await crypto.generateSessionKey(this.sessionKeyAlgorithm); - } + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; + this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV + const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); + const modeInstance = await mode(algo, key); + this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, adata); + } else { + const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); + const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); + this.encrypted = await crypto.cfb.encrypt(algo, key, private_key, new Uint8Array(crypto.cipher[algo].blockSize)); + } - if (this.version === 5) { - const mode = crypto[this.aeadAlgorithm]; - this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV - const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); - const modeInstance = await mode(algo, key); - this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, adata); - } else { - const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); - const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); - this.encrypted = await crypto.cfb.encrypt(algo, key, private_key, new Uint8Array(crypto.cipher[algo].blockSize)); + return true; } - - return true; -}; +} export default SymEncryptedSessionKeyPacket; diff --git a/src/packet/symmetrically_encrypted_data.js b/src/packet/symmetrically_encrypted_data.js index ba3fb883..c4a254ef 100644 --- a/src/packet/symmetrically_encrypted_data.js +++ b/src/packet/symmetrically_encrypted_data.js @@ -46,85 +46,86 @@ import { * theory other Symmetrically Encrypted Data packets or sequences of packets * that form whole OpenPGP messages). * @memberof module:packet - * @constructor */ -function SymmetricallyEncryptedDataPacket() { - /** - * Packet type - * @type {module:enums.packet} - */ - this.tag = enums.packet.symmetricallyEncryptedData; - /** - * Encrypted secret-key data - */ - this.encrypted = null; - /** - * Decrypted packets contained within. - * @type {PacketList} - */ - this.packets = null; - /** - * When true, decrypt fails if message is not integrity protected - * @see module:config.ignoreMdcError - */ - this.ignoreMdcError = config.ignoreMdcError; -} - -SymmetricallyEncryptedDataPacket.prototype.read = function (bytes) { - this.encrypted = bytes; -}; +class SymmetricallyEncryptedDataPacket { + constructor() { + /** + * Packet type + * @type {module:enums.packet} + */ + this.tag = enums.packet.symmetricallyEncryptedData; + /** + * Encrypted secret-key data + */ + this.encrypted = null; + /** + * Decrypted packets contained within. + * @type {PacketList} + */ + this.packets = null; + /** + * When true, decrypt fails if message is not integrity protected + * @see module:config.ignoreMdcError + */ + this.ignoreMdcError = config.ignoreMdcError; + } -SymmetricallyEncryptedDataPacket.prototype.write = function () { - return this.encrypted; -}; + read(bytes) { + this.encrypted = bytes; + } -/** - * Decrypt the symmetrically-encrypted packet data - * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. - * @param {module:enums.symmetric} sessionKeyAlgorithm Symmetric key algorithm to use - * @param {Uint8Array} key The key of cipher blocksize length to be used - * @returns {Promise} - * @async - */ -SymmetricallyEncryptedDataPacket.prototype.decrypt = async function (sessionKeyAlgorithm, key, streaming) { - // If MDC errors are not being ignored, all missing MDC packets in symmetrically encrypted data should throw an error - if (!this.ignoreMdcError) { - throw new Error('Decryption failed due to missing MDC.'); + write() { + return this.encrypted; } - this.encrypted = await stream.readToEnd(this.encrypted); - const decrypted = await crypto.cfb.decrypt(sessionKeyAlgorithm, key, - this.encrypted.subarray(crypto.cipher[sessionKeyAlgorithm].blockSize + 2), - this.encrypted.subarray(2, crypto.cipher[sessionKeyAlgorithm].blockSize + 2) - ); + /** + * Decrypt the symmetrically-encrypted packet data + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} sessionKeyAlgorithm Symmetric key algorithm to use + * @param {Uint8Array} key The key of cipher blocksize length to be used + * @returns {Promise} + * @async + */ + async decrypt(sessionKeyAlgorithm, key, streaming) { + // If MDC errors are not being ignored, all missing MDC packets in symmetrically encrypted data should throw an error + if (!this.ignoreMdcError) { + throw new Error('Decryption failed due to missing MDC.'); + } - await this.packets.read(decrypted, { - LiteralDataPacket, - CompressedDataPacket, - OnePassSignaturePacket, - SignaturePacket - }, streaming); + this.encrypted = await stream.readToEnd(this.encrypted); + const decrypted = await crypto.cfb.decrypt(sessionKeyAlgorithm, key, + this.encrypted.subarray(crypto.cipher[sessionKeyAlgorithm].blockSize + 2), + this.encrypted.subarray(2, crypto.cipher[sessionKeyAlgorithm].blockSize + 2) + ); - return true; -}; + await this.packets.read(decrypted, { + LiteralDataPacket, + CompressedDataPacket, + OnePassSignaturePacket, + SignaturePacket + }, streaming); -/** - * Encrypt the symmetrically-encrypted packet data - * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. - * @param {module:enums.symmetric} sessionKeyAlgorithm Symmetric key algorithm to use - * @param {Uint8Array} key The key of cipher blocksize length to be used - * @returns {Promise} - * @async - */ -SymmetricallyEncryptedDataPacket.prototype.encrypt = async function (algo, key) { - const data = this.packets.write(); + return true; + } + + /** + * Encrypt the symmetrically-encrypted packet data + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} sessionKeyAlgorithm Symmetric key algorithm to use + * @param {Uint8Array} key The key of cipher blocksize length to be used + * @returns {Promise} + * @async + */ + async encrypt(algo, key) { + const data = this.packets.write(); - const prefix = await crypto.getPrefixRandom(algo); - const FRE = await crypto.cfb.encrypt(algo, key, prefix, new Uint8Array(crypto.cipher[algo].blockSize)); - const ciphertext = await crypto.cfb.encrypt(algo, key, data, FRE.subarray(2)); - this.encrypted = util.concat([FRE, ciphertext]); + const prefix = await crypto.getPrefixRandom(algo); + const FRE = await crypto.cfb.encrypt(algo, key, prefix, new Uint8Array(crypto.cipher[algo].blockSize)); + const ciphertext = await crypto.cfb.encrypt(algo, key, data, FRE.subarray(2)); + this.encrypted = util.concat([FRE, ciphertext]); - return true; -}; + return true; + } +} export default SymmetricallyEncryptedDataPacket; diff --git a/src/packet/trust.js b/src/packet/trust.js index d40bf3c8..3d98a3cc 100644 --- a/src/packet/trust.js +++ b/src/packet/trust.js @@ -19,17 +19,18 @@ import enums from '../enums'; * transferred to other users, and they SHOULD be ignored on any input * other than local keyring files. * @memberof module:packet - * @constructor */ -function TrustPacket() { - this.tag = enums.packet.trust; -} +class TrustPacket { + constructor() { + this.tag = enums.packet.trust; + } -/** - * Parsing function for a trust packet (tag 12). - * Currently not implemented as we ignore trust packets - * @param {String} byptes payload of a tag 12 packet - */ -TrustPacket.prototype.read = function () {}; // TODO + /** + * Parsing function for a trust packet (tag 12). + * Currently not implemented as we ignore trust packets + * @param {String} byptes payload of a tag 12 packet + */ + read() {} // TODO +} export default TrustPacket; diff --git a/src/packet/user_attribute.js b/src/packet/user_attribute.js index 841848ab..d73a7803 100644 --- a/src/packet/user_attribute.js +++ b/src/packet/user_attribute.js @@ -42,53 +42,54 @@ import util from '../util'; * User Attribute packet as a User ID packet with opaque contents, but * an implementation may use any method desired. * @memberof module:packet - * @constructor */ -function UserAttributePacket() { - this.tag = enums.packet.userAttribute; - this.attributes = []; -} +class UserAttributePacket { + constructor() { + this.tag = enums.packet.userAttribute; + this.attributes = []; + } -/** - * parsing function for a user attribute packet (tag 17). - * @param {Uint8Array} input payload of a tag 17 packet - */ -UserAttributePacket.prototype.read = function(bytes) { - let i = 0; - while (i < bytes.length) { - const len = packet.readSimpleLength(bytes.subarray(i, bytes.length)); - i += len.offset; + /** + * parsing function for a user attribute packet (tag 17). + * @param {Uint8Array} input payload of a tag 17 packet + */ + read(bytes) { + let i = 0; + while (i < bytes.length) { + const len = packet.readSimpleLength(bytes.subarray(i, bytes.length)); + i += len.offset; - this.attributes.push(util.uint8ArrayToStr(bytes.subarray(i, i + len.len))); - i += len.len; + this.attributes.push(util.uint8ArrayToStr(bytes.subarray(i, i + len.len))); + i += len.len; + } } -}; -/** - * Creates a binary representation of the user attribute packet - * @returns {Uint8Array} string representation - */ -UserAttributePacket.prototype.write = function() { - const arr = []; - for (let i = 0; i < this.attributes.length; i++) { - arr.push(packet.writeSimpleLength(this.attributes[i].length)); - arr.push(util.strToUint8Array(this.attributes[i])); + /** + * Creates a binary representation of the user attribute packet + * @returns {Uint8Array} string representation + */ + write() { + const arr = []; + for (let i = 0; i < this.attributes.length; i++) { + arr.push(packet.writeSimpleLength(this.attributes[i].length)); + arr.push(util.strToUint8Array(this.attributes[i])); + } + return util.concatUint8Array(arr); } - return util.concatUint8Array(arr); -}; -/** - * Compare for equality - * @param {UserAttributePacket} usrAttr - * @returns {Boolean} true if equal - */ -UserAttributePacket.prototype.equals = function(usrAttr) { - if (!usrAttr || !(usrAttr instanceof UserAttributePacket)) { - return false; + /** + * Compare for equality + * @param {UserAttributePacket} usrAttr + * @returns {Boolean} true if equal + */ + equals(usrAttr) { + if (!usrAttr || !(usrAttr instanceof UserAttributePacket)) { + return false; + } + return this.attributes.every(function(attr, index) { + return attr === usrAttr.attributes[index]; + }); } - return this.attributes.every(function(attr, index) { - return attr === usrAttr.attributes[index]; - }); -}; +} export default UserAttributePacket; diff --git a/src/packet/userid.js b/src/packet/userid.js index daadcc0a..0978dc13 100644 --- a/src/packet/userid.js +++ b/src/packet/userid.js @@ -32,56 +32,57 @@ import util from '../util'; * restrictions on its content. The packet length in the header * specifies the length of the User ID. * @memberof module:packet - * @constructor */ -function UserIDPacket() { - this.tag = enums.packet.userID; - /** A string containing the user id. Usually in the form - * John Doe - * @type {String} - */ - this.userid = ''; +class UserIDPacket { + constructor() { + this.tag = enums.packet.userID; + /** A string containing the user id. Usually in the form + * John Doe + * @type {String} + */ + this.userid = ''; - this.name = ''; - this.email = ''; - this.comment = ''; -} + this.name = ''; + this.email = ''; + this.comment = ''; + } -/** - * Parsing function for a user id packet (tag 13). - * @param {Uint8Array} input payload of a tag 13 packet - */ -UserIDPacket.prototype.read = function (bytes) { - this.parse(util.decodeUtf8(bytes)); -}; + /** + * Parsing function for a user id packet (tag 13). + * @param {Uint8Array} input payload of a tag 13 packet + */ + read(bytes) { + this.parse(util.decodeUtf8(bytes)); + } -/** - * Parse userid string, e.g. 'John Doe ' - */ -UserIDPacket.prototype.parse = function (userid) { - try { - Object.assign(this, util.parseUserId(userid)); - } catch (e) {} - this.userid = userid; -}; + /** + * Parse userid string, e.g. 'John Doe ' + */ + parse(userid) { + try { + Object.assign(this, util.parseUserId(userid)); + } catch (e) {} + this.userid = userid; + } -/** - * Creates a binary representation of the user id packet - * @returns {Uint8Array} binary representation - */ -UserIDPacket.prototype.write = function () { - return util.encodeUtf8(this.userid); -}; + /** + * Creates a binary representation of the user id packet + * @returns {Uint8Array} binary representation + */ + write() { + return util.encodeUtf8(this.userid); + } -/** - * Set userid string from object, e.g. { name:'Phil Zimmermann', email:'phil@openpgp.org' } - */ -UserIDPacket.prototype.format = function (userid) { - if (util.isString(userid)) { - userid = util.parseUserId(userid); + /** + * Set userid string from object, e.g. { name:'Phil Zimmermann', email:'phil@openpgp.org' } + */ + format(userid) { + if (util.isString(userid)) { + userid = util.parseUserId(userid); + } + Object.assign(this, userid); + this.userid = util.formatUserId(userid); } - Object.assign(this, userid); - this.userid = util.formatUserId(userid); -}; +} export default UserIDPacket; diff --git a/src/signature.js b/src/signature.js index e1488eaa..9cbf2158 100644 --- a/src/signature.js +++ b/src/signature.js @@ -27,33 +27,32 @@ import { PacketList, SignaturePacket } from './packet'; import enums from './enums'; /** - * @class - * @classdesc Class that represents an OpenPGP signature. - * @param {PacketList} packetlist The signature packets + * Class that represents an OpenPGP signature. */ -export function Signature(packetlist) { - if (!(this instanceof Signature)) { - return new Signature(packetlist); +export class Signature { + /** + * @param {PacketList} packetlist The signature packets + */ + constructor(packetlist) { + this.packets = packetlist || new PacketList(); } - this.packets = packetlist || new PacketList(); -} - -/** - * Returns binary encoded signature - * @returns {ReadableStream} binary signature - */ -Signature.prototype.write = function() { - return this.packets.write(); -}; + /** + * Returns binary encoded signature + * @returns {ReadableStream} binary signature + */ + write() { + return this.packets.write(); + } -/** - * Returns ASCII armored text of signature - * @returns {ReadableStream} ASCII armor - */ -Signature.prototype.armor = function() { - return armor.encode(enums.armor.signature, this.write()); -}; + /** + * Returns ASCII armored text of signature + * @returns {ReadableStream} ASCII armor + */ + armor() { + return armor.encode(enums.armor.signature, this.write()); + } +} /** * reads an OpenPGP armored signature and returns a signature object diff --git a/src/type/ecdh_symkey.js b/src/type/ecdh_symkey.js index ee224a3c..4ad26724 100644 --- a/src/type/ecdh_symkey.js +++ b/src/type/ecdh_symkey.js @@ -24,42 +24,41 @@ import util from '../util'; -/** - * @constructor - */ -function ECDHSymmetricKey(data) { - if (typeof data === 'undefined') { - data = new Uint8Array([]); - } else if (util.isString(data)) { - data = util.strToUint8Array(data); - } else { - data = new Uint8Array(data); +class ECDHSymmetricKey { + constructor(data) { + if (typeof data === 'undefined') { + data = new Uint8Array([]); + } else if (util.isString(data)) { + data = util.strToUint8Array(data); + } else { + data = new Uint8Array(data); + } + this.data = data; } - this.data = data; -} -/** - * Read an ECDHSymmetricKey from an Uint8Array - * @param {Uint8Array} input Where to read the encoded symmetric key from - * @returns {Number} Number of read bytes - */ -ECDHSymmetricKey.prototype.read = function (input) { - if (input.length >= 1) { - const length = input[0]; - if (input.length >= 1 + length) { - this.data = input.subarray(1, 1 + length); - return 1 + this.data.length; + /** + * Read an ECDHSymmetricKey from an Uint8Array + * @param {Uint8Array} input Where to read the encoded symmetric key from + * @returns {Number} Number of read bytes + */ + read(input) { + if (input.length >= 1) { + const length = input[0]; + if (input.length >= 1 + length) { + this.data = input.subarray(1, 1 + length); + return 1 + this.data.length; + } } + throw new Error('Invalid symmetric key'); } - throw new Error('Invalid symmetric key'); -}; -/** - * Write an ECDHSymmetricKey as an Uint8Array - * @returns {Uint8Array} An array containing the value - */ -ECDHSymmetricKey.prototype.write = function () { - return util.concatUint8Array([new Uint8Array([this.data.length]), this.data]); -}; + /** + * Write an ECDHSymmetricKey as an Uint8Array + * @returns {Uint8Array} An array containing the value + */ + write() { + return util.concatUint8Array([new Uint8Array([this.data.length]), this.data]); + } +} export default ECDHSymmetricKey; diff --git a/src/type/kdf_params.js b/src/type/kdf_params.js index c8520692..c46619dc 100644 --- a/src/type/kdf_params.js +++ b/src/type/kdf_params.js @@ -27,42 +27,43 @@ * @module type/kdf_params */ -/** - * @constructor - * @param {enums.hash} hash Hash algorithm - * @param {enums.symmetric} cipher Symmetric algorithm - */ -function KDFParams(data) { - if (data) { - const { hash, cipher } = data; - this.hash = hash; - this.cipher = cipher; - } else { - this.hash = null; - this.cipher = null; +class KDFParams { + /** + * @param {enums.hash} hash Hash algorithm + * @param {enums.symmetric} cipher Symmetric algorithm + */ + constructor(data) { + if (data) { + const { hash, cipher } = data; + this.hash = hash; + this.cipher = cipher; + } else { + this.hash = null; + this.cipher = null; + } } -} -/** - * Read KDFParams from an Uint8Array - * @param {Uint8Array} input Where to read the KDFParams from - * @returns {Number} Number of read bytes - */ -KDFParams.prototype.read = function (input) { - if (input.length < 4 || input[0] !== 3 || input[1] !== 1) { - throw new Error('Cannot read KDFParams'); + /** + * Read KDFParams from an Uint8Array + * @param {Uint8Array} input Where to read the KDFParams from + * @returns {Number} Number of read bytes + */ + read(input) { + if (input.length < 4 || input[0] !== 3 || input[1] !== 1) { + throw new Error('Cannot read KDFParams'); + } + this.hash = input[2]; + this.cipher = input[3]; + return 4; } - this.hash = input[2]; - this.cipher = input[3]; - return 4; -}; -/** - * Write KDFParams to an Uint8Array - * @returns {Uint8Array} Array with the KDFParams value - */ -KDFParams.prototype.write = function () { - return new Uint8Array([3, 1, this.hash, this.cipher]); -}; + /** + * Write KDFParams to an Uint8Array + * @returns {Uint8Array} Array with the KDFParams value + */ + write() { + return new Uint8Array([3, 1, this.hash, this.cipher]); + } +} export default KDFParams; diff --git a/src/type/keyid.js b/src/type/keyid.js index d90a65a4..df0682dd 100644 --- a/src/type/keyid.js +++ b/src/type/keyid.js @@ -29,76 +29,75 @@ import util from '../util.js'; -/** - * @constructor - */ -function Keyid() { - this.bytes = ''; -} +class Keyid { + constructor() { + this.bytes = ''; + } -/** - * Parsing method for a key id - * @param {Uint8Array} input Input to read the key id from - */ -Keyid.prototype.read = function(bytes) { - this.bytes = util.uint8ArrayToStr(bytes.subarray(0, 8)); -}; + /** + * Parsing method for a key id + * @param {Uint8Array} input Input to read the key id from + */ + read(bytes) { + this.bytes = util.uint8ArrayToStr(bytes.subarray(0, 8)); + } -/** - * Serializes the Key ID - * @returns {Uint8Array} Key ID as a Uint8Array - */ -Keyid.prototype.write = function() { - return util.strToUint8Array(this.bytes); -}; + /** + * Serializes the Key ID + * @returns {Uint8Array} Key ID as a Uint8Array + */ + write() { + return util.strToUint8Array(this.bytes); + } -/** - * Returns the Key ID represented as a hexadecimal string - * @returns {String} Key ID as a hexadecimal string - */ -Keyid.prototype.toHex = function() { - return util.strToHex(this.bytes); -}; + /** + * Returns the Key ID represented as a hexadecimal string + * @returns {String} Key ID as a hexadecimal string + */ + toHex() { + return util.strToHex(this.bytes); + } -/** - * Checks equality of Key ID's - * @param {Keyid} keyid - * @param {Boolean} matchWildcard Indicates whether to check if either keyid is a wildcard - */ -Keyid.prototype.equals = function(keyid, matchWildcard = false) { - return (matchWildcard && (keyid.isWildcard() || this.isWildcard())) || this.bytes === keyid.bytes; -}; + /** + * Checks equality of Key ID's + * @param {Keyid} keyid + * @param {Boolean} matchWildcard Indicates whether to check if either keyid is a wildcard + */ + equals(keyid, matchWildcard = false) { + return (matchWildcard && (keyid.isWildcard() || this.isWildcard())) || this.bytes === keyid.bytes; + } -/** - * Checks to see if the Key ID is unset - * @returns {Boolean} true if the Key ID is null - */ -Keyid.prototype.isNull = function() { - return this.bytes === ''; -}; + /** + * Checks to see if the Key ID is unset + * @returns {Boolean} true if the Key ID is null + */ + isNull() { + return this.bytes === ''; + } -/** - * Checks to see if the Key ID is a "wildcard" Key ID (all zeros) - * @returns {Boolean} true if this is a wildcard Key ID - */ -Keyid.prototype.isWildcard = function() { - return /^0+$/.test(this.toHex()); -}; + /** + * Checks to see if the Key ID is a "wildcard" Key ID (all zeros) + * @returns {Boolean} true if this is a wildcard Key ID + */ + isWildcard() { + return /^0+$/.test(this.toHex()); + } -Keyid.mapToHex = function (keyId) { - return keyId.toHex(); -}; + static mapToHex(keyId) { + return keyId.toHex(); + } -Keyid.fromId = function (hex) { - const keyid = new Keyid(); - keyid.read(util.hexToUint8Array(hex)); - return keyid; -}; + static fromId(hex) { + const keyid = new Keyid(); + keyid.read(util.hexToUint8Array(hex)); + return keyid; + } -Keyid.wildcard = function () { - const keyid = new Keyid(); - keyid.read(new Uint8Array(8)); - return keyid; -}; + static wildcard() { + const keyid = new Keyid(); + keyid.read(new Uint8Array(8)); + return keyid; + } +} export default Keyid; diff --git a/src/type/mpi.js b/src/type/mpi.js index 919282ae..a746119f 100644 --- a/src/type/mpi.js +++ b/src/type/mpi.js @@ -36,99 +36,98 @@ import BN from 'bn.js'; import util from '../util'; -/** - * @constructor - */ -function MPI(data) { - /** An implementation dependent integer */ - if (data instanceof MPI) { - this.data = data.data; - } else if (BN.isBN(data)) { - this.fromBN(data); - } else if (util.isUint8Array(data)) { - this.fromUint8Array(data); - } else if (util.isString(data)) { - this.fromString(data); - } else { - this.data = null; +class MPI { + constructor(data) { + /** An implementation dependent integer */ + if (data instanceof MPI) { + this.data = data.data; + } else if (BN.isBN(data)) { + this.fromBN(data); + } else if (util.isUint8Array(data)) { + this.fromUint8Array(data); + } else if (util.isString(data)) { + this.fromString(data); + } else { + this.data = null; + } } -} -/** - * Parsing function for a MPI ({@link https://tools.ietf.org/html/rfc4880#section-3.2|RFC 4880 3.2}). - * @param {Uint8Array} input Payload of MPI data - * @param {String} endian Endianness of the data; 'be' for big-endian or 'le' for little-endian - * @returns {Integer} Length of data read - */ -MPI.prototype.read = function (bytes, endian = 'be') { - if (util.isString(bytes)) { - bytes = util.strToUint8Array(bytes); + /** + * Parsing function for a MPI ({@link https://tools.ietf.org/html/rfc4880#section-3.2|RFC 4880 3.2}). + * @param {Uint8Array} input Payload of MPI data + * @param {String} endian Endianness of the data; 'be' for big-endian or 'le' for little-endian + * @returns {Integer} Length of data read + */ + read(bytes, endian = 'be') { + if (util.isString(bytes)) { + bytes = util.strToUint8Array(bytes); + } + + const bits = (bytes[0] << 8) | bytes[1]; + const bytelen = (bits + 7) >>> 3; + const payload = bytes.subarray(2, 2 + bytelen); + + this.fromUint8Array(payload, endian); + + return 2 + bytelen; } - const bits = (bytes[0] << 8) | bytes[1]; - const bytelen = (bits + 7) >>> 3; - const payload = bytes.subarray(2, 2 + bytelen); + /** + * Converts the mpi object to a bytes as specified in + * {@link https://tools.ietf.org/html/rfc4880#section-3.2|RFC4880 3.2} + * @param {String} endian Endianness of the payload; 'be' for big-endian or 'le' for little-endian + * @param {Integer} length Length of the data part of the MPI + * @returns {Uint8Aray} mpi Byte representation + */ + write(endian, length) { + return util.uint8ArrayToMpi(this.toUint8Array(endian, length)); + } - this.fromUint8Array(payload, endian); + bitLength() { + return (this.data.length - 1) * 8 + util.nbits(this.data[0]); + } - return 2 + bytelen; -}; + byteLength() { + return this.data.length; + } -/** - * Converts the mpi object to a bytes as specified in - * {@link https://tools.ietf.org/html/rfc4880#section-3.2|RFC4880 3.2} - * @param {String} endian Endianness of the payload; 'be' for big-endian or 'le' for little-endian - * @param {Integer} length Length of the data part of the MPI - * @returns {Uint8Aray} mpi Byte representation - */ -MPI.prototype.write = function (endian, length) { - return util.uint8ArrayToMpi(this.toUint8Array(endian, length)); -}; - -MPI.prototype.bitLength = function () { - return (this.data.length - 1) * 8 + util.nbits(this.data[0]); -}; - -MPI.prototype.byteLength = function () { - return this.data.length; -}; - -MPI.prototype.toUint8Array = function (endian, length) { - endian = endian || 'be'; - length = length || this.data.length; - - const payload = new Uint8Array(length); - const start = endian === 'le' ? 0 : length - this.data.length; - payload.set(this.data, start); - if (endian === 'le') { - payload.reverse(); + toUint8Array(endian, length) { + endian = endian || 'be'; + length = length || this.data.length; + + const payload = new Uint8Array(length); + const start = endian === 'le' ? 0 : length - this.data.length; + payload.set(this.data, start); + if (endian === 'le') { + payload.reverse(); + } + return payload; } - return payload; -}; -MPI.prototype.fromUint8Array = function (bytes, endian = 'be') { - this.data = new Uint8Array(bytes.length); - this.data.set(bytes); + fromUint8Array(bytes, endian = 'be') { + this.data = new Uint8Array(bytes.length); + this.data.set(bytes); - if (endian === 'le') { - this.data.reverse(); + if (endian === 'le') { + this.data.reverse(); + } } -}; -MPI.prototype.toString = function () { - return util.uint8ArrayToStr(this.toUint8Array()); -}; + toString() { + return util.uint8ArrayToStr(this.toUint8Array()); + } -MPI.prototype.fromString = function (str, endian = 'be') { - this.fromUint8Array(util.strToUint8Array(str), endian); -}; + fromString(str, endian = 'be') { + this.fromUint8Array(util.strToUint8Array(str), endian); + } -MPI.prototype.toBN = function () { - return new BN(this.toUint8Array()); -}; + toBN() { + return new BN(this.toUint8Array()); + } -MPI.prototype.fromBN = function (bn) { - this.data = bn.toArrayLike(Uint8Array); -}; + fromBN(bn) { + this.data = bn.toArrayLike(Uint8Array); + } +} export default MPI; diff --git a/src/type/oid.js b/src/type/oid.js index 7cc6cbcc..7316bd86 100644 --- a/src/type/oid.js +++ b/src/type/oid.js @@ -37,70 +37,69 @@ import util from '../util'; import enums from '../enums'; -/** - * @constructor - */ -function OID(oid) { - if (oid instanceof OID) { - this.oid = oid.oid; - } else if (util.isArray(oid) || - util.isUint8Array(oid)) { - oid = new Uint8Array(oid); - if (oid[0] === 0x06) { // DER encoded oid byte array - if (oid[1] !== oid.length - 2) { - throw new Error('Length mismatch in DER encoded oid'); +class OID { + constructor(oid) { + if (oid instanceof OID) { + this.oid = oid.oid; + } else if (util.isArray(oid) || + util.isUint8Array(oid)) { + oid = new Uint8Array(oid); + if (oid[0] === 0x06) { // DER encoded oid byte array + if (oid[1] !== oid.length - 2) { + throw new Error('Length mismatch in DER encoded oid'); + } + oid = oid.subarray(2); } - oid = oid.subarray(2); + this.oid = oid; + } else { + this.oid = ''; } - this.oid = oid; - } else { - this.oid = ''; } -} -/** - * Method to read an OID object - * @param {Uint8Array} input Where to read the OID from - * @returns {Number} Number of read bytes - */ -OID.prototype.read = function (input) { - if (input.length >= 1) { - const length = input[0]; - if (input.length >= 1 + length) { - this.oid = input.subarray(1, 1 + length); - return 1 + this.oid.length; + /** + * Method to read an OID object + * @param {Uint8Array} input Where to read the OID from + * @returns {Number} Number of read bytes + */ + read(input) { + if (input.length >= 1) { + const length = input[0]; + if (input.length >= 1 + length) { + this.oid = input.subarray(1, 1 + length); + return 1 + this.oid.length; + } } + throw new Error('Invalid oid'); } - throw new Error('Invalid oid'); -}; -/** - * Serialize an OID object - * @returns {Uint8Array} Array with the serialized value the OID - */ -OID.prototype.write = function () { - return util.concatUint8Array([new Uint8Array([this.oid.length]), this.oid]); -}; + /** + * Serialize an OID object + * @returns {Uint8Array} Array with the serialized value the OID + */ + write() { + return util.concatUint8Array([new Uint8Array([this.oid.length]), this.oid]); + } -/** - * Serialize an OID object as a hex string - * @returns {string} String with the hex value of the OID - */ -OID.prototype.toHex = function() { - return util.uint8ArrayToHex(this.oid); -}; + /** + * Serialize an OID object as a hex string + * @returns {string} String with the hex value of the OID + */ + toHex() { + return util.uint8ArrayToHex(this.oid); + } -/** - * If a known curve object identifier, return the canonical name of the curve - * @returns {string} String with the canonical name of the curve - */ -OID.prototype.getName = function() { - const hex = this.toHex(); - if (enums.curve[hex]) { - return enums.write(enums.curve, hex); - } else { - throw new Error('Unknown curve object identifier.'); + /** + * If a known curve object identifier, return the canonical name of the curve + * @returns {string} String with the canonical name of the curve + */ + getName() { + const hex = this.toHex(); + if (enums.curve[hex]) { + return enums.write(enums.curve, hex); + } else { + throw new Error('Unknown curve object identifier.'); + } } -}; +} export default OID; diff --git a/src/type/s2k.js b/src/type/s2k.js index 95ece03f..6c828883 100644 --- a/src/type/s2k.js +++ b/src/type/s2k.js @@ -36,159 +36,157 @@ import crypto from '../crypto'; import enums from '../enums.js'; import util from '../util.js'; -/** - * @constructor - */ -function S2K() { - /** @type {module:enums.hash} */ - this.algorithm = 'sha256'; - /** @type {module:enums.s2k} */ - this.type = 'iterated'; - /** @type {Integer} */ - this.c = config.s2kIterationCountByte; - /** Eight bytes of salt in a binary string. - * @type {String} - */ - this.salt = null; -} - -S2K.prototype.get_count = function () { - // Exponent bias, defined in RFC4880 - const expbias = 6; +class S2K { + constructor() { + /** @type {module:enums.hash} */ + this.algorithm = 'sha256'; + /** @type {module:enums.s2k} */ + this.type = 'iterated'; + /** @type {Integer} */ + this.c = config.s2kIterationCountByte; + /** Eight bytes of salt in a binary string. + * @type {String} + */ + this.salt = null; + } - return (16 + (this.c & 15)) << ((this.c >> 4) + expbias); -}; + get_count() { + // Exponent bias, defined in RFC4880 + const expbias = 6; -/** - * Parsing function for a string-to-key specifier ({@link https://tools.ietf.org/html/rfc4880#section-3.7|RFC 4880 3.7}). - * @param {String} input Payload of string-to-key specifier - * @returns {Integer} Actual length of the object - */ -S2K.prototype.read = function (bytes) { - let i = 0; - this.type = enums.read(enums.s2k, bytes[i++]); - this.algorithm = bytes[i++]; - if (this.type !== 'gnu') { - this.algorithm = enums.read(enums.hash, this.algorithm); + return (16 + (this.c & 15)) << ((this.c >> 4) + expbias); } - switch (this.type) { - case 'simple': - break; - - case 'salted': - this.salt = bytes.subarray(i, i + 8); - i += 8; - break; - - case 'iterated': - this.salt = bytes.subarray(i, i + 8); - i += 8; - - // Octet 10: count, a one-octet, coded value - this.c = bytes[i++]; - break; - - case 'gnu': - if (util.uint8ArrayToStr(bytes.subarray(i, i + 3)) === "GNU") { - i += 3; // GNU - const gnuExtType = 1000 + bytes[i++]; - if (gnuExtType === 1001) { - this.type = 'gnu-dummy'; - // GnuPG extension mode 1001 -- don't write secret key at all - } else { - throw new Error("Unknown s2k gnu protection mode."); - } - } else { - throw new Error("Unknown s2k type."); - } - break; + /** + * Parsing function for a string-to-key specifier ({@link https://tools.ietf.org/html/rfc4880#section-3.7|RFC 4880 3.7}). + * @param {String} input Payload of string-to-key specifier + * @returns {Integer} Actual length of the object + */ + read(bytes) { + let i = 0; + this.type = enums.read(enums.s2k, bytes[i++]); + this.algorithm = bytes[i++]; + if (this.type !== 'gnu') { + this.algorithm = enums.read(enums.hash, this.algorithm); + } - default: - throw new Error("Unknown s2k type."); - } + switch (this.type) { + case 'simple': + break; - return i; -}; + case 'salted': + this.salt = bytes.subarray(i, i + 8); + i += 8; + break; + case 'iterated': + this.salt = bytes.subarray(i, i + 8); + i += 8; -/** - * Serializes s2k information - * @returns {Uint8Array} binary representation of s2k - */ -S2K.prototype.write = function () { - if (this.type === 'gnu-dummy') { - return new Uint8Array([101, 0, ...util.strToUint8Array('GNU'), 1]); - } + // Octet 10: count, a one-octet, coded value + this.c = bytes[i++]; + break; - const arr = [new Uint8Array([enums.write(enums.s2k, this.type), enums.write(enums.hash, this.algorithm)])]; - - switch (this.type) { - case 'simple': - break; - case 'salted': - arr.push(this.salt); - break; - case 'iterated': - arr.push(this.salt); - arr.push(new Uint8Array([this.c])); - break; - case 'gnu': - throw new Error("GNU s2k type not supported."); - default: - throw new Error("Unknown s2k type."); - } + case 'gnu': + if (util.uint8ArrayToStr(bytes.subarray(i, i + 3)) === "GNU") { + i += 3; // GNU + const gnuExtType = 1000 + bytes[i++]; + if (gnuExtType === 1001) { + this.type = 'gnu-dummy'; + // GnuPG extension mode 1001 -- don't write secret key at all + } else { + throw new Error("Unknown s2k gnu protection mode."); + } + } else { + throw new Error("Unknown s2k type."); + } + break; + + default: + throw new Error("Unknown s2k type."); + } - return util.concatUint8Array(arr); -}; + return i; + } -/** - * Produces a key using the specified passphrase and the defined - * hashAlgorithm - * @param {String} passphrase Passphrase containing user input - * @returns {Uint8Array} Produced key with a length corresponding to - * hashAlgorithm hash length - */ -S2K.prototype.produce_key = async function (passphrase, numBytes) { - passphrase = util.encodeUtf8(passphrase); - const algorithm = enums.write(enums.hash, this.algorithm); + /** + * Serializes s2k information + * @returns {Uint8Array} binary representation of s2k + */ + write() { + if (this.type === 'gnu-dummy') { + return new Uint8Array([101, 0, ...util.strToUint8Array('GNU'), 1]); + } - const arr = []; - let rlength = 0; + const arr = [new Uint8Array([enums.write(enums.s2k, this.type), enums.write(enums.hash, this.algorithm)])]; - let prefixlen = 0; - while (rlength < numBytes) { - let toHash; switch (this.type) { case 'simple': - toHash = util.concatUint8Array([new Uint8Array(prefixlen), passphrase]); break; case 'salted': - toHash = util.concatUint8Array([new Uint8Array(prefixlen), this.salt, passphrase]); + arr.push(this.salt); break; - case 'iterated': { - const data = util.concatUint8Array([this.salt, passphrase]); - let datalen = data.length; - const count = Math.max(this.get_count(), datalen); - toHash = new Uint8Array(prefixlen + count); - toHash.set(data, prefixlen); - for (let pos = prefixlen + datalen; pos < count; pos += datalen, datalen *= 2) { - toHash.copyWithin(pos, prefixlen, pos); - } + case 'iterated': + arr.push(this.salt); + arr.push(new Uint8Array([this.c])); break; - } case 'gnu': throw new Error("GNU s2k type not supported."); default: throw new Error("Unknown s2k type."); } - const result = await crypto.hash.digest(algorithm, toHash); - arr.push(result); - rlength += result.length; - prefixlen++; + + return util.concatUint8Array(arr); } - return util.concatUint8Array(arr).subarray(0, numBytes); -}; + /** + * Produces a key using the specified passphrase and the defined + * hashAlgorithm + * @param {String} passphrase Passphrase containing user input + * @returns {Uint8Array} Produced key with a length corresponding to + * hashAlgorithm hash length + */ + async produce_key(passphrase, numBytes) { + passphrase = util.encodeUtf8(passphrase); + const algorithm = enums.write(enums.hash, this.algorithm); + + const arr = []; + let rlength = 0; + + let prefixlen = 0; + while (rlength < numBytes) { + let toHash; + switch (this.type) { + case 'simple': + toHash = util.concatUint8Array([new Uint8Array(prefixlen), passphrase]); + break; + case 'salted': + toHash = util.concatUint8Array([new Uint8Array(prefixlen), this.salt, passphrase]); + break; + case 'iterated': { + const data = util.concatUint8Array([this.salt, passphrase]); + let datalen = data.length; + const count = Math.max(this.get_count(), datalen); + toHash = new Uint8Array(prefixlen + count); + toHash.set(data, prefixlen); + for (let pos = prefixlen + datalen; pos < count; pos += datalen, datalen *= 2) { + toHash.copyWithin(pos, prefixlen, pos); + } + break; + } + case 'gnu': + throw new Error("GNU s2k type not supported."); + default: + throw new Error("Unknown s2k type."); + } + const result = await crypto.hash.digest(algorithm, toHash); + arr.push(result); + rlength += result.length; + prefixlen++; + } + + return util.concatUint8Array(arr).subarray(0, numBytes); + } +} export default S2K; diff --git a/src/wkd.js b/src/wkd.js index 5c8bc9d8..c0705442 100644 --- a/src/wkd.js +++ b/src/wkd.js @@ -26,59 +26,60 @@ import util from './util'; import crypto from './crypto'; import * as keyMod from './key'; -/** - * Initialize the WKD client - * @constructor - */ -function WKD() { - this._fetch = typeof globalThis.fetch === 'function' ? globalThis.fetch : require('node-fetch'); -} +class WKD { + /** + * Initialize the WKD client + */ + constructor() { + this._fetch = typeof globalThis.fetch === 'function' ? globalThis.fetch : require('node-fetch'); + } -/** - * Search for a public key using Web Key Directory protocol. - * @param {String} options.email User's email. - * @param {Boolean} options.rawBytes Returns Uint8Array instead of parsed key. - * @returns {Promise, - * err: (Array|null)}>} The public key. - * @async - */ -WKD.prototype.lookup = async function(options) { - const fetch = this._fetch; + /** + * Search for a public key using Web Key Directory protocol. + * @param {String} options.email User's email. + * @param {Boolean} options.rawBytes Returns Uint8Array instead of parsed key. + * @returns {Promise, + * err: (Array|null)}>} The public key. + * @async + */ + async lookup(options) { + const fetch = this._fetch; - if (!options.email) { - throw new Error('You must provide an email parameter!'); - } + if (!options.email) { + throw new Error('You must provide an email parameter!'); + } - if (!util.isEmailAddress(options.email)) { - throw new Error('Invalid e-mail address.'); - } + if (!util.isEmailAddress(options.email)) { + throw new Error('Invalid e-mail address.'); + } - const [, localPart, domain] = /(.*)@(.*)/.exec(options.email); - const localEncoded = util.encodeZBase32(await crypto.hash.sha1(util.strToUint8Array(localPart.toLowerCase()))); + const [, localPart, domain] = /(.*)@(.*)/.exec(options.email); + const localEncoded = util.encodeZBase32(await crypto.hash.sha1(util.strToUint8Array(localPart.toLowerCase()))); - const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`; - const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`; + const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`; + const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`; - let response; - try { - response = await fetch(urlAdvanced); - if (response.status !== 200) { - throw new Error('Advanced WKD lookup failed: ' + response.statusText); - } - } catch (err) { - util.print_debug_error(err); - response = await fetch(urlDirect); - if (response.status !== 200) { - throw new Error('Direct WKD lookup failed: ' + response.statusText); + let response; + try { + response = await fetch(urlAdvanced); + if (response.status !== 200) { + throw new Error('Advanced WKD lookup failed: ' + response.statusText); + } + } catch (err) { + util.printDebugError(err); + response = await fetch(urlDirect); + if (response.status !== 200) { + throw new Error('Direct WKD lookup failed: ' + response.statusText); + } } - } - const rawBytes = new Uint8Array(await response.arrayBuffer()); - if (options.rawBytes) { - return rawBytes; + const rawBytes = new Uint8Array(await response.arrayBuffer()); + if (options.rawBytes) { + return rawBytes; + } + return keyMod.readAll(rawBytes); } - return keyMod.readAll(rawBytes); -}; +} export default WKD; diff --git a/test/general/key.js b/test/general/key.js index 5ff8808a..e8a68f0a 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2806,7 +2806,7 @@ module.exports = () => describe('Key', function() { it('makeDummy() - the converted key can be parsed', async function() { const { key: key } = await openpgp.generateKey({ userIds: 'dummy ' }); key.primaryKey.makeDummy(); - const parsedKeys = (await openpgp.key.readArmored(key.armor())).keys; + const parsedKeys = await openpgp.key.readArmored(key.armor()); expect(parsedKeys).to.not.be.empty; });