Check if any (sub)key is decrypted in Key.prototype.isDecrypted ()

`key.isDecrypted()` now returns true if either the primary key or any subkey
is decrypted.

Additionally, implement `SecretKeyPacket.prototype.makeDummy` for encrypted
keys.
This commit is contained in:
larabr 2021-01-24 18:19:27 +01:00 committed by Daniel Huigens
parent c23ed58387
commit 66c06dab3e
7 changed files with 75 additions and 26 deletions

View File

@ -77,13 +77,13 @@ export async function generate(options) {
export async function reformat(options) {
options = sanitize(options);
try {
const isDecrypted = options.privateKey.getKeys().every(key => key.isDecrypted());
if (!isDecrypted) {
await options.privateKey.decrypt();
}
} catch (err) {
throw new Error('Key not decrypted');
if (options.privateKey.primaryKey.isDummy()) {
throw new Error('Cannot reformat a gnu-dummy primary key');
}
const isDecrypted = options.privateKey.getKeys().every(({ keyPacket }) => keyPacket.isDecrypted());
if (!isDecrypted) {
throw new Error('Key is not decrypted');
}
const packetlist = options.privateKey.toPacketlist();

View File

@ -210,6 +210,9 @@ export async function getPreferredAlgo(type, keys, date = new Date(), userIds =
* @returns {module:packet/signature} signature packet
*/
export async function createSignaturePacket(dataToSign, privateKey, signingKeyPacket, signatureProperties, date, userId, detached = false, streaming = false) {
if (signingKeyPacket.isDummy()) {
throw new Error('Cannot sign with a gnu-dummy key.');
}
if (!signingKeyPacket.isDecrypted()) {
throw new Error('Private key is not decrypted.');
}

View File

@ -47,7 +47,6 @@ import * as helper from './helper';
* @borrows PublicKeyPacket#hasSameFingerprintAs as Key#hasSameFingerprintAs
* @borrows PublicKeyPacket#getAlgorithmInfo as Key#getAlgorithmInfo
* @borrows PublicKeyPacket#getCreationTime as Key#getCreationTime
* @borrows PublicKeyPacket#isDecrypted as Key#isDecrypted
*/
class Key {
/**
@ -457,6 +456,14 @@ class Key {
}
}
/**
* Returns true if the primary key or any subkey is decrypted.
* A dummy key is considered encrypted.
*/
isDecrypted() {
return this.getKeys().some(({ keyPacket }) => keyPacket.isDecrypted());
}
/**
* Check whether the private and public primary key parameters correspond
* Together with verification of binding signatures, this guarantees key integrity
@ -883,6 +890,9 @@ class Key {
throw new Error(`rsaBits should be at least ${config.minRsaBits}, got: ${options.rsaBits}`);
}
const secretKeyPacket = this.primaryKey;
if (secretKeyPacket.isDummy()) {
throw new Error("Cannot add subkey to gnu-dummy primary key");
}
if (!secretKeyPacket.isDecrypted()) {
throw new Error("Key is not decrypted");
}
@ -900,7 +910,7 @@ class Key {
}
}
['getKeyId', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'isDecrypted', 'hasSameFingerprintAs'].forEach(name => {
['getKeyId', 'getFingerprint', 'getAlgorithmInfo', 'getCreationTime', 'hasSameFingerprintAs'].forEach(name => {
Key.prototype[name] =
SubKey.prototype[name];
});

View File

@ -208,7 +208,7 @@ export class Message {
// do not check key expiration to allow decryption of old messages
const privateKeyPackets = (await privateKey.getDecryptionKeys(keyPacket.publicKeyId, null)).map(key => key.keyPacket);
await Promise.all(privateKeyPackets.map(async function(privateKeyPacket) {
if (!privateKeyPacket) {
if (!privateKeyPacket || privateKeyPacket.isDummy()) {
return;
}
if (!privateKeyPacket.isDecrypted()) {

View File

@ -228,7 +228,8 @@ class SecretKeyPacket extends PublicKeyPacket {
}
/**
* Check whether secret-key data is available in decrypted form. Returns null for public keys.
* Check whether secret-key data is available in decrypted form.
* Returns false for gnu-dummy keys and null for public keys.
* @returns {Boolean|null}
*/
isDecrypted() {
@ -244,20 +245,18 @@ class SecretKeyPacket extends PublicKeyPacket {
}
/**
* Remove private key material, converting the key to a dummy one
* The resulting key cannot be used for signing/decrypting but can still verify signatures
* Remove private key material, converting the key to a dummy one.
* The resulting key cannot be used for signing/decrypting but can still verify signatures.
*/
makeDummy() {
if (this.isDummy()) {
return;
}
if (!this.isDecrypted()) {
// this is technically not needed, but makes the conversion simpler
throw new Error("Key is not decrypted");
if (this.isDecrypted()) {
this.clearPrivateParams();
}
this.clearPrivateParams();
this.isEncrypted = null;
this.keyMaterial = null;
this.isEncrypted = false;
this.s2k = new type_s2k();
this.s2k.algorithm = 0;
this.s2k.c = 0;
@ -325,8 +324,7 @@ class SecretKeyPacket extends PublicKeyPacket {
*/
async decrypt(passphrase) {
if (this.isDummy()) {
this.isEncrypted = false;
return;
return false;
}
if (this.isDecrypted()) {
@ -418,7 +416,6 @@ class SecretKeyPacket extends PublicKeyPacket {
*/
clearPrivateParams() {
if (this.isDummy()) {
this.isEncrypted = true;
return;
}

View File

@ -2616,7 +2616,7 @@ function versionSpecificTests() {
return openpgp.reformatKey({ privateKey: original.key, userIds: { name: 'test2', email: 'a@b.com' }, passphrase: '1234' }).then(function() {
throw new Error('reformatKey should result in error when key not decrypted');
}).catch(function(error) {
expect(error.message).to.equal('Error reformatting keypair: Key not decrypted');
expect(error.message).to.equal('Error reformatting keypair: Key is not decrypted');
});
});
});
@ -2925,6 +2925,26 @@ module.exports = () => describe('Key', function() {
await expect(key.validate()).to.be.rejectedWith('Key is invalid');
});
it("isDecrypted() - should reflect whether all (sub)keys are encrypted", async function() {
const passphrase = '12345678';
const { key } = await openpgp.generateKey({ userIds: {}, curve: 'ed25519', passphrase });
expect(key.isDecrypted()).to.be.false;
await key.decrypt(passphrase, key.subKeys[0].getKeyId());
expect(key.isDecrypted()).to.be.true;
});
it("isDecrypted() - gnu-dummy primary key", async function() {
const key = await openpgp.readArmoredKey(gnuDummyKeySigningSubkey);
expect(key.isDecrypted()).to.be.true;
await key.encrypt('12345678');
expect(key.isDecrypted()).to.be.false;
});
it("isDecrypted() - all-gnu-dummy key", async function() {
const key = await openpgp.readArmoredKey(gnuDummyKey);
expect(key.isDecrypted()).to.be.false;
});
it('makeDummy() - the converted key can be parsed', async function() {
const { key } = await openpgp.generateKey({ userIds: { name: 'dummy', email: 'dummy@alice.com' } });
key.primaryKey.makeDummy();
@ -2950,7 +2970,7 @@ module.exports = () => describe('Key', function() {
key.primaryKey.makeDummy();
expect(key.primaryKey.isDummy()).to.be.true;
await key.validate();
await expect(openpgp.reformatKey({ privateKey: key, userIds: { name: 'test', email: 'a@b.com' } })).to.be.rejectedWith(/Missing key parameters/);
await expect(openpgp.reformatKey({ privateKey: key, userIds: { name: 'test', email: 'a@b.com' } })).to.be.rejectedWith(/Cannot reformat a gnu-dummy primary key/);
});
it('makeDummy() - subkeys of the converted key can still sign', async function() {
@ -2962,6 +2982,25 @@ module.exports = () => describe('Key', function() {
await expect(openpgp.sign({ message: openpgp.Message.fromText('test'), privateKeys: [key] })).to.be.fulfilled;
});
it('makeDummy() - should work for encrypted keys', async function() {
const key = await openpgp.readArmoredKey(priv_key_rsa);
expect(key.primaryKey.isDummy()).to.be.false;
expect(key.primaryKey.makeDummy()).to.not.throw;
expect(key.primaryKey.isDummy()).to.be.true;
// dummy primary key should always be marked as not decrypted
await expect(key.decrypt('hello world')).to.be.fulfilled;
expect(key.primaryKey.isDummy()).to.be.true;
expect(key.primaryKey.isEncrypted === null);
expect(key.primaryKey.isDecrypted()).to.be.false;
await expect(key.encrypt('hello world')).to.be.fulfilled;
expect(key.primaryKey.isDummy()).to.be.true;
expect(key.primaryKey.isEncrypted === null);
expect(key.primaryKey.isDecrypted()).to.be.false;
// confirm that the converted key can be parsed
const parsedKeys = (await openpgp.readArmoredKey(key.armor())).keys;
expect(parsedKeys).to.be.undefined;
});
it('clearPrivateParams() - check that private key can no longer be used', async function() {
const key = await openpgp.readArmoredKey(priv_key_rsa);
await key.decrypt('hello world');

View File

@ -878,9 +878,9 @@ hUhMKMuiM3pRwdIyDOItkUWQmjEEw7/XmhgInkXsCw==
expect(msg.signatures).to.have.length(1);
expect(msg.signatures[0].valid).to.be.true;
expect(msg.signatures[0].signature.packets.length).to.equal(1);
await expect(openpgp.sign({ message: openpgp.Message.fromText('test'), privateKeys: [priv_key_gnupg_ext] })).to.eventually.be.rejectedWith(/Missing key parameters/);
await expect(openpgp.reformatKey({ userIds: { name: 'test' }, privateKey: priv_key_gnupg_ext })).to.eventually.be.rejectedWith(/Missing key parameters/);
await expect(openpgp.reformatKey({ userIds: { name: 'test' }, privateKey: priv_key_gnupg_ext_2, passphrase: 'test' })).to.eventually.be.rejectedWith(/Missing key parameters/);
await expect(openpgp.sign({ message: openpgp.Message.fromText('test'), privateKeys: [priv_key_gnupg_ext] })).to.eventually.be.rejectedWith(/Cannot sign with a gnu-dummy key/);
await expect(openpgp.reformatKey({ userIds: { name: 'test' }, privateKey: priv_key_gnupg_ext })).to.eventually.be.rejectedWith(/Cannot reformat a gnu-dummy primary key/);
await expect(openpgp.reformatKey({ userIds: { name: 'test' }, privateKey: priv_key_gnupg_ext_2, passphrase: 'test' })).to.eventually.be.rejectedWith(/Cannot reformat a gnu-dummy primary key/);
await priv_key_gnupg_ext.encrypt("abcd");
expect(priv_key_gnupg_ext.isDecrypted()).to.be.false;
const primaryKey_packet2 = priv_key_gnupg_ext.primaryKey.write();