mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-10-14 00:59:29 +00:00
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:
parent
f77ed0c0ed
commit
7e382e6e43
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user