Add support for PKESK v6

Also, set version in PKESK constructor to null,
requiring to explicitly set all fields.

Co-authored-by: Lukas Burkhalter <lukas.burkhalter@proton.ch>
This commit is contained in:
Daniel Huigens 2023-03-17 18:41:34 +01:00 committed by larabr
parent f77ed0c0ed
commit 7e382e6e43
6 changed files with 124 additions and 29 deletions

View File

@ -121,7 +121,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
const { A } = publicKeyParams;
const { k } = privateKeyParams;
const { ephemeralPublicKey, C } = sessionKeyParams;
if (!util.isAES(C.algorithm)) {
if (C.algorithm !== null && !util.isAES(C.algorithm)) {
throw new Error('AES session key expected');
}
return publicKey.elliptic.ecdhX.decrypt(

View File

@ -108,8 +108,6 @@ export class Message {
* @async
*/
async decrypt(decryptionKeys, passwords, sessionKeys, date = new Date(), config = defaultConfig) {
const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, date, config);
const symEncryptedPacketlist = this.packets.filterByTag(
enums.packet.symmetricallyEncryptedData,
enums.packet.symEncryptedIntegrityProtectedData,
@ -121,14 +119,18 @@ export class Message {
}
const symEncryptedPacket = symEncryptedPacketlist[0];
const expectedSymmetricAlgorithm = symEncryptedPacket.cipherAlgorithm;
const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, expectedSymmetricAlgorithm, date, config);
let exception = null;
const decryptedPromise = Promise.all(sessionKeyObjects.map(async ({ algorithm: algorithmName, data }) => {
if (!util.isUint8Array(data) || !util.isString(algorithmName)) {
if (!util.isUint8Array(data) || (!symEncryptedPacket.cipherAlgorithm && !util.isString(algorithmName))) {
throw new Error('Invalid session key for decryption.');
}
try {
const algo = enums.write(enums.symmetric, algorithmName);
const algo = symEncryptedPacket.cipherAlgorithm || enums.write(enums.symmetric, algorithmName);
await symEncryptedPacket.decrypt(algo, data, config);
} catch (e) {
util.printDebugError(e);
@ -154,6 +156,7 @@ export class Message {
* Decrypt encrypted session keys either with private keys or passwords.
* @param {Array<PrivateKey>} [decryptionKeys] - Private keys with decrypted secret data
* @param {Array<String>} [passwords] - Passwords used to decrypt
* @param {enums.symmetric} [expectedSymmetricAlgorithm] - The symmetric algorithm the SEIPDv2 / AEAD packet is encrypted with (if applicable)
* @param {Date} [date] - Use the given date for key verification, instead of current time
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Array<{
@ -162,7 +165,7 @@ export class Message {
* }>>} array of object with potential sessionKey, algorithm pairs
* @async
*/
async decryptSessionKeys(decryptionKeys, passwords, date = new Date(), config = defaultConfig) {
async decryptSessionKeys(decryptionKeys, passwords, expectedSymmetricAlgorithm, date = new Date(), config = defaultConfig) {
let decryptedSessionKeyPackets = [];
let exception;
@ -260,7 +263,8 @@ export class Message {
} else {
try {
await pkeskPacket.decrypt(decryptionKeyPacket);
if (!algos.includes(enums.write(enums.symmetric, pkeskPacket.sessionKeyAlgorithm))) {
const symmetricAlgorithm = expectedSymmetricAlgorithm || pkeskPacket.sessionKeyAlgorithm;
if (symmetricAlgorithm && !algos.includes(enums.write(enums.symmetric, symmetricAlgorithm))) {
throw new Error('A non-preferred symmetric algorithm was used.');
}
decryptedSessionKeyPackets.push(pkeskPacket);
@ -294,7 +298,7 @@ export class Message {
return decryptedSessionKeyPackets.map(packet => ({
data: packet.sessionKey,
algorithm: enums.read(enums.symmetric, packet.sessionKeyAlgorithm)
algorithm: packet.sessionKeyAlgorithm && enums.read(enums.symmetric, packet.sessionKeyAlgorithm)
}));
}
throw exception || new Error('Session key decryption failed.');
@ -350,7 +354,7 @@ export class Message {
await Promise.all(encryptionKeys.map(key => key.getEncryptionKey()
.catch(() => null) // ignore key strength requirements
.then(maybeKey => {
if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519) && !util.isAES(symmetricAlgo)) {
if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519) && !aeadAlgoName && !util.isAES(symmetricAlgo)) { // if AEAD is defined, then PKESK v6 are used, and the algo info is encrypted
throw new Error('Could not generate a session key compatible with the given `encryptionKeys`: X22519 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.');
}
})
@ -430,7 +434,14 @@ export class Message {
const results = await Promise.all(encryptionKeys.map(async function(primaryKey, i) {
const encryptionKey = await primaryKey.getEncryptionKey(encryptionKeyIDs[i], date, userIDs, config);
const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket();
pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID();
if (aeadAlgorithm) {
pkESKeyPacket.version = 6;
pkESKeyPacket.publicKeyVersion = wildcard ? 0 : encryptionKey.keyPacket.version;
pkESKeyPacket.publicKeyFingerprint = wildcard ? null : encryptionKey.keyPacket.getFingerprintBytes();
} else {
pkESKeyPacket.version = 3;
pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID();
}
pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm;
pkESKeyPacket.sessionKey = sessionKey;
pkESKeyPacket.sessionKeyAlgorithm = algorithm;

View File

@ -591,7 +591,7 @@ export async function decryptSessionKeys({ message, decryptionKeys, passwords, d
const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`);
try {
const sessionKeys = await message.decryptSessionKeys(decryptionKeys, passwords, date, config);
const sessionKeys = await message.decryptSessionKeys(decryptionKeys, passwords, undefined, date, config);
return sessionKeys;
} catch (err) {
throw util.wrapError('Error decrypting session keys', err);

View File

@ -21,8 +21,6 @@ import enums from '../enums';
import util from '../util';
import { UnsupportedError } from './packet';
const VERSION = 3;
/**
* Public-Key Encrypted Session Key Packets (Tag 1)
*
@ -45,9 +43,16 @@ class PublicKeyEncryptedSessionKeyPacket {
}
constructor() {
this.version = 3;
this.version = null;
// For version 3:
this.publicKeyID = new KeyID();
// For version 6:
this.publicKeyVersion = null;
this.publicKeyFingerprint = null;
// For all versions:
this.publicKeyAlgorithm = null;
this.sessionKey = null;
@ -67,15 +72,39 @@ class PublicKeyEncryptedSessionKeyPacket {
* @param {Uint8Array} bytes - Payload of a tag 1 packet
*/
read(bytes) {
let i = 0;
this.version = bytes[i++];
if (this.version !== VERSION) {
let offset = 0;
this.version = bytes[offset++];
if (this.version !== 3 && this.version !== 6) {
throw new UnsupportedError(`Version ${this.version} of the PKESK packet is unsupported.`);
}
i += this.publicKeyID.read(bytes.subarray(i));
this.publicKeyAlgorithm = bytes[i++];
this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(i), this.version);
if (this.publicKeyAlgorithm === enums.publicKey.x25519) {
if (this.version === 6) {
// A one-octet size of the following two fields:
// - A one octet key version number.
// - The fingerprint of the public key or subkey to which the session key is encrypted.
// The size may also be zero.
const versionAndFingerprintLength = bytes[offset++];
if (versionAndFingerprintLength) {
this.publicKeyVersion = bytes[offset++];
const fingerprintLength = versionAndFingerprintLength - 1;
this.publicKeyFingerprint = bytes.subarray(offset, offset + fingerprintLength); offset += fingerprintLength;
if (this.publicKeyVersion >= 5) {
// For v5/6 the Key ID is the high-order 64 bits of the fingerprint.
this.publicKeyID.read(this.publicKeyFingerprint);
} else {
// For v4 The Key ID is the low-order 64 bits of the fingerprint.
this.publicKeyID.read(this.publicKeyFingerprint.subarray(-8));
}
} else {
// The size may also be zero, and the key version and
// fingerprint omitted for an "anonymous recipient"
this.publicKeyID = KeyID.wildcard();
}
} else {
offset += this.publicKeyID.read(bytes.subarray(offset, offset + 8));
}
this.publicKeyAlgorithm = bytes[offset++];
this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset));
if (this.version === 3 && this.publicKeyAlgorithm === enums.publicKey.x25519) {
this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm);
}
}
@ -87,11 +116,27 @@ class PublicKeyEncryptedSessionKeyPacket {
*/
write() {
const arr = [
new Uint8Array([this.version]),
this.publicKeyID.write(),
new Uint8Array([this.version])
];
if (this.version === 6) {
if (this.publicKeyFingerprint !== null) {
arr.push(new Uint8Array([
this.publicKeyFingerprint.length + 1,
this.publicKeyVersion]
));
arr.push(this.publicKeyFingerprint);
} else {
arr.push(new Uint8Array([0]));
}
} else {
arr.push(this.publicKeyID.write());
}
arr.push(
new Uint8Array([this.publicKeyAlgorithm]),
crypto.serializeParams(this.publicKeyAlgorithm, this.encrypted)
];
);
return util.concatUint8Array(arr);
}
@ -131,7 +176,7 @@ class PublicKeyEncryptedSessionKeyPacket {
const { sessionKey, sessionKeyAlgorithm } = decodeSessionKey(this.version, this.publicKeyAlgorithm, decryptedData, randomSessionKey);
// v3 Montgomery curves have cleartext cipher algo
if (this.publicKeyAlgorithm !== enums.publicKey.x25519) {
if (this.version === 3 && this.publicKeyAlgorithm !== enums.publicKey.x25519) {
this.sessionKeyAlgorithm = sessionKeyAlgorithm;
}
this.sessionKey = sessionKey;
@ -149,7 +194,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) {
case enums.publicKey.ecdh: {
// add checksum
return util.concatUint8Array([
new Uint8Array([cipherAlgo]),
new Uint8Array(version === 6 ? [] : [cipherAlgo]),
sessionKeyData,
util.writeChecksum(sessionKeyData.subarray(sessionKeyData.length % 8))
]);
@ -173,7 +218,9 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) {
const checksum = decryptedData.subarray(decryptedData.length - 2);
const computedChecksum = util.writeChecksum(result.subarray(result.length % 8));
const isValidChecksum = computedChecksum[0] === checksum[0] & computedChecksum[1] === checksum[1];
const decryptedSessionKey = { sessionKeyAlgorithm: result[0], sessionKey: result.subarray(1) };
const decryptedSessionKey = version === 6 ?
{ sessionKeyAlgorithm: null, sessionKey: result } :
{ sessionKeyAlgorithm: result[0], sessionKey: result.subarray(1) };
if (randomSessionKey) {
// We must not leak info about the validity of the decrypted checksum or cipher algo.
// The decrypted session key must be of the same algo and size as the random session key, otherwise we discard it and use the random data.
@ -182,14 +229,15 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) {
decryptedSessionKey.sessionKey.length === randomSessionKey.sessionKey.length;
return {
sessionKey: util.selectUint8Array(isValidPayload, decryptedSessionKey.sessionKey, randomSessionKey.sessionKey),
sessionKeyAlgorithm: util.selectUint8(
sessionKeyAlgorithm: version === 6 ? null : util.selectUint8(
isValidPayload,
decryptedSessionKey.sessionKeyAlgorithm,
randomSessionKey.sessionKeyAlgorithm
)
};
} else {
const isValidPayload = isValidChecksum && enums.read(enums.symmetric, decryptedSessionKey.sessionKeyAlgorithm);
const isValidPayload = isValidChecksum && (
version === 6 || enums.read(enums.symmetric, decryptedSessionKey.sessionKeyAlgorithm));
if (isValidPayload) {
return decryptedSessionKey;
} else {

View File

@ -1311,6 +1311,40 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu
await expect(openpgp.decrypt(decOpt)).to.be.rejectedWith('Error decrypting message: Decryption key is not decrypted.');
});
it('should decrypt test vector X25519-AEAD-OCB (PKESK v6, SEIPDv2)', async function() {
// test vector https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-10.html#appendix-A.8
const armoredMessage = `-----BEGIN PGP MESSAGE-----
wV0GIQYSyD8ecG9jCP4VGkF3Q6HwM3kOk+mXhIjR2zeNqZMIhRmHzxjV8bU/gXzO
WgBM85PMiVi93AZfJfhK9QmxfdNnZBjeo1VDeVZheQHgaVf7yopqR6W1FT6NOrfS
aQIHAgZhZBZTW+CwcW1g4FKlbExAf56zaw76/prQoN+bAzxpohup69LA7JW/Vp0l
yZnuSj3hcFj0DfqLTGgr4/u717J+sPWbtQBfgMfG9AOIwwrUBqsFE9zW+f1zdlYo
bhF30A+IitsxxA==
-----END PGP MESSAGE-----`;
const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK-----
xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB
exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ
BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh
RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe
7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/
LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG
GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE
M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr
k0mXubZvyl4GBg==
-----END PGP PRIVATE KEY BLOCK-----` });
const { data: decryptedData } = await openpgp.decrypt({
message: await openpgp.readMessage({ armoredMessage }),
decryptionKeys: privateKey
});
expect(decryptedData).to.equal('Hello, world!');
});
it('decrypt/verify should succeed with valid signature (expectSigned=true)', async function () {
const publicKey = await openpgp.readKey({ armoredKey: pub_key });
const privateKey = await openpgp.decryptKey({

View File

@ -474,6 +474,7 @@ export default () => describe('Packet', function() {
return crypto.generateParams(rsa, keySize, 65537).then(function({ publicParams, privateParams }) {
const enc = new openpgp.PublicKeyEncryptedSessionKeyPacket();
enc.version = 3;
const msg = new openpgp.PacketList();
const msg2 = new openpgp.PacketList();
@ -523,6 +524,7 @@ export default () => describe('Packet', function() {
key = key[0];
const enc = new openpgp.PublicKeyEncryptedSessionKeyPacket();
enc.version = 3;
const secret = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]);
enc.sessionKey = secret;