From 95014e91eafae3af10352b05697c893155d851ff Mon Sep 17 00:00:00 2001 From: larabr Date: Mon, 15 May 2023 18:07:39 +0200 Subject: [PATCH] Update support for decrypting autoforwarded messages (#6) Update implementation to reflect spec changes to KDF params (v2 -> v255) and new forwarding-related key flag (0x40). --- openpgp.d.ts | 13 ++++ src/config/config.js | 7 ++ src/crypto/public_key/elliptic/ecdh.js | 2 +- src/enums.js | 2 + src/index.js | 2 + src/key/helper.js | 3 +- src/type/kdf_params.js | 52 +++++++-------- test/crypto/ecdh.js | 89 ++++++-------------------- test/general/forwarding.js | 66 +++++++++++-------- test/general/key.js | 2 +- 10 files changed, 110 insertions(+), 128 deletions(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index 2b5509b43..f6d5aaace 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -881,6 +881,7 @@ export namespace enums { encryptStorage = 8, splitPrivateKey = 16, authentication = 32, + forwardedCommunication = 64, sharedPrivateKey = 128, } @@ -925,3 +926,15 @@ export namespace enums { gnu = 101 } } + +interface KDFParamsData { + version: number; + hash: enums.hash; + cipher: enums.symmetric; + replacementFingerprint?: Uint8Array; +} + +export class KDFParams { + constructor(data: KDFParamsData); + write(forReplacementParams?: boolean): Uint8Array; +} diff --git a/src/config/config.js b/src/config/config.js index fe944d7aa..0eca6de75 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -138,6 +138,13 @@ export default { * @property {Boolean} allowUnauthenticatedStream */ allowUnauthenticatedStream: false, + /** + * Allow decrypting forwarded messages, using keys with 0x40 ('forwarded communication') flag. + * Note: this is related to a **non-standard feature**. + * @memberof module:config + * @property {Boolean} allowForwardedMessages + */ + allowForwardedMessages: false, /** * Minimum RSA key size allowed for key generation and message signing, verification and encryption. * The default is 2047 since due to a bug, previous versions of OpenPGP.js could generate 2047-bit keys instead of 2048-bit ones. diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 357f08c2e..ed05f7ebd 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -51,7 +51,7 @@ function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) { return util.concatUint8Array([ oid.write(), new Uint8Array([public_algo]), - kdfParams.replacementKDFParams || kdfParams.write(), + kdfParams.write(true), util.stringToUint8Array('Anonymous Sender '), kdfParams.replacementFingerprint || fingerprint.subarray(0, 20) ]); diff --git a/src/enums.js b/src/enums.js index 4adbe7fd1..8b191216a 100644 --- a/src/enums.js +++ b/src/enums.js @@ -430,6 +430,8 @@ export default { splitPrivateKey: 16, /** 0x20 - This key may be used for authentication. */ authentication: 32, + /** This key may be used for forwarded communications */ + forwardedCommunication: 64, /** 0x80 - The private component of this key may be in the * possession of more than one person. */ sharedPrivateKey: 128 diff --git a/src/index.js b/src/index.js index 75b320a8d..6d9f5cb71 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,8 @@ export { CleartextMessage, readCleartextMessage, createCleartextMessage } from ' export * from './packet'; +export { default as KDFParams } from './type/kdf_params'; + export * from './encoding/armor'; export { default as enums } from './enums'; diff --git a/src/key/helper.js b/src/key/helper.js index 47e56c2b7..32bd0512f 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -436,7 +436,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) { return !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.encryptCommunication) !== 0 || - (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0; + (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0 || + (config.allowForwardedMessages && (signature.keyFlags[0] & enums.keyFlags.forwardedCommunication) !== 0); } default: return false; diff --git a/src/type/kdf_params.js b/src/type/kdf_params.js index 240fb433b..7a3724742 100644 --- a/src/type/kdf_params.js +++ b/src/type/kdf_params.js @@ -28,34 +28,29 @@ import { UnsupportedError } from '../packet/packet'; * @module type/kdf_params */ import util from '../util'; -import enums from '../enums'; + +const VERSION_FORWARDING = 0xFF; class KDFParams { /** * @param {Integer} version Version, defaults to 1 * @param {enums.hash} hash Hash algorithm * @param {enums.symmetric} cipher Symmetric algorithm - * @param {enums.kdfFlags} flags (v2 only) flags - * @param {Uint8Array} replacementFingerprint (v2 only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) - * @param {Uint8Array} replacementKDFParams (v2 only) serialized KDF params to use in KDF digest computation + * @param {Uint8Array} replacementFingerprint (forwarding only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) */ constructor(data) { if (data) { - const { version, hash, cipher, flags, replacementFingerprint, replacementKDFParams } = data; + const { version, hash, cipher, replacementFingerprint } = data; this.version = version || 1; this.hash = hash; this.cipher = cipher; - this.flags = flags; this.replacementFingerprint = replacementFingerprint; - this.replacementKDFParams = replacementKDFParams; } else { this.version = null; this.hash = null; this.cipher = null; - this.flags = null; this.replacementFingerprint = null; - this.replacementKDFParams = null; } } @@ -65,47 +60,44 @@ class KDFParams { * @returns {Number} Number of read bytes. */ read(input) { - if (input.length < 4 || (input[1] !== 1 && input[1] !== 2)) { + if (input.length < 4 || (input[1] !== 1 && input[1] !== VERSION_FORWARDING)) { throw new UnsupportedError('Cannot read KDFParams'); } + const totalBytes = input[0]; this.version = input[1]; this.hash = input[2]; this.cipher = input[3]; let readBytes = 4; - if (this.version === 2) { - this.flags = input[readBytes++]; - if (this.flags & enums.kdfFlags.replace_fingerprint) { - this.replacementFingerprint = input.slice(readBytes, readBytes + 20); - readBytes += 20; - } - if (this.flags & enums.kdfFlags.replace_kdf_params) { - const fieldLength = input[readBytes] + 1; // account for length - this.replacementKDFParams = input.slice(readBytes, readBytes + fieldLength); - readBytes += fieldLength; - } + if (this.version === VERSION_FORWARDING) { + const fingerprintLength = totalBytes - readBytes + 1; // acount for length byte + this.replacementFingerprint = input.slice(readBytes, readBytes + fingerprintLength); + readBytes += fingerprintLength; } return readBytes; } /** * Write KDFParams to an Uint8Array + * @param {Boolean} [forReplacementParams] - forwarding only: whether to serialize data to use for replacement params * @returns {Uint8Array} Array with the KDFParams value */ - write() { - if (!this.version || this.version === 1) { + write(forReplacementParams) { + if (!this.version || this.version === 1 || forReplacementParams) { return new Uint8Array([3, 1, this.hash, this.cipher]); } - const v2Fields = util.concatUint8Array([ - new Uint8Array([4, 2, this.hash, this.cipher, this.flags]), - this.replacementFingerprint || new Uint8Array(), - this.replacementKDFParams || new Uint8Array() + const forwardingFields = util.concatUint8Array([ + new Uint8Array([ + 3 + this.replacementFingerprint.length, + this.version, + this.hash, + this.cipher + ]), + this.replacementFingerprint ]); - // update length field - v2Fields[0] = v2Fields.length - 1; - return new Uint8Array(v2Fields); + return forwardingFields; } } diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 24cdad28c..5f9d3dc6e 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -267,78 +267,31 @@ export default () => describe('ECDH key exchange @lightweight', function () { }); }); }); -}); - -describe('KDF parameters', function () { - const fingerprint = new Uint8Array([ - 177, 183, 116, 123, 76, 133, 245, 212, 151, 243, - 236, 71, 245, 86, 3, 168, 101, 74, 209, 105 - ]); - - it('Valid serialization', async function () { - const cipher = openpgp.enums.symmetric.aes256; - const hash = openpgp.enums.hash.sha256; - - const v1 = new KDFParams({ cipher, hash }); - const v1Copy = new KDFParams({}); - v1Copy.read(v1.write()); - expect(v1Copy).to.deep.equal(v1); - - const v1Flags0x0 = new KDFParams({ - cipher, - hash, - flags: 0x0 // discarded - }); - const v1Flags0x0Copy = new KDFParams({}); - v1Flags0x0Copy.read(v1Flags0x0.write()); - v1Flags0x0.flags = undefined; - expect(v1Flags0x0Copy).to.deep.equal(v1Flags0x0); - - const v2Flags0x3 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x3, - replacementFingerprint: fingerprint, - replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) - }); - const v2Flags0x3Copy = new KDFParams(); - v2Flags0x3Copy.read(v2Flags0x3.write()); - expect(v2Flags0x3Copy).to.deep.equal(v2Flags0x3); - const v2Flags0x0 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x0 - }); - const v2Flags0x0Copy = new KDFParams({}); - v2Flags0x0Copy.read(v2Flags0x0.write()); + describe('KDF parameters', function () { + const fingerprint = new Uint8Array([ + 177, 183, 116, 123, 76, 133, 245, 212, 151, 243, + 236, 71, 245, 86, 3, 168, 101, 74, 209, 105 + ]); - expect(v2Flags0x0Copy).to.deep.equal(v2Flags0x0); + it('Valid serialization', async function () { + const cipher = openpgp.enums.symmetric.aes256; + const hash = openpgp.enums.hash.sha256; - const v2Flags0x1 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x1, - replacementFingerprint: fingerprint - }); - const v2Flags0x1Copy = new KDFParams(); - v2Flags0x1Copy.read(v2Flags0x1.write()); - v2Flags0x1.replacementKDFParams = null; - expect(v2Flags0x1Copy).to.deep.equal(v2Flags0x1); + const v1 = new KDFParams({ cipher, hash }); + const v1Copy = new KDFParams({}); + v1Copy.read(v1.write()); + expect(v1Copy).to.deep.equal(v1); - const v2Flags0x2 = new KDFParams({ - cipher, - hash, - version: 2, - flags: 0x2, - replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) + const forwardingFlags = new KDFParams({ + cipher, + hash, + version: 0xFF, + replacementFingerprint: fingerprint + }); + const forwardingFlagsCopy = new KDFParams(); + forwardingFlagsCopy.read(forwardingFlags.write()); + expect(forwardingFlagsCopy).to.deep.equal(forwardingFlags); }); - const v2Flags0x2Copy = new KDFParams(); - v2Flags0x2Copy.read(v2Flags0x2.write()); - v2Flags0x2.replacementFingerprint = null; - expect(v2Flags0x2Copy).to.deep.equal(v2Flags0x2); }); }); diff --git a/test/general/forwarding.js b/test/general/forwarding.js index f0aa8f471..b39fb90b8 100644 --- a/test/general/forwarding.js +++ b/test/general/forwarding.js @@ -3,41 +3,53 @@ import { expect } from 'chai'; import openpgp from '../initOpenpgp.js'; const charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- -Version: OpenPGP.js v4.10.4 -Comment: https://openpgpjs.org - -xVgEXqG7KRYJKwYBBAHaRw8BAQdA/q4cs9Pwms3R4trjUd7YyrsRYdQHC9wI -MqLdefob4KUAAQDfy9e8qleM+a1EnPCjDpm69FIY769mo/dpwYlkuI2T/RQt -zSlCb2IgKEZvcndhcmRlZCB0byBDaGFybGllKSA8aW5mb0Bib2IuY29tPsJ4 -BBAWCgAgBQJeobspBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQN2cz -+W7U/RnS8AEArtRly8vW6uUSng9EJ0iuIwJpwgZfykSLl/t4u3HTBZ4BALzY -3XsnvKtZZVvaKvFvCUu/2NvC/1yw2wJk9wGbCwEOx3YEXqG7KRIKKwYBBAGX -VQEFAQEHQCGxSJahhDUdTKnlqT3UIn3rXn5i47I4MsG4kSWfTwcOHAIIBwPe -7fJ+kOrMea9aIUeYtGpUzABa9gMBCAcAAP95QjbjU7kyugp39vhi60YW5T8p -Me0kKFCWzmSYzstgGBBbwmEEGBYIAAkFAl6huykCGwwACgkQN2cz+W7U/RkP -WQD+KcU1HKn6PkVJKxg6RS0Q7RcCZwaQ1DyEyjUoneMCRAgA/jUl9uvPAoCS -3+4Wqg9Q//zOwXNImimIPIdpWNXYZJID -=FVvG + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj -----END PGP PRIVATE KEY BLOCK-----`; const fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- -Version: OpenPGP.js v4.10.4 -Comment: https://openpgpjs.org - -wV4Dog8LAQLriGUSAQdA/I6k0IvGxyNG2SdSDHrv3bZQDWH18OhTWkcmSF0M -Bxcw3w8KMjr2v69ro5cyZztymEXi5RemRx+oPZGKIZ9N5T+26TaOltH7h8eR -Mu4H03Lp0k4BRsjpFNUBL3HsAuMIemNf4369g+szlpuzjNE1KQhQzZbh87AU -T7KAKygwz0EpOWpx2RHtshDy/bZ1EC8Ia4qDAebameIqCU929OmY1uI= -=3iIr + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa -----END PGP MESSAGE-----`; export default () => describe('Forwarding', function() { it('can decrypt forwarded ciphertext', async function() { const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); - const msg = await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }); - const result = await openpgp.decrypt({ decryptionKeys: charlieKey, message: msg }); - expect(result).to.exist; - expect(result.data).to.equal('Hello Bob, hello world'); + await expect(openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), + decryptionKeys: charlieKey + })).to.be.rejectedWith(/Error decrypting message/); + + const result = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), + decryptionKeys: charlieKey, + config: { allowForwardedMessages: true } + }); + + expect(result.data).to.equal('Message for Bob'); + }); + + it('supports serialising key with KDF params for forwarding', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + + const serializedKey = charlieKey.write(); + const { data: expectedSerializedKey } = await openpgp.unarmor(charlieKeyArmored); + expect(serializedKey).to.deep.equal(expectedSerializedKey); }); }); diff --git a/test/general/key.js b/test/general/key.js index 1b93d778e..72f812993 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -3213,7 +3213,7 @@ ruh8m7Xo2ehSSFyWRSuTSZe5tm/KXgYG } }); - it('Parsing ECDH key with unknown kdf param version', async function() { + it.skip('Parsing ECDH key with unknown kdf param version', async function() { // subkey with unknown kdfParam version 255. Parsing should not fail, the subkey should simply dropped const key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK-----