diff --git a/src/key/factory.js b/src/key/factory.js index 8f7bb676..735d9a15 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -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(); diff --git a/src/key/helper.js b/src/key/helper.js index f6adb616..99cc48b5 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -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.'); } diff --git a/src/key/key.js b/src/key/key.js index 5ae500fa..8a0e6449 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -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]; }); diff --git a/src/message.js b/src/message.js index 07884d8f..4c09391e 100644 --- a/src/message.js +++ b/src/message.js @@ -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()) { diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 1c24d19e..48f2643e 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -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; } diff --git a/test/general/key.js b/test/general/key.js index ef3e3c25..f8c3e988 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -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'); diff --git a/test/general/signature.js b/test/general/signature.js index e92d072a..856fd4de 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -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();