From 7b9d10383e90a8beaef05d29ceca20dd89a87e46 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:27:26 +0200 Subject: [PATCH] `generateForwardingMaterial`: proxy all encryption keys, add `doesKeySupportForwarding` Previously, we only proxied the first encryption subkey. Now, the generated forwardee key will include a subkey for each encryption key of the forwarder key. Multiple proxy parameters will also be returned. Due to the change, we enforce that all encryption [sub]keys are compatible with forwarding. --- lib/key/forwarding.ts | 112 ++++++++++++++++--------- lib/pmcrypto.js | 2 +- test/key/forwarding.spec.ts | 160 +++++++++++++++++++++++++++++++++++- 3 files changed, 229 insertions(+), 45 deletions(-) diff --git a/lib/key/forwarding.ts b/lib/key/forwarding.ts index e852c671..31fd3fed 100644 --- a/lib/key/forwarding.ts +++ b/lib/key/forwarding.ts @@ -1,4 +1,4 @@ -import { KDFParams, KeyID, PrivateKey, UserID, SecretSubkeyPacket, MaybeArray } from '../openpgp'; +import { KDFParams, PrivateKey, UserID, SecretSubkeyPacket, MaybeArray, Subkey, config as defaultConfig } from '../openpgp'; import { generateKey, reformatKey } from './utils'; // TODO (investigate): top-level import of BigIntegerInterface causes issues in Jest tests in web-clients; @@ -26,7 +26,10 @@ const getBigInteger = async () => { return BigIntegerInterface; }; -export async function computeProxyParameter(forwarderSecret: Uint8Array, forwardeeSecret: Uint8Array) { +export async function computeProxyParameter( + forwarderSecret: Uint8Array, + forwardeeSecret: Uint8Array +): Promise { const BigInteger = await getBigInteger(); const dB = BigInteger.new(forwarderSecret); @@ -37,61 +40,90 @@ export async function computeProxyParameter(forwarderSecret: Uint8Array, forward return proxyParameter; } +async function getEncryptionKeysForForwarding(forwarderKey: PrivateKey) { + const curveName = 'curve25519'; + const forwarderEncryptionKeys = await forwarderKey.getDecryptionKeys( + undefined, + undefined, + undefined, + { ...defaultConfig, allowInsecureDecryptionWithSigningKeys: false } + ) as any as (PrivateKey | Subkey)[]; // TODO wrong TS defintion for `getDecryptionKeys` + + if (forwarderEncryptionKeys.some((forwarderSubkey) => ( + !forwarderSubkey || + !forwarderSubkey.isDecrypted() || + forwarderSubkey.getAlgorithmInfo().algorithm !== 'ecdh' || + forwarderSubkey.getAlgorithmInfo().curve !== curveName + ))) { + throw new Error('One or more encryption key packets are unsuitable for forwarding'); + } + + return forwarderEncryptionKeys; +} + +/** + * Whether the given key can be used as input to `generateForwardingMaterial` to setup forwarding. + */ +export const doesKeySupportForwarding = (forwarderKey: PrivateKey) => ( + getEncryptionKeysForForwarding(forwarderKey) + .then((keys) => keys.length > 0) + .catch(() => false) +); + /** * Generate a forwarding key for the final recipient ('forwardee'), as well as the corresponding proxy parameter, * needed to transform the forwarded ciphertext. - * The key in input must be a v4 primary key and must have at least one ECDH subkey using curve25519 (legacy format). + * The key in input must be a v4 primary key and its encryption subkeys must be of type ECDH curve25519 (legacy format). * @param forwarderKey - ECC primary key of original recipient * @param userIDsForForwardeeKey - user IDs for generated key - * @param subkeyID - keyID of the ECDH subKey to use for the original recipient * @returns The generated forwarding material * @async */ export async function generateForwardingMaterial( forwarderKey: PrivateKey, - userIDsForForwardeeKey: MaybeArray, - subkeyID?: KeyID + userIDsForForwardeeKey: MaybeArray ) { const curveName = 'curve25519'; - - // Setup subKey: find ECDH subkey to override - const forwarderSubkey = await forwarderKey.getEncryptionKey(subkeyID); - if ( - !forwarderSubkey || - !forwarderSubkey.isDecrypted() || - forwarderSubkey.getAlgorithmInfo().algorithm !== 'ecdh' || - forwarderSubkey.getAlgorithmInfo().curve !== curveName - ) { - throw new Error('Could not find a suitable ECDH encryption key packet'); - } - const forwarderSubkeyPacket = forwarderSubkey.keyPacket as SecretSubkeyPacket; // this is necessarily an encryption subkey (ECDH keys cannot sign) - - const { privateKey: forwardeeKeyToSetup } = await generateKey({ type: 'ecc', userIDs: userIDsForForwardeeKey, format: 'object' }); - const forwardeeSubkeyPacket = forwardeeKeyToSetup.subkeys[0].keyPacket as SecretSubkeyPacket; - - // Add KDF params for forwarding - // @ts-ignore missing publicParams definition - const { hash, cipher } = forwardeeSubkeyPacket.publicParams.kdfParams; - // @ts-ignore missing publicParams definition - forwardeeSubkeyPacket.publicParams.kdfParams = new KDFParams({ - version: 0xFF, - hash, - cipher, - replacementFingerprint: forwarderSubkeyPacket.getFingerprintBytes()!.subarray(0, 20) + const forwarderEncryptionKeys = await getEncryptionKeysForForwarding(forwarderKey); + const { privateKey: forwardeeKeyToSetup } = await generateKey({ + type: 'ecc', + userIDs: userIDsForForwardeeKey, + subkeys: new Array(forwarderEncryptionKeys.length).fill({ curve: curveName }), + format: 'object' }); + // Setup forwardee encryption subkeys and generated corresponding proxy params + const proxyParameters = await Promise.all(forwarderEncryptionKeys.map(async (forwarderSubkey, i) => { + + const forwarderSubkeyPacket = forwarderSubkey.keyPacket as SecretSubkeyPacket; + const forwardeeSubkeyPacket = forwardeeKeyToSetup.subkeys[i].keyPacket as SecretSubkeyPacket; + + // Add KDF params for forwarding + // @ts-ignore missing publicParams definition + const { hash, cipher } = forwardeeSubkeyPacket.publicParams.kdfParams; + // @ts-ignore missing publicParams definition + forwardeeSubkeyPacket.publicParams.kdfParams = new KDFParams({ + version: 0xFF, + hash, + cipher, + replacementFingerprint: forwarderSubkeyPacket.getFingerprintBytes()!.subarray(0, 20) + }); + + // Generate proxy factor k (server secret) + const proxyParameter = await computeProxyParameter( + // @ts-ignore privateParams fields are not defined + forwarderSubkeyPacket.privateParams!.d, + // @ts-ignore privateParams fields are not defined + forwardeeSubkeyPacket.privateParams!.d + ); + + return proxyParameter; + })); + // Update subkey binding signatures to account for updated KDF params const { privateKey: finalForwardeeKey } = await reformatKey({ privateKey: forwardeeKeyToSetup, userIDs: userIDsForForwardeeKey, format: 'object' }); - // Generate proxy factor k (server secret) - const proxyParameter = await computeProxyParameter( - // @ts-ignore privateParams fields are not defined - forwarderSubkeyPacket.privateParams!.d, - // @ts-ignore privateParams fields are not defined - forwardeeSubkeyPacket.privateParams!.d - ); - - return { proxyParameter, forwardeeKey: finalForwardeeKey }; + return { proxyParameters, forwardeeKey: finalForwardeeKey }; } diff --git a/lib/pmcrypto.js b/lib/pmcrypto.js index b45dedca..dc80f890 100644 --- a/lib/pmcrypto.js +++ b/lib/pmcrypto.js @@ -33,7 +33,7 @@ export { getMatchingKey } from './key/utils'; -export { generateForwardingMaterial } from './key/forwarding'; +export { generateForwardingMaterial, doesKeySupportForwarding } from './key/forwarding'; export { decryptSessionKey } from './key/decrypt'; export { encryptKey, encryptSessionKey } from './key/encrypt'; diff --git a/test/key/forwarding.spec.ts b/test/key/forwarding.spec.ts index 491b2dac..d9b3abb1 100644 --- a/test/key/forwarding.spec.ts +++ b/test/key/forwarding.spec.ts @@ -3,8 +3,8 @@ import { ec as EllipticCurve } from 'elliptic'; import BN from 'bn.js'; import { enums, KeyID, PacketList } from '../../lib/openpgp'; -import { generateKey, generateForwardingMaterial, encryptMessage, decryptMessage, readMessage, readKey } from '../../lib'; -import { computeProxyParameter } from '../../lib/key/forwarding'; +import { generateKey, generateForwardingMaterial, encryptMessage, decryptMessage, readMessage, readKey, readPrivateKey } from '../../lib'; +import { computeProxyParameter, doesKeySupportForwarding } from '../../lib/key/forwarding'; import { hexStringToArray, concatArrays } from '../../lib/utils'; // this is only intended for testing purposes, due to BN.js dependency, which is huge @@ -70,15 +70,43 @@ describe('forwarding', () => { ).to.deep.equal(bobSubkey.keyPacket.getFingerprintBytes()); }); + it('generate forwarding key - should throw for P256 encryption key', async () => { + const keyWithP256Subkey = await readPrivateKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xXcEZNI7SRMIKoZIzj0DAQcCAwRNbEFVQ7/5dkZsMEObzf2bL6bYLg7UmbOL +nC8LG9BWIfEmTH3QNOO2IuJDRyF/WmqpoNXQBuO7Emophg+23x1WAAD+JRQA +cUMAXKtqmey7d06r7EHIYyE/dgZeGo/z0WKmmjcO5M0OPHRlc3RAdGVzdC5p +dD7CiQQQEwgAOwWCZNI7SQMLCQcJkLi5pXUe27CfAxUICgIWAAIZAQKbAwIe +ARYhBEJtG+YOG/wgLGeeOri5pXUe27CfAADxkAEA4dh2u60jIlRo5yMwSBeb +nDEuRrt4M1XNs78OgDkHv0QBALrQuKGEP7UVo5O6Vr0ah91O5VAcC9XxwjtY +xl1CersLx3sEZNI7SRIIKoZIzj0DAQcCAwQTk1ESj08ix1DHXGW4ZQ5KiQNi +KL3z6+KiYnjEDNjsPtH4o0FHS6d5zUmEXZ1xqbGcOmOKZ8YgKyNklYu3T5g1 +AwEIBwABAKdySgrgktTT86zgFJRkxpPkNDhMRFpBj9APRJZE1NhlEIPCeAQY +EwgAKgWCZNI7SQmQuLmldR7bsJ8CmwwWIQRCbRvmDhv8ICxnnjq4uaV1Htuw +nwAAwa4BAPslluPut3qHU2h7PB+D93ttxCn/AhSgOc5lUOafZt2VAP91FuPa +8ziVOrUmQTj0eOBjfW0XYIlm7JTERrRlh5S8R8ddBGTSO0kSCisGAQQBl1UB +BQEBB0CrsfLaOT7JAcwc2vg36SSJ6YCXODfvudM9INHNA3kxcQMBCAcAAP9h +0r01q6Jz/KvfNkJXzkvfaAfXOe6GfrFs10QvTvjpwBL4wngEGBMIACoFgmTS +O0kJkLi5pXUe27CfApsMFiEEQm0b5g4b/CAsZ546uLmldR7bsJ8AAGnuAQCF +lAWga4MJBiFLbBiYD7248zu+xmvUAWBU7f/dkHenYAD+K8UCcwQrqeDhCl0q +z5FbOJXSHsoez1SZ7GKgoxC+X0w= +-----END PGP PRIVATE KEY BLOCK-----` }); + + await expect( + generateForwardingMaterial(keyWithP256Subkey, [{ name: 'Charlie', email: 'info@charlie.com' }]) + ).to.be.rejectedWith(/unsuitable for forwarding/); + }); + it('decryption with forwarding - v4 key', async () => { const { privateKey: bobKey } = await generateKey({ userIDs: [{ name: 'Bob', email: 'info@bob.com' }], curve: 'curve25519', format: 'object' }); const plaintext = 'Hello Bob, hello world'; - const { proxyParameter, forwardeeKey: charlieKey } = await generateForwardingMaterial(bobKey, [ + const { proxyParameters, forwardeeKey: charlieKey } = await generateForwardingMaterial(bobKey, [ { name: 'Charlie', email: 'info@charlie.com', comment: 'Forwarded from Bob' } ]); + expect(proxyParameters).to.have.length(1); const { message: originalCiphertext } = await encryptMessage({ textData: plaintext, @@ -87,7 +115,7 @@ describe('forwarding', () => { const transformedCiphertext = await testProxyTransform( originalCiphertext, - proxyParameter, + proxyParameters[0], bobKey.subkeys[0].getKeyID(), charlieKey.subkeys[0].getKeyID() ); @@ -104,4 +132,128 @@ describe('forwarding', () => { }); expect(decryptionTrialPromise).to.be.rejectedWith(/Session key decryption failed/); }); + + it('decryption with forwarding - v4 key with multiple subkeys', async () => { + const { privateKey: bobKey } = await generateKey({ + curve: 'curve25519', + userIDs: [{ name: 'Bob', email: 'info@bob.com' }], + subkeys: [{}, { sign: true }, {}], // ensure that signing subkey creates no issues + format: 'object' + }); + const plaintext = 'Hello Bob, hello world'; + + const { proxyParameters, forwardeeKey: charlieKey } = await generateForwardingMaterial(bobKey, [ + { name: 'Charlie', email: 'info@charlie.com', comment: 'Forwarded from Bob' } + ]); + expect(proxyParameters).to.have.length(2); + + // test first encryption subkey + const { message: originalCiphertext1 } = await encryptMessage({ + textData: plaintext, + encryptionKeys: bobKey + }); + const transformedCiphertext1 = await testProxyTransform( + originalCiphertext1, + proxyParameters[0], + bobKey.subkeys[0].getKeyID(), + charlieKey.subkeys[0].getKeyID() + ); + const { data: decryptedData1 } = await decryptMessage({ + message: await readMessage({ armoredMessage: transformedCiphertext1 }), + decryptionKeys: charlieKey + }); + expect(decryptedData1).to.equal(plaintext); + + // test second encryption subkey + // @ts-ignore missing `clone` definition + const bobKeySecondEncryptionKey = bobKey.clone(); + bobKeySecondEncryptionKey.subkeys = [bobKey.subkeys[2]]; // keep second encryption subkey only + + const { message: originalCiphertext2 } = await encryptMessage({ + textData: plaintext, + encryptionKeys: bobKeySecondEncryptionKey + }); + const transformedCiphertext2 = await testProxyTransform( + originalCiphertext2, + proxyParameters[1], + bobKey.subkeys[2].getKeyID(), + charlieKey.subkeys[1].getKeyID() + ); + const { data: decryptedData2 } = await decryptMessage({ + message: await readMessage({ armoredMessage: transformedCiphertext2 }), + decryptionKeys: charlieKey + }); + expect(decryptedData2).to.equal(plaintext); + }); + + it('supports forwarding - should return false for key without encryption subkeys', async () => { + // key one signing subkey (eddsa) and no encryption subkey + const keyWithoutEncryptionSubkey = await readPrivateKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xXcEZNI7/RMIKoZIzj0DAQcCAwSNFSvjFVNYffbO5XUFvFJ5xESepuHQLQnh +W/tojHJokrXUDoJVAFsuY75WQazg+tijE9lwsqWoHfmx2+ON701pAAEAjlRL +J4b3p99h5PtitDhJ7oOsJ53/NBRnB9WEaWe/B3AQJM0OPHRlc3RAdGVzdC5p +dD7CiQQQEwgAOwWCZNI7/QMLCQcJkOysb71imEtxAxUICgIWAAIZAQKbAwIe +ARYhBDL/D8Jh/QrgN2oaheysb71imEtxAABgZgD/Y1SgPcpCNYjXE6Bl4W2p +VoGwWTQw5v4mfiHSK7qIBD8BAMJh3Yy4JLcOFrP1nHniSqTofzV7/WIhbC4S +4X6P0OJzx3cEZNI7/RMIKoZIzj0DAQcCAwTAEms2toyTFVJxVcVfaR1PTgXF +5b+NPxup3KIl76V0pVnj2MLo9ybrT9FmUtcPpnv0yPbupth574+cmjPuKUad +AAD/YqNbylQRJ1piWpcI49IuTM6ziVFDVYgEn0DnfwqmEI0OqsLALwQYEwgA +oQWCZNI7/QmQ7KxvvWKYS3ECmwJ2oAQZEwgAJwWCZNI7/QmQu8ngLvN7JUsW +IQSYVaNgBswfa0ijOye7yeAu83slSwAAnisA/35TkgN/YOzx7xmuyEB9gU3C +8QamMZYvYNSE3RcyS+fdAQCsrPkmzfOGiRoklhYfw/kVrePu8ZBkWYkv5t8M +tJ0UnxYhBDL/D8Jh/QrgN2oaheysb71imEtxAAASQQEA7Y/Kqi5PO0ippJWt +WVQQHpRSfwBq7E9MwabhzSONxcgA/iosiBLv2PRyLGLdr4Jv3U40c4UK/4vk +yhtWgu8zFVCg +-----END PGP PRIVATE KEY BLOCK-----` }); + expect(await doesKeySupportForwarding(keyWithoutEncryptionSubkey)).to.be.false; + }); + + it('supports forwarding - should return false for encryption subkey without private key material (gnu-dummy)', async () => { + // key with two ecdh subkeys, one of which does not have private key material (gnu-dummy) + const keyWithDummySubkey = await readPrivateKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZNI5VxYJKwYBBAHaRw8BAQdAuT2PU1Ud1ouGL/M3IDL0T8Id7VCnJdli +W9kOy7uaYH8AAQC7PMst8kOBnhJr0zWjoKXBiACvWDoS7fy/4qbokPT3BxHn +zQ48dGVzdEB0ZXN0Lml0PsKJBBAWCgA7BYJk0jlXAwsJBwmQ0c3MqmerEnQD +FQgKAhYAAhkBApsDAh4BFiEEUAye1Mg4OC7HGN8X0c3MqmerEnQAADqMAP9f +8C71XjonSBjBX/itYIyzD7Hys6FvKukPwZLCg5bzaAEA9/6uSeuYaDLPzOpI +Cn4d/8Z7O8bDWD3dKKn7mNYNYgjHXQRk0jlXEgorBgEEAZdVAQUBAQdAFT64 +s/Pg0veAEzjTmVJVC3qRG2tOLi55CZOeyhLXw20DAQgHAAD/RXHA/5cxbVUm +Y1+kAEgqbMni8ZNx0sKt4gBzoyI8M5gQasJ4BBgWCAAqBYJk0jlXCZDRzcyq +Z6sSdAKbDBYhBFAMntTIODguxxjfF9HNzKpnqxJ0AADD3wD/dC0/pPOOR4bW +n2L4G+VVB8Do/2vGvmlqsDBQEovc9hIBAMa0R31jAD+HaIMmlYGSitA3tfPF +Lo07Y7/piuY3Uh8Ox0AEZNI5VxIKKwYBBAGXVQEFAQEHQJ3zUcuB2xyrZ8gj +wD3yBLsmig1s+V3zNWJPET9C9YcjAwEIB/4JZQBHTlUBwngEGBYIACoFgmTS +OVcJkNHNzKpnqxJ0ApsMFiEEUAye1Mg4OC7HGN8X0c3MqmerEnQAACNMAP90 +AxcnmfGjsJHMfjS4Bm7THR5NtVAdCsjjBnJKABbE/wEA5Rqdw2rwo14iIXR4 +qEAteMrSvyBNSgSuY4BIpZJNygI= +-----END PGP PRIVATE KEY BLOCK-----` }); + expect(await doesKeySupportForwarding(keyWithDummySubkey)).to.be.false; + }); + + it('supports forwarding - should return false if an encryption subkey is NIST p256', async () => { + // two ecdh subkeys: one curve25519, one p256 + const keyWithP256Subkey = await readPrivateKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xXcEZNI7SRMIKoZIzj0DAQcCAwRNbEFVQ7/5dkZsMEObzf2bL6bYLg7UmbOL +nC8LG9BWIfEmTH3QNOO2IuJDRyF/WmqpoNXQBuO7Emophg+23x1WAAD+JRQA +cUMAXKtqmey7d06r7EHIYyE/dgZeGo/z0WKmmjcO5M0OPHRlc3RAdGVzdC5p +dD7CiQQQEwgAOwWCZNI7SQMLCQcJkLi5pXUe27CfAxUICgIWAAIZAQKbAwIe +ARYhBEJtG+YOG/wgLGeeOri5pXUe27CfAADxkAEA4dh2u60jIlRo5yMwSBeb +nDEuRrt4M1XNs78OgDkHv0QBALrQuKGEP7UVo5O6Vr0ah91O5VAcC9XxwjtY +xl1CersLx3sEZNI7SRIIKoZIzj0DAQcCAwQTk1ESj08ix1DHXGW4ZQ5KiQNi +KL3z6+KiYnjEDNjsPtH4o0FHS6d5zUmEXZ1xqbGcOmOKZ8YgKyNklYu3T5g1 +AwEIBwABAKdySgrgktTT86zgFJRkxpPkNDhMRFpBj9APRJZE1NhlEIPCeAQY +EwgAKgWCZNI7SQmQuLmldR7bsJ8CmwwWIQRCbRvmDhv8ICxnnjq4uaV1Htuw +nwAAwa4BAPslluPut3qHU2h7PB+D93ttxCn/AhSgOc5lUOafZt2VAP91FuPa +8ziVOrUmQTj0eOBjfW0XYIlm7JTERrRlh5S8R8ddBGTSO0kSCisGAQQBl1UB +BQEBB0CrsfLaOT7JAcwc2vg36SSJ6YCXODfvudM9INHNA3kxcQMBCAcAAP9h +0r01q6Jz/KvfNkJXzkvfaAfXOe6GfrFs10QvTvjpwBL4wngEGBMIACoFgmTS +O0kJkLi5pXUe27CfApsMFiEEQm0b5g4b/CAsZ546uLmldR7bsJ8AAGnuAQCF +lAWga4MJBiFLbBiYD7248zu+xmvUAWBU7f/dkHenYAD+K8UCcwQrqeDhCl0q +z5FbOJXSHsoez1SZ7GKgoxC+X0w= +-----END PGP PRIVATE KEY BLOCK-----` }); + expect(await doesKeySupportForwarding(keyWithP256Subkey)).to.be.false; + }); });