PrivateKey.getDecryptionKeys: throw if no decryption key is found

To avoid returning dummy key packets, and improving error reporting.
This new behavior is also better aligned with that of `Key.getSigningKey()`.

This is a breaking change for apps that call `getDecryptionKeys()` directly.
The related error messages returned by `openpgp.decrypt` have also changed,
becoming more specific.

This change is also made in preparation of supporting private keys with
public key packets (to be released in the next minor version, hence we
want to avoid breaking changes there).
This commit is contained in:
larabr 2024-08-20 16:48:51 +02:00
parent 5fd7ef370f
commit b0873eb98d
4 changed files with 39 additions and 10 deletions

View File

@ -77,28 +77,45 @@ class PrivateKey extends PublicKey {
* @param {String} userID, optional * @param {String} userID, optional
* @param {Object} [config] - Full configuration, defaults to openpgp.config * @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Array<Key|Subkey>>} Array of decryption keys. * @returns {Promise<Array<Key|Subkey>>} Array of decryption keys.
* @throws {Error} if no decryption key is found
* @async * @async
*/ */
async getDecryptionKeys(keyID, date = new Date(), userID = {}, config = defaultConfig) { async getDecryptionKeys(keyID, date = new Date(), userID = {}, config = defaultConfig) {
const primaryKey = this.keyPacket; const primaryKey = this.keyPacket;
const keys = []; const keys = [];
let exception = null;
for (let i = 0; i < this.subkeys.length; i++) { for (let i = 0; i < this.subkeys.length; i++) {
if (!keyID || this.subkeys[i].getKeyID().equals(keyID, true)) { if (!keyID || this.subkeys[i].getKeyID().equals(keyID, true)) {
if (this.subkeys[i].keyPacket.isDummy()) {
exception = exception || new Error('Gnu-dummy key packets cannot be used for decryption');
continue;
}
try { try {
const dataToVerify = { key: primaryKey, bind: this.subkeys[i].keyPacket }; const dataToVerify = { key: primaryKey, bind: this.subkeys[i].keyPacket };
const bindingSignature = await helper.getLatestValidSignature(this.subkeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config); const bindingSignature = await helper.getLatestValidSignature(this.subkeys[i].bindingSignatures, primaryKey, enums.signature.subkeyBinding, dataToVerify, date, config);
if (helper.validateDecryptionKeyPacket(this.subkeys[i].keyPacket, bindingSignature, config)) { if (helper.validateDecryptionKeyPacket(this.subkeys[i].keyPacket, bindingSignature, config)) {
keys.push(this.subkeys[i]); keys.push(this.subkeys[i]);
} }
} catch (e) {} } catch (e) {
exception = e;
}
} }
} }
// evaluate primary key // evaluate primary key
const selfCertification = await this.getPrimarySelfSignature(date, userID, config); const selfCertification = await this.getPrimarySelfSignature(date, userID, config);
if ((!keyID || primaryKey.getKeyID().equals(keyID, true)) && if ((!keyID || primaryKey.getKeyID().equals(keyID, true)) && helper.validateDecryptionKeyPacket(primaryKey, selfCertification, config)) {
helper.validateDecryptionKeyPacket(primaryKey, selfCertification, config)) { if (primaryKey.isDummy()) {
keys.push(this); exception = exception || new Error('Gnu-dummy key packets cannot be used for decryption');
} else {
keys.push(this);
}
}
if (keys.length === 0) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw exception || new Error('No decryption key packets found');
} }
return keys; return keys;

View File

@ -199,6 +199,15 @@ export class Message {
} }
await Promise.all(pkeskPackets.map(async function(pkeskPacket) { await Promise.all(pkeskPackets.map(async function(pkeskPacket) {
await Promise.all(decryptionKeys.map(async function(decryptionKey) { await Promise.all(decryptionKeys.map(async function(decryptionKey) {
let decryptionKeyPackets;
try {
// do not check key expiration to allow decryption of old messages
decryptionKeyPackets = (await decryptionKey.getDecryptionKeys(pkeskPacket.publicKeyID, null, undefined, config)).map(key => key.keyPacket);
} catch (err) {
exception = err;
return;
}
let algos = [ let algos = [
enums.symmetric.aes256, // Old OpenPGP.js default fallback enums.symmetric.aes256, // Old OpenPGP.js default fallback
enums.symmetric.aes128, // RFC4880bis fallback enums.symmetric.aes128, // RFC4880bis fallback
@ -212,8 +221,6 @@ export class Message {
} }
} catch (e) {} } catch (e) {}
// do not check key expiration to allow decryption of old messages
const decryptionKeyPackets = (await decryptionKey.getDecryptionKeys(pkeskPacket.publicKeyID, null, undefined, config)).map(key => key.keyPacket);
await Promise.all(decryptionKeyPackets.map(async function(decryptionKeyPacket) { await Promise.all(decryptionKeyPackets.map(async function(decryptionKeyPacket) {
if (!decryptionKeyPacket || decryptionKeyPacket.isDummy()) { if (!decryptionKeyPacket || decryptionKeyPacket.isDummy()) {
return; return;

View File

@ -3541,7 +3541,7 @@ PzIEeL7UH3trraFmi+Gq8u4kAA==
await expect(openpgp.decrypt({ await expect(openpgp.decrypt({
message: await openpgp.readMessage({ armoredMessage: encryptedRsaSignOnly }), message: await openpgp.readMessage({ armoredMessage: encryptedRsaSignOnly }),
decryptionKeys: key decryptionKeys: key
})).to.be.rejectedWith(/Session key decryption failed/); })).to.be.rejectedWith(/No decryption key packets found/);
await expect(openpgp.decrypt({ await expect(openpgp.decrypt({
message: await openpgp.readMessage({ armoredMessage: encryptedRsaSignOnly }), message: await openpgp.readMessage({ armoredMessage: encryptedRsaSignOnly }),
@ -3550,6 +3550,11 @@ PzIEeL7UH3trraFmi+Gq8u4kAA==
})).to.be.fulfilled; })).to.be.fulfilled;
}); });
it('PrivateKey.getDecryptionKeys() - should throw for sign-only key', async function() {
const key = await openpgp.readKey({ armoredKey: rsaSignOnly });
await expect(key.getDecryptionKeys()).to.be.rejectedWith(/No decryption key packets found/);
});
it('Key.getExpirationTime()', async function() { it('Key.getExpirationTime()', async function() {
const [, pubKey] = await openpgp.readKeys({ armoredKeys: twoKeys }); const [, pubKey] = await openpgp.readKeys({ armoredKeys: twoKeys });
expect(pubKey).to.exist; expect(pubKey).to.exist;

View File

@ -1768,7 +1768,7 @@ aOU=
message: await openpgp.readMessage({ armoredMessage: encrypted }), message: await openpgp.readMessage({ armoredMessage: encrypted }),
decryptionKeys: privateKeyRSA, decryptionKeys: privateKeyRSA,
config config
})).to.be.rejectedWith(/Session key decryption failed/); })).to.be.rejectedWith(/No decryption key packets found/);
// decryption using ECC key should succeed (PKCS1 is not used, so constant time countermeasures are not applied) // decryption using ECC key should succeed (PKCS1 is not used, so constant time countermeasures are not applied)
const { data } = await openpgp.decrypt({ const { data } = await openpgp.decrypt({
message: await openpgp.readMessage({ armoredMessage: encrypted }), message: await openpgp.readMessage({ armoredMessage: encrypted }),
@ -2642,7 +2642,7 @@ XfA3pqV4mTzF
}).then(() => { }).then(() => {
throw new Error('Should not decrypt with invalid key'); throw new Error('Should not decrypt with invalid key');
}).catch(error => { }).catch(error => {
expect(error.message).to.match(/Error decrypting session keys: Session key decryption failed./); expect(error.message).to.match(/Error decrypting session keys: Could not find valid subkey binding signature in key/);
}); });
}); });
}); });
@ -4174,7 +4174,7 @@ XfA3pqV4mTzF
decryptionKeys: decryptedKeyDE decryptionKeys: decryptedKeyDE
}; };
// binding signature is invalid // binding signature is invalid
await expect(openpgp.decrypt(decOpt)).to.be.rejectedWith(/Session key decryption failed/); await expect(openpgp.decrypt(decOpt)).to.be.rejectedWith(/Could not find valid subkey binding signature in key/);
}); });
it('RSA decryption with PKCS1 padding of wrong length should fail', async function() { it('RSA decryption with PKCS1 padding of wrong length should fail', async function() {