diff --git a/src/cleartext.js b/src/cleartext.js index c4fea507..ea2eba64 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -66,29 +66,31 @@ export class CleartextMessage { * 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 {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] * @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)); + async sign(privateKeys, signature = null, signingKeyIds = [], date = new Date(), userIds = []) { + return new CleartextMessage(this.text, await this.signDetached(privateKeys, signature, signingKeyIds, date, userIds)); } /** * 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 {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] * @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 = []) { + async signDetached(privateKeys, signature = null, signingKeyIds = [], date = new Date(), userIds = []) { const literalDataPacket = new LiteralDataPacket(); literalDataPacket.setText(this.text); - return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, true)); + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, signingKeyIds, date, userIds, true)); } /** diff --git a/src/message.js b/src/message.js index 4c09391e..e48d458e 100644 --- a/src/message.js +++ b/src/message.js @@ -304,17 +304,18 @@ export class Message { /** * 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 + * @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 {Array} encryptionKeyIds (optional) array of key IDs to use for encryption. Each encryptionKeyIds[i] corresponds to publicKeys[i] + * @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) { + async encrypt(keys, passwords, sessionKey, wildcard = false, encryptionKeyIds = [], date = new Date(), userIds = [], streaming) { if (sessionKey) { if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { throw new Error('Invalid session key for encryption.'); @@ -329,7 +330,7 @@ export class Message { const { data: sessionKeyData, algorithm, aeadAlgorithm } = sessionKey; - const msg = await Message.encryptSessionKey(sessionKeyData, algorithm, aeadAlgorithm, keys, passwords, wildcard, date, userIds); + const msg = await Message.encryptSessionKey(sessionKeyData, algorithm, aeadAlgorithm, keys, passwords, wildcard, encryptionKeyIds, date, userIds); let symEncryptedPacket; if (aeadAlgorithm) { @@ -351,23 +352,24 @@ export class Message { /** * 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 + * @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 {Array} encryptionKeyIds (optional) array of key IDs to use for encryption. Each encryptionKeyIds[i] corresponds to publicKeys[i] + * @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 = []) { + static async encryptSessionKey(sessionKey, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard = false, encryptionKeyIds = [], 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 results = await Promise.all(publicKeys.map(async function(publicKey, i) { + const encryptionKey = await publicKey.getEncryptionKey(encryptionKeyIds[i], date, userIds); const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket(); pkESKeyPacket.publicKeyId = wildcard ? type_keyid.wildcard() : encryptionKey.getKeyId(); pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm; @@ -420,15 +422,16 @@ export class Message { /** * 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 + * @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 {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] + * @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) { + async sign(privateKeys = [], signature = null, signingKeyIds = [], date = new Date(), userIds = [], streaming = false) { const packetlist = new PacketList(); const literalDataPacket = this.packets.findPacket(enums.packet.literalData); @@ -462,7 +465,8 @@ export class Message { if (privateKey.isPublic()) { throw new Error('Need private key for signing'); } - const signingKey = await privateKey.getSigningKey(undefined, date, userIds); + const signingKeyId = signingKeyIds[privateKeys.length - 1 - i]; + const signingKey = await privateKey.getSigningKey(signingKeyId, date, userIds); const onePassSig = new OnePassSignaturePacket(); onePassSig.signatureType = signatureType; onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, signingKey.keyPacket, date, userIds); @@ -477,7 +481,7 @@ export class Message { }); packetlist.push(literalDataPacket); - packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, date, userIds, false, streaming)); + packetlist.concat(await createSignaturePackets(literalDataPacket, privateKeys, signature, signingKeyIds, date, userIds, false, streaming)); return new Message(packetlist); } @@ -506,18 +510,19 @@ export class Message { * 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 {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] * @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) { + async signDetached(privateKeys = [], signature = null, signingKeyIds = [], 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)); + return new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, signingKeyIds, date, userIds, true, streaming)); } /** @@ -689,6 +694,7 @@ export class Message { * @param {LiteralDataPacket} literalDataPacket the literal data packet to sign * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature to append + * @param {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] * @param {Date} date (optional) override the creationtime of the signature * @param {Array} userIds (optional) user IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] * @param {Boolean} detached (optional) whether to create detached signature packets @@ -696,7 +702,7 @@ export class Message { * @returns {Promise} list of signature packets * @async */ -export async function createSignaturePackets(literalDataPacket, privateKeys, signature = null, date = new Date(), userIds = [], detached = false, streaming = false) { +export async function createSignaturePackets(literalDataPacket, privateKeys, signature = null, signingKeyIds = [], date = new Date(), userIds = [], detached = false, streaming = false) { const packetlist = new PacketList(); // If data packet was created from Uint8Array, use binary, otherwise use text @@ -708,7 +714,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig if (privateKey.isPublic()) { throw new Error('Need private key for signing'); } - const signingKey = await privateKey.getSigningKey(undefined, date, userId); + const signingKey = await privateKey.getSigningKey(signingKeyIds[i], date, userId); return createSignaturePacket(literalDataPacket, privateKey, signingKey.keyPacket, { signatureType }, date, userId, detached, streaming); })).then(signatureList => { signatureList.forEach(signaturePacket => packetlist.push(signaturePacket)); diff --git a/src/openpgp.js b/src/openpgp.js index a6777114..23702a88 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -240,24 +240,26 @@ export async function encryptKey({ privateKey, passphrase }) { /** * Encrypts message text/data with public keys, passwords or both at once. At least either public keys or passwords * must be specified. If private keys are specified, those will be used to sign the message. - * @param {Message} message message to be encrypted as created by openpgp.Message.fromText or openpgp.Message.fromBinary - * @param {Key|Array} publicKeys (optional) array of keys or single key, used to encrypt the message - * @param {Key|Array} privateKeys (optional) private keys for signing. If omitted message will not be signed - * @param {String|Array} passwords (optional) array of passwords or a single password to encrypt the message - * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String } - * @param {module:enums.compression} compression (optional) which compression algorithm to compress the message with, defaults to what is specified in config - * @param {Boolean} armor (optional) whether the return values should be ascii armored (true, the default) or binary (false) - * @param {'web'|'ponyfill'|'node'|false} streaming (optional) whether to return data as a stream. Defaults to the type of stream `message` was created from, if any. - * @param {Signature} signature (optional) a detached signature to add to the encrypted message - * @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 message signature - * @param {Array} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] - * @param {Array} toUserIds (optional) array of user IDs to encrypt for, one per key in `publicKeys`, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] + * @param {Message} message message to be encrypted as created by openpgp.Message.fromText or openpgp.Message.fromBinary + * @param {Key|Array} publicKeys (optional) array of keys or single key, used to encrypt the message + * @param {Key|Array} privateKeys (optional) private keys for signing. If omitted message will not be signed + * @param {String|Array} passwords (optional) array of passwords or a single password to encrypt the message + * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String } + * @param {module:enums.compression} compression (optional) which compression algorithm to compress the message with, defaults to what is specified in config + * @param {Boolean} armor (optional) whether the return values should be ascii armored (true, the default) or binary (false) + * @param {'web'|'ponyfill'|'node'|false} streaming (optional) whether to return data as a stream. Defaults to the type of stream `message` was created from, if any. + * @param {Signature} signature (optional) a detached signature to add to the encrypted message + * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs + * @param {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] + * @param {Array} encryptionKeyIds (optional) array of key IDs to use for encryption. Each encryptionKeyIds[i] corresponds to publicKeys[i] + * @param {Date} date (optional) override the creation date of the message signature + * @param {Array} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] + * @param {Array} toUserIds (optional) array of user IDs to encrypt for, one per key in `publicKeys`, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }] * @returns {Promise|NodeStream|Uint8Array|ReadableStream|NodeStream>} (String if `armor` was true, the default; Uint8Array if `armor` was false) * @async * @static */ -export function encrypt({ message, publicKeys, privateKeys, passwords, sessionKey, compression = config.compression, armor = true, streaming = message && message.fromStream, detached = false, signature = null, wildcard = false, date = new Date(), fromUserIds = [], toUserIds = [] }) { +export function encrypt({ message, publicKeys, privateKeys, passwords, sessionKey, compression = config.compression, armor = true, streaming = message && message.fromStream, detached = false, signature = null, wildcard = false, signingKeyIds = [], encryptionKeyIds = [], date = new Date(), fromUserIds = [], toUserIds = [] }) { checkMessage(message); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); fromUserIds = toArray(fromUserIds); toUserIds = toArray(toUserIds); if (detached) { throw new Error("detached option has been removed from openpgp.encrypt. Separately call openpgp.sign instead. Don't forget to remove privateKeys option as well."); @@ -268,10 +270,10 @@ export function encrypt({ message, publicKeys, privateKeys, passwords, sessionKe privateKeys = []; } if (privateKeys.length || signature) { // sign the message only if private keys or signature is specified - message = await message.sign(privateKeys, signature, date, fromUserIds, message.fromStream); + message = await message.sign(privateKeys, signature, signingKeyIds, date, fromUserIds, message.fromStream); } message = message.compress(compression); - message = await message.encrypt(publicKeys, passwords, sessionKey, wildcard, date, toUserIds, streaming); + message = await message.encrypt(publicKeys, passwords, sessionKey, wildcard, encryptionKeyIds, date, toUserIds, streaming); const data = armor ? message.armor() : message.write(); return convertStream(data, streaming, armor ? 'utf8' : 'binary'); }).catch(onError.bind(null, 'Error encrypting message')); @@ -340,13 +342,14 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe * @param {Boolean} armor (optional) whether the return values should be ascii armored (true, the default) or binary (false) * @param {'web'|'ponyfill'|'node'|false} streaming (optional) whether to return data as a stream. Defaults to the type of stream `message` was created from, if any. * @param {Boolean} detached (optional) if the return value should contain a detached signature + * @param {Array} signingKeyIds (optional) array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i] * @param {Date} date (optional) override the creation date of the signature * @param {Array} fromUserIds (optional) array of user IDs to sign with, one per key in `privateKeys`, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }] * @returns {Promise|NodeStream|Uint8Array|ReadableStream|NodeStream>} (String if `armor` was true, the default; Uint8Array if `armor` was false) * @async * @static */ -export function sign({ message, privateKeys, armor = true, streaming = message && message.fromStream, detached = false, date = new Date(), fromUserIds = [] }) { +export function sign({ message, privateKeys, armor = true, streaming = message && message.fromStream, detached = false, signingKeyIds = [], date = new Date(), fromUserIds = [] }) { checkCleartextOrMessage(message); if (message instanceof CleartextMessage && !armor) throw new Error("Can't sign non-armored cleartext message"); if (message instanceof CleartextMessage && detached) throw new Error("Can't sign detached cleartext message"); @@ -355,9 +358,9 @@ export function sign({ message, privateKeys, armor = true, streaming = message & return Promise.resolve().then(async function() { let signature; if (detached) { - signature = await message.signDetached(privateKeys, undefined, date, fromUserIds, message.fromStream); + signature = await message.signDetached(privateKeys, undefined, signingKeyIds, date, fromUserIds, message.fromStream); } else { - signature = await message.sign(privateKeys, undefined, date, fromUserIds, message.fromStream); + signature = await message.sign(privateKeys, undefined, signingKeyIds, date, fromUserIds, message.fromStream); } signature = armor ? signature.armor() : signature.write(); if (detached) { @@ -441,25 +444,26 @@ export function generateSessionKey({ publicKeys, date = new Date(), toUserIds = /** * Encrypt a symmetric session key with public keys, passwords, or both at once. At least either public keys * or passwords must be specified. - * @param {Uint8Array} data the session key to be encrypted e.g. 16 random bytes (for aes128) - * @param {String} algorithm algorithm of the symmetric session key e.g. 'aes128' or 'aes256' - * @param {String} aeadAlgorithm (optional) aead algorithm, e.g. 'eax' or 'ocb' - * @param {Key|Array} publicKeys (optional) array of public keys or single key, used to encrypt the key - * @param {String|Array} passwords (optional) passwords for the message - * @param {Boolean} armor (optional) whether the return values should be ascii armored (true, the default) or binary (false) - * @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} toUserIds (optional) array of user IDs to encrypt for, one per key in `publicKeys`, e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }] + * @param {Uint8Array} data the session key to be encrypted e.g. 16 random bytes (for aes128) + * @param {String} algorithm algorithm of the symmetric session key e.g. 'aes128' or 'aes256' + * @param {String} aeadAlgorithm (optional) aead algorithm, e.g. 'eax' or 'ocb' + * @param {Key|Array} publicKeys (optional) array of public keys or single key, used to encrypt the key + * @param {String|Array} passwords (optional) passwords for the message + * @param {Boolean} armor (optional) whether the return values should be ascii armored (true, the default) or binary (false) + * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs + * @param {Array} encryptionKeyIds (optional) array of key IDs to use for encryption. Each encryptionKeyIds[i] corresponds to publicKeys[i] + * @param {Date} date (optional) override the date + * @param {Array} toUserIds (optional) array of user IDs to encrypt for, one per key in `publicKeys`, e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }] * @returns {Promise} (String if `armor` was true, the default; Uint8Array if `armor` was false) * @async * @static */ -export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, passwords, armor = true, wildcard = false, date = new Date(), toUserIds = [] }) { +export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, passwords, armor = true, wildcard = false, encryptionKeyIds = [], date = new Date(), toUserIds = [] }) { checkBinary(data); checkString(algorithm, 'algorithm'); publicKeys = toArray(publicKeys); passwords = toArray(passwords); toUserIds = toArray(toUserIds); return Promise.resolve().then(async function() { - const message = await Message.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard, date, toUserIds); + const message = await Message.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard, encryptionKeyIds, date, toUserIds); return armor ? message.armor() : message.write(); }).catch(onError.bind(null, 'Error encrypting session key')); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 7ee1a6f9..a46116bb 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -5,6 +5,7 @@ const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp const crypto = require('../../src/crypto'); const random = require('../../src/crypto/random'); const util = require('../../src/util'); +const keyIdType = require('../../src/type/keyid'); const spy = require('sinon/lib/sinon/spy'); const input = require('./testInputs.js'); @@ -544,6 +545,120 @@ mNG2ibD6lftLOtDsVSDY8a6a -----END PGP PRIVATE KEY BLOCK----- `; +const multipleEncryptionAndSigningSubkeys = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=PUAJ +-----END PGP PRIVATE KEY BLOCK-----`; + function withCompression(tests) { const compressionTypes = Object.keys(openpgp.enums.compression).map(k => openpgp.enums.compression[k]); @@ -2596,6 +2711,90 @@ amnR6g== }); }); + + describe('Specific encryption/signing key testing', async function () { + const encryptionKeyIds = [ + keyIdType.fromId("87EAE0977B2185EA"), + keyIdType.fromId("F94F9B34AF93FA14"), + keyIdType.fromId("08F7D4C7C59545C0") + ]; + const signingKeyIds = [ + keyIdType.fromId("663277AF60400638"), + keyIdType.fromId("BBE14491E6EE6366"), + keyIdType.fromId("3E0F20F1A71D6DFD") + ]; + const getPrimaryKey = async () => openpgp.readArmoredKey( + multipleEncryptionAndSigningSubkeys + ); + + it('Encrypt message with a specific encryption key id', async function () { + const primaryKey = await getPrimaryKey(); + let m; + let p; + for (let i = 0; i < encryptionKeyIds.length; i++) { + m = await openpgp.readArmoredMessage(await openpgp.encrypt({ + message: openpgp.Message.fromText("Hello World\n"), + publicKeys: primaryKey, + encryptionKeyIds: [encryptionKeyIds[i]] + })); + p = m.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey); + expect(p.length).equals(1); + expect(p[0].publicKeyId.equals(encryptionKeyIds[i])).to.be.true; + } + }); + + it('Sign message with a specific signing key id', async function () { + const primaryKey = await getPrimaryKey(); + let s; + let p; + for (let i = 0; i < signingKeyIds.length; i++) { + s = await openpgp.readArmoredSignature(await openpgp.sign({ + message: openpgp.Message.fromText("Hello World\n"), + privateKeys: primaryKey, + signingKeyIds: [signingKeyIds[i]], + detached: true + })); + p = s.packets.filterByTag(openpgp.enums.packet.signature); + expect(p.length).equals(1); + expect(p[0].issuerKeyId.equals(signingKeyIds[i])).to.be.true; + } + }); + + it('Encrypt and sign with specific encryption/signing key ids', async function () { + const primaryKey = await getPrimaryKey(); + const plaintextMessage = openpgp.Message.fromText("Hello World\n"); + + const checkEncryptedPackets = (encryptionKeyIds, pKESKList) => { + pKESKList.forEach(({ publicKeyId }, i) => { + expect(publicKeyId.equals(encryptionKeyIds[i])).to.be.true; + }); + }; + const checkSignatures = (signingKeyIds, signatures) => { + signatures.forEach(({ keyid }, i) => { + expect(keyid.equals(signingKeyIds[i])).to.be.true; + }); + }; + + const kIds = [encryptionKeyIds[1], encryptionKeyIds[0], encryptionKeyIds[2]]; + const sIds = [signingKeyIds[2], signingKeyIds[1], signingKeyIds[0]]; + const message = await openpgp.readArmoredMessage(await openpgp.encrypt({ + message: plaintextMessage, + privateKeys: [primaryKey, primaryKey, primaryKey], + publicKeys: [primaryKey, primaryKey, primaryKey], + encryptionKeyIds: kIds, + signingKeyIds: sIds + })); + const pKESKList = message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey); + expect(pKESKList.length).equals(3); + checkEncryptedPackets(kIds, pKESKList); + const { signatures } = await openpgp.decrypt({ + message, + privateKeys: [primaryKey, primaryKey, primaryKey] + }); + expect(signatures.length).equals(3); + checkSignatures(sIds, signatures); + }); + }); }); });