Skip to content

Commit

Permalink
generateForwardingMaterial: proxy all encryption keys, add `doesKey…
Browse files Browse the repository at this point in the history
…SupportForwarding`

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.
  • Loading branch information
larabr committed Aug 8, 2023
1 parent 893dd89 commit 7b9d103
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 45 deletions.
112 changes: 72 additions & 40 deletions lib/key/forwarding.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Uint8Array> {
const BigInteger = await getBigInteger();

const dB = BigInteger.new(forwarderSecret);
Expand All @@ -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<UserID>,
subkeyID?: KeyID
userIDsForForwardeeKey: MaybeArray<UserID>
) {
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 };
}
2 changes: 1 addition & 1 deletion lib/pmcrypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
160 changes: 156 additions & 4 deletions test/key/forwarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -87,7 +115,7 @@ describe('forwarding', () => {

const transformedCiphertext = await testProxyTransform(
originalCiphertext,
proxyParameter,
proxyParameters[0],
bobKey.subkeys[0].getKeyID(),
charlieKey.subkeys[0].getKeyID()
);
Expand All @@ -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;
});
});

0 comments on commit 7b9d103

Please sign in to comment.