Skip to content

Commit

Permalink
Update support for decrypting autoforwarded messages (#6)
Browse files Browse the repository at this point in the history
Update implementation to reflect spec changes to KDF params (v2 -> v255) and
new forwarding-related key flag (0x40).
  • Loading branch information
larabr committed Oct 26, 2023
1 parent 5ab182b commit 95014e9
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 128 deletions.
13 changes: 13 additions & 0 deletions openpgp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@ export namespace enums {
encryptStorage = 8,
splitPrivateKey = 16,
authentication = 32,
forwardedCommunication = 64,
sharedPrivateKey = 128,
}

Expand Down Expand Up @@ -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;
}
7 changes: 7 additions & 0 deletions src/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/public_key/elliptic/ecdh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]);
Expand Down
2 changes: 2 additions & 0 deletions src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion src/key/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
52 changes: 22 additions & 30 deletions src/type/kdf_params.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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;
}
}

Expand Down
89 changes: 21 additions & 68 deletions test/crypto/ecdh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
66 changes: 39 additions & 27 deletions test/general/forwarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
2 changes: 1 addition & 1 deletion test/general/key.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-----
Expand Down

0 comments on commit 95014e9

Please sign in to comment.