From 7e382e6e43d9867b3c3ccb3d04849743fde37b3a Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 17 Mar 2023 18:41:34 +0100 Subject: [PATCH] Add support for PKESK v6 Also, set version in PKESK constructor to null, requiring to explicitly set all fields. Co-authored-by: Lukas Burkhalter --- src/crypto/crypto.js | 2 +- src/message.js | 29 +++++-- src/openpgp.js | 2 +- .../public_key_encrypted_session_key.js | 84 +++++++++++++++---- test/general/openpgp.js | 34 ++++++++ test/general/packet.js | 2 + 6 files changed, 124 insertions(+), 29 deletions(-) diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index aa6e6f34..fa252a09 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -121,7 +121,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, const { A } = publicKeyParams; const { k } = privateKeyParams; const { ephemeralPublicKey, C } = sessionKeyParams; - if (!util.isAES(C.algorithm)) { + if (C.algorithm !== null && !util.isAES(C.algorithm)) { throw new Error('AES session key expected'); } return publicKey.elliptic.ecdhX.decrypt( diff --git a/src/message.js b/src/message.js index fac3eb96..ce103022 100644 --- a/src/message.js +++ b/src/message.js @@ -108,8 +108,6 @@ export class Message { * @async */ async decrypt(decryptionKeys, passwords, sessionKeys, date = new Date(), config = defaultConfig) { - const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, date, config); - const symEncryptedPacketlist = this.packets.filterByTag( enums.packet.symmetricallyEncryptedData, enums.packet.symEncryptedIntegrityProtectedData, @@ -121,14 +119,18 @@ export class Message { } const symEncryptedPacket = symEncryptedPacketlist[0]; + const expectedSymmetricAlgorithm = symEncryptedPacket.cipherAlgorithm; + + const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, expectedSymmetricAlgorithm, date, config); + let exception = null; const decryptedPromise = Promise.all(sessionKeyObjects.map(async ({ algorithm: algorithmName, data }) => { - if (!util.isUint8Array(data) || !util.isString(algorithmName)) { + if (!util.isUint8Array(data) || (!symEncryptedPacket.cipherAlgorithm && !util.isString(algorithmName))) { throw new Error('Invalid session key for decryption.'); } try { - const algo = enums.write(enums.symmetric, algorithmName); + const algo = symEncryptedPacket.cipherAlgorithm || enums.write(enums.symmetric, algorithmName); await symEncryptedPacket.decrypt(algo, data, config); } catch (e) { util.printDebugError(e); @@ -154,6 +156,7 @@ export class Message { * Decrypt encrypted session keys either with private keys or passwords. * @param {Array} [decryptionKeys] - Private keys with decrypted secret data * @param {Array} [passwords] - Passwords used to decrypt + * @param {enums.symmetric} [expectedSymmetricAlgorithm] - The symmetric algorithm the SEIPDv2 / AEAD packet is encrypted with (if applicable) * @param {Date} [date] - Use the given date for key verification, instead of current time * @param {Object} [config] - Full configuration, defaults to openpgp.config * @returns {Promise>} array of object with potential sessionKey, algorithm pairs * @async */ - async decryptSessionKeys(decryptionKeys, passwords, date = new Date(), config = defaultConfig) { + async decryptSessionKeys(decryptionKeys, passwords, expectedSymmetricAlgorithm, date = new Date(), config = defaultConfig) { let decryptedSessionKeyPackets = []; let exception; @@ -260,7 +263,8 @@ export class Message { } else { try { await pkeskPacket.decrypt(decryptionKeyPacket); - if (!algos.includes(enums.write(enums.symmetric, pkeskPacket.sessionKeyAlgorithm))) { + const symmetricAlgorithm = expectedSymmetricAlgorithm || pkeskPacket.sessionKeyAlgorithm; + if (symmetricAlgorithm && !algos.includes(enums.write(enums.symmetric, symmetricAlgorithm))) { throw new Error('A non-preferred symmetric algorithm was used.'); } decryptedSessionKeyPackets.push(pkeskPacket); @@ -294,7 +298,7 @@ export class Message { return decryptedSessionKeyPackets.map(packet => ({ data: packet.sessionKey, - algorithm: enums.read(enums.symmetric, packet.sessionKeyAlgorithm) + algorithm: packet.sessionKeyAlgorithm && enums.read(enums.symmetric, packet.sessionKeyAlgorithm) })); } throw exception || new Error('Session key decryption failed.'); @@ -350,7 +354,7 @@ export class Message { await Promise.all(encryptionKeys.map(key => key.getEncryptionKey() .catch(() => null) // ignore key strength requirements .then(maybeKey => { - if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519) && !util.isAES(symmetricAlgo)) { + if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519) && !aeadAlgoName && !util.isAES(symmetricAlgo)) { // if AEAD is defined, then PKESK v6 are used, and the algo info is encrypted throw new Error('Could not generate a session key compatible with the given `encryptionKeys`: X22519 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.'); } }) @@ -430,7 +434,14 @@ export class Message { const results = await Promise.all(encryptionKeys.map(async function(primaryKey, i) { const encryptionKey = await primaryKey.getEncryptionKey(encryptionKeyIDs[i], date, userIDs, config); const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket(); - pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID(); + if (aeadAlgorithm) { + pkESKeyPacket.version = 6; + pkESKeyPacket.publicKeyVersion = wildcard ? 0 : encryptionKey.keyPacket.version; + pkESKeyPacket.publicKeyFingerprint = wildcard ? null : encryptionKey.keyPacket.getFingerprintBytes(); + } else { + pkESKeyPacket.version = 3; + pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID(); + } pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm; pkESKeyPacket.sessionKey = sessionKey; pkESKeyPacket.sessionKeyAlgorithm = algorithm; diff --git a/src/openpgp.js b/src/openpgp.js index 223ebfdf..70dfd240 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -591,7 +591,7 @@ export async function decryptSessionKeys({ message, decryptionKeys, passwords, d const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`); try { - const sessionKeys = await message.decryptSessionKeys(decryptionKeys, passwords, date, config); + const sessionKeys = await message.decryptSessionKeys(decryptionKeys, passwords, undefined, date, config); return sessionKeys; } catch (err) { throw util.wrapError('Error decrypting session keys', err); diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 231c62c7..02828b5e 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -21,8 +21,6 @@ import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; -const VERSION = 3; - /** * Public-Key Encrypted Session Key Packets (Tag 1) * @@ -45,9 +43,16 @@ class PublicKeyEncryptedSessionKeyPacket { } constructor() { - this.version = 3; + this.version = null; + // For version 3: this.publicKeyID = new KeyID(); + + // For version 6: + this.publicKeyVersion = null; + this.publicKeyFingerprint = null; + + // For all versions: this.publicKeyAlgorithm = null; this.sessionKey = null; @@ -67,15 +72,39 @@ class PublicKeyEncryptedSessionKeyPacket { * @param {Uint8Array} bytes - Payload of a tag 1 packet */ read(bytes) { - let i = 0; - this.version = bytes[i++]; - if (this.version !== VERSION) { + let offset = 0; + this.version = bytes[offset++]; + if (this.version !== 3 && this.version !== 6) { throw new UnsupportedError(`Version ${this.version} of the PKESK packet is unsupported.`); } - i += this.publicKeyID.read(bytes.subarray(i)); - this.publicKeyAlgorithm = bytes[i++]; - this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(i), this.version); - if (this.publicKeyAlgorithm === enums.publicKey.x25519) { + if (this.version === 6) { + // A one-octet size of the following two fields: + // - A one octet key version number. + // - The fingerprint of the public key or subkey to which the session key is encrypted. + // The size may also be zero. + const versionAndFingerprintLength = bytes[offset++]; + if (versionAndFingerprintLength) { + this.publicKeyVersion = bytes[offset++]; + const fingerprintLength = versionAndFingerprintLength - 1; + this.publicKeyFingerprint = bytes.subarray(offset, offset + fingerprintLength); offset += fingerprintLength; + if (this.publicKeyVersion >= 5) { + // For v5/6 the Key ID is the high-order 64 bits of the fingerprint. + this.publicKeyID.read(this.publicKeyFingerprint); + } else { + // For v4 The Key ID is the low-order 64 bits of the fingerprint. + this.publicKeyID.read(this.publicKeyFingerprint.subarray(-8)); + } + } else { + // The size may also be zero, and the key version and + // fingerprint omitted for an "anonymous recipient" + this.publicKeyID = KeyID.wildcard(); + } + } else { + offset += this.publicKeyID.read(bytes.subarray(offset, offset + 8)); + } + this.publicKeyAlgorithm = bytes[offset++]; + this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset)); + if (this.version === 3 && this.publicKeyAlgorithm === enums.publicKey.x25519) { this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); } } @@ -87,11 +116,27 @@ class PublicKeyEncryptedSessionKeyPacket { */ write() { const arr = [ - new Uint8Array([this.version]), - this.publicKeyID.write(), + new Uint8Array([this.version]) + ]; + + if (this.version === 6) { + if (this.publicKeyFingerprint !== null) { + arr.push(new Uint8Array([ + this.publicKeyFingerprint.length + 1, + this.publicKeyVersion] + )); + arr.push(this.publicKeyFingerprint); + } else { + arr.push(new Uint8Array([0])); + } + } else { + arr.push(this.publicKeyID.write()); + } + + arr.push( new Uint8Array([this.publicKeyAlgorithm]), crypto.serializeParams(this.publicKeyAlgorithm, this.encrypted) - ]; + ); return util.concatUint8Array(arr); } @@ -131,7 +176,7 @@ class PublicKeyEncryptedSessionKeyPacket { const { sessionKey, sessionKeyAlgorithm } = decodeSessionKey(this.version, this.publicKeyAlgorithm, decryptedData, randomSessionKey); // v3 Montgomery curves have cleartext cipher algo - if (this.publicKeyAlgorithm !== enums.publicKey.x25519) { + if (this.version === 3 && this.publicKeyAlgorithm !== enums.publicKey.x25519) { this.sessionKeyAlgorithm = sessionKeyAlgorithm; } this.sessionKey = sessionKey; @@ -149,7 +194,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { case enums.publicKey.ecdh: { // add checksum return util.concatUint8Array([ - new Uint8Array([cipherAlgo]), + new Uint8Array(version === 6 ? [] : [cipherAlgo]), sessionKeyData, util.writeChecksum(sessionKeyData.subarray(sessionKeyData.length % 8)) ]); @@ -173,7 +218,9 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { const checksum = decryptedData.subarray(decryptedData.length - 2); const computedChecksum = util.writeChecksum(result.subarray(result.length % 8)); const isValidChecksum = computedChecksum[0] === checksum[0] & computedChecksum[1] === checksum[1]; - const decryptedSessionKey = { sessionKeyAlgorithm: result[0], sessionKey: result.subarray(1) }; + const decryptedSessionKey = version === 6 ? + { sessionKeyAlgorithm: null, sessionKey: result } : + { sessionKeyAlgorithm: result[0], sessionKey: result.subarray(1) }; if (randomSessionKey) { // We must not leak info about the validity of the decrypted checksum or cipher algo. // The decrypted session key must be of the same algo and size as the random session key, otherwise we discard it and use the random data. @@ -182,14 +229,15 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { decryptedSessionKey.sessionKey.length === randomSessionKey.sessionKey.length; return { sessionKey: util.selectUint8Array(isValidPayload, decryptedSessionKey.sessionKey, randomSessionKey.sessionKey), - sessionKeyAlgorithm: util.selectUint8( + sessionKeyAlgorithm: version === 6 ? null : util.selectUint8( isValidPayload, decryptedSessionKey.sessionKeyAlgorithm, randomSessionKey.sessionKeyAlgorithm ) }; } else { - const isValidPayload = isValidChecksum && enums.read(enums.symmetric, decryptedSessionKey.sessionKeyAlgorithm); + const isValidPayload = isValidChecksum && ( + version === 6 || enums.read(enums.symmetric, decryptedSessionKey.sessionKeyAlgorithm)); if (isValidPayload) { return decryptedSessionKey; } else { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 16a2c949..69260be9 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -1311,6 +1311,40 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu await expect(openpgp.decrypt(decOpt)).to.be.rejectedWith('Error decrypting message: Decryption key is not decrypted.'); }); + it('should decrypt test vector X25519-AEAD-OCB (PKESK v6, SEIPDv2)', async function() { + // test vector https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-10.html#appendix-A.8 + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO +WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS +aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l +yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo +bhF30A+IitsxxA== +-----END PGP MESSAGE-----`; + + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB +exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ +BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh +RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe +7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/ +LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG +GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE +M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr +k0mXubZvyl4GBg== +-----END PGP PRIVATE KEY BLOCK-----` }); + + const { data: decryptedData } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey + }); + + expect(decryptedData).to.equal('Hello, world!'); + }); + it('decrypt/verify should succeed with valid signature (expectSigned=true)', async function () { const publicKey = await openpgp.readKey({ armoredKey: pub_key }); const privateKey = await openpgp.decryptKey({ diff --git a/test/general/packet.js b/test/general/packet.js index 11397a45..0e1cdaab 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -474,6 +474,7 @@ export default () => describe('Packet', function() { return crypto.generateParams(rsa, keySize, 65537).then(function({ publicParams, privateParams }) { const enc = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + enc.version = 3; const msg = new openpgp.PacketList(); const msg2 = new openpgp.PacketList(); @@ -523,6 +524,7 @@ export default () => describe('Packet', function() { key = key[0]; const enc = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + enc.version = 3; const secret = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); enc.sessionKey = secret;