From d8bdf2bed37312bee7651eb1139fe09d866ef027 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Fri, 19 Jun 2020 16:52:36 +0200 Subject: [PATCH] Add support for decrypting autoforwarded messages (#1) --- src/crypto/public_key/elliptic/ecdh.js | 4 +- src/enums.js | 12 +++++ src/type/kdf_params.js | 55 ++++++++++++++++--- test/crypto/ecdh.js | 74 ++++++++++++++++++++++++++ test/general/forwarding.js | 43 +++++++++++++++ test/general/index.js | 2 + 6 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 test/general/forwarding.js diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 66937b2c..b07448a8 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -50,9 +50,9 @@ function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) { return util.concatUint8Array([ oid.write(), new Uint8Array([public_algo]), - kdfParams.write(), + kdfParams.replacementKDFParams || kdfParams.write(), util.stringToUint8Array('Anonymous Sender '), - fingerprint + kdfParams.replacementFingerprint || fingerprint ]); } diff --git a/src/enums.js b/src/enums.js index 85f0a5d7..1d5b96cf 100644 --- a/src/enums.js +++ b/src/enums.js @@ -50,6 +50,18 @@ export default { 'brainpoolP512r1': 'brainpoolP512r1' }, + /** KDF parameters flags + * Non-standard extensions (for now) to allow email forwarding + * @enum {Integer} + * @readonly + */ + kdfFlags: { + /** Specify fingerprint to use instead of the recipient's */ + replace_fingerprint: 0x01, + /** Specify custom parameters to use in the KDF digest computation */ + replace_kdf_params: 0x02 + }, + /** A string to key specifier type * @enum {Integer} * @readonly diff --git a/src/type/kdf_params.js b/src/type/kdf_params.js index 84728b3c..240fb433 100644 --- a/src/type/kdf_params.js +++ b/src/type/kdf_params.js @@ -26,22 +26,36 @@ import { UnsupportedError } from '../packet/packet'; * Alternative 1) [NIST-SP800-56A] with the KDF hash function that is * SHA2-256 [FIPS-180-3] or stronger is REQUIRED. * @module type/kdf_params - * @private */ +import util from '../util'; +import enums from '../enums'; class KDFParams { /** - * @param {enums.hash} hash - Hash algorithm - * @param {enums.symmetric} cipher - Symmetric algorithm + * @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 */ constructor(data) { if (data) { - const { hash, cipher } = data; + const { version, hash, cipher, flags, replacementFingerprint, replacementKDFParams } = 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; } } @@ -51,12 +65,27 @@ class KDFParams { * @returns {Number} Number of read bytes. */ read(input) { - if (input.length < 4 || input[0] !== 3 || input[1] !== 1) { + if (input.length < 4 || (input[1] !== 1 && input[1] !== 2)) { throw new UnsupportedError('Cannot read KDFParams'); } + this.version = input[1]; this.hash = input[2]; this.cipher = input[3]; - return 4; + 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; + } + } + return readBytes; } /** @@ -64,7 +93,19 @@ class KDFParams { * @returns {Uint8Array} Array with the KDFParams value */ write() { - return new Uint8Array([3, 1, this.hash, this.cipher]); + if (!this.version || this.version === 1) { + 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() + ]); + + // update length field + v2Fields[0] = v2Fields.length - 1; + return new Uint8Array(v2Fields); } } diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 72a8892b..a56f0ee9 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -362,3 +362,77 @@ 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()); + + expect(v2Flags0x0Copy).to.deep.equal(v2Flags0x0); + + 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 v2Flags0x2 = new KDFParams({ + cipher, + hash, + version: 2, + flags: 0x2, + replacementKDFParams: new Uint8Array([3, 1, cipher, hash]) + }); + 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 new file mode 100644 index 00000000..f0aa8f47 --- /dev/null +++ b/test/general/forwarding.js @@ -0,0 +1,43 @@ +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 +-----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 +-----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'); + }); +}); diff --git a/test/general/index.js b/test/general/index.js index 478a0c5a..c7af1fe1 100644 --- a/test/general/index.js +++ b/test/general/index.js @@ -4,6 +4,7 @@ import testArmor from './armor.js'; import testPacket from './packet.js'; import testSignature from './signature.js'; import testKey from './key.js'; +import testForwarding from './forwarding.js'; import testOpenPGP from './openpgp.js'; import testConfig from './config.js'; import testOID from './oid.js'; @@ -20,6 +21,7 @@ export default () => describe('General', function () { testPacket(); testSignature(); testKey(); + testForwarding(); testOpenPGP(); testConfig(); testOID();