diff --git a/src/key.js b/src/key.js index c5831244..be8e1552 100644 --- a/src/key.js +++ b/src/key.js @@ -826,6 +826,37 @@ Key.prototype.verifyAllUsers = async function(keys) { return results; }; +/** + * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. + * Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key. + * @param {Integer} options.numBits number of bits for the key creation. + * @param {Number} [options.keyExpirationTime=0] + * The number of seconds after the key creation time that the key expires + * @param {String} curve (optional) Elliptic curve for ECC keys + * @param {Date} date (optional) Override the creation date of the key and the key signatures + * @param {Boolean} subkeys (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false + * @returns {Promise} + * @async + */ +Key.prototype.addSubkey = async function(options = {}) { + if (!this.isPrivate()) { + throw new Error("Cannot add a subkey to a public key"); + } + const defaultOptions = this.primaryKey.getAlgorithmInfo(); + defaultOptions.numBits = defaultOptions.bits; + const secretKeyPacket = this.primaryKey; + if (!secretKeyPacket.isDecrypted()) { + throw new Error("Key is not decrypted"); + } + options = sanitizeKeyOptions(options, defaultOptions); + const keyPacket = await generateSecretSubkey(options); + const bindingSignature = await createBindingSignature(keyPacket, secretKeyPacket, options); + const packetList = this.toPacketlist(); + packetList.push(keyPacket); + packetList.push(bindingSignature); + return new Key(packetList); +}; + /** * @class * @classdesc Class that represents an user ID or attribute packet and the relevant signatures. @@ -1328,53 +1359,53 @@ export async function generate(options) { let promises = [generateSecretKey(options)]; promises = promises.concat(options.subkeys.map(generateSecretSubkey)); return Promise.all(promises).then(packets => wrapKeyObject(packets[0], packets.slice(1), options)); +} - function sanitizeKeyOptions(options, subkeyDefaults = {}) { - options.curve = options.curve || subkeyDefaults.curve; - options.numBits = options.numBits || subkeyDefaults.numBits; - options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime; - options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; - options.date = options.date || subkeyDefaults.date; +function sanitizeKeyOptions(options, subkeyDefaults = {}) { + options.curve = options.curve || subkeyDefaults.curve; + options.numBits = options.numBits || subkeyDefaults.numBits; + options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime; + options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; + options.date = options.date || subkeyDefaults.date; - options.sign = options.sign || false; + options.sign = options.sign || false; - if (options.curve) { - try { - options.curve = enums.write(enums.curve, options.curve); - } catch (e) { - throw new Error('Not valid curve.'); - } - if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) { - options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519; - } - if (options.sign) { - options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa; - } else { - options.algorithm = enums.publicKey.ecdh; - } - } else if (options.numBits) { - options.algorithm = enums.publicKey.rsa_encrypt_sign; - } else { - throw new Error('Unrecognized key type'); + if (options.curve) { + try { + options.curve = enums.write(enums.curve, options.curve); + } catch (e) { + throw new Error('Not valid curve.'); } - return options; + if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) { + options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519; + } + if (options.sign) { + options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa; + } else { + options.algorithm = enums.publicKey.ecdh; + } + } else if (options.numBits) { + options.algorithm = enums.publicKey.rsa_encrypt_sign; + } else { + throw new Error('Unrecognized key type'); } + return options; +} - async function generateSecretKey(options) { - const secretKeyPacket = new packet.SecretKey(options.date); - secretKeyPacket.packets = null; - secretKeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm); - await secretKeyPacket.generate(options.numBits, options.curve); - return secretKeyPacket; - } +async function generateSecretKey(options) { + const secretKeyPacket = new packet.SecretKey(options.date); + secretKeyPacket.packets = null; + secretKeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm); + await secretKeyPacket.generate(options.numBits, options.curve); + return secretKeyPacket; +} - async function generateSecretSubkey(options) { - const secretSubkeyPacket = new packet.SecretSubkey(options.date); - secretSubkeyPacket.packets = null; - secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm); - await secretSubkeyPacket.generate(options.numBits, options.curve); - return secretSubkeyPacket; - } +async function generateSecretSubkey(options) { + const secretSubkeyPacket = new packet.SecretSubkey(options.date); + secretSubkeyPacket.packets = null; + secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm); + await secretSubkeyPacket.generate(options.numBits, options.curve); + return secretSubkeyPacket; } /** @@ -1541,27 +1572,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) { const subkeyOptions = options.subkeys[index]; - const dataToSign = {}; - dataToSign.key = secretKeyPacket; - dataToSign.bind = secretSubkeyPacket; - const subkeySignaturePacket = new packet.Signature(subkeyOptions.date); - subkeySignaturePacket.signatureType = enums.signature.subkey_binding; - subkeySignaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm; - subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, secretSubkeyPacket); - if (subkeyOptions.sign) { - subkeySignaturePacket.keyFlags = [enums.keyFlags.sign_data]; - subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, secretSubkeyPacket, { - signatureType: enums.signature.key_binding - }, subkeyOptions.date); - } else { - subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage]; - } - if (subkeyOptions.keyExpirationTime > 0) { - subkeySignaturePacket.keyExpirationTime = subkeyOptions.keyExpirationTime; - subkeySignaturePacket.keyNeverExpires = false; - } - await subkeySignaturePacket.sign(secretKeyPacket, dataToSign); - + const subkeySignaturePacket = await createBindingSignature(secretSubkeyPacket, secretKeyPacket, subkeyOptions); return { secretSubkeyPacket, subkeySignaturePacket }; })).then(packets => { packets.forEach(({ secretSubkeyPacket, subkeySignaturePacket }) => { @@ -1594,6 +1605,37 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { return new Key(packetlist); } +/** + * Create subkey binding signature + * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1} + * @param {module:packet.SecretSubkey} subkey Subkey key packet + * @param {module:packet.SecretKey} primaryKey Primary key packet + * @param {Object} options + */ +async function createBindingSignature(subkey, primaryKey, options) { + const dataToSign = {}; + dataToSign.key = primaryKey; + dataToSign.bind = subkey; + const subkeySignaturePacket = new packet.Signature(options.date); + subkeySignaturePacket.signatureType = enums.signature.subkey_binding; + subkeySignaturePacket.publicKeyAlgorithm = primaryKey.algorithm; + subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, subkey); + if (options.sign) { + subkeySignaturePacket.keyFlags = [enums.keyFlags.sign_data]; + subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, { + signatureType: enums.signature.key_binding + }, options.date); + } else { + subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage]; + } + if (options.keyExpirationTime > 0) { + subkeySignaturePacket.keyExpirationTime = options.keyExpirationTime; + subkeySignaturePacket.keyNeverExpires = false; + } + await subkeySignaturePacket.sign(primaryKey, dataToSign); + return subkeySignaturePacket; +} + /** * Checks if a given certificate or binding signature is revoked * @param {module:packet.SecretKey| diff --git a/test/general/key.js b/test/general/key.js index c8e63224..9aa6f76d 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2865,3 +2865,173 @@ VYGdb3eNlV8CfoEC })()).to.be.rejectedWith('Key packet is already encrypted'); }); }); + +describe('addSubkey functionality testing', function(){ + it('create and add a new rsa subkey to a rsa key', async function() { + const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0]; + await privateKey.decrypt('hello world'); + const total = privateKey.subKeys.length; + let newPrivateKey = await privateKey.addSubkey(); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + const subKey = newPrivateKey.subKeys[total]; + expect(subKey).to.exist; + expect(newPrivateKey.subKeys.length).to.be.equal(total+1); + const subkeyN = subKey.keyPacket.params[0]; + const pkN = privateKey.primaryKey.params[0]; + expect(subkeyN.byteLength()).to.be.equal(pkN.byteLength()); + expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign'); + expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + }); + + it('encrypt and decrypt key with added subkey', async function() { + const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0]; + await privateKey.decrypt('hello world'); + const total = privateKey.subKeys.length; + let newPrivateKey = await privateKey.addSubkey(); + newPrivateKey = (await openpgp.key.readArmored(newPrivateKey.armor())).keys[0]; + await newPrivateKey.encrypt('12345678'); + const armoredKey = newPrivateKey.armor(); + let importedPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + await importedPrivateKey.decrypt('12345678'); + const subKey = importedPrivateKey.subKeys[total]; + expect(subKey).to.exist; + expect(importedPrivateKey.subKeys.length).to.be.equal(total+1); + const subkeyN = subKey.keyPacket.params[0]; + const pkN = privateKey.primaryKey.params[0]; + expect(subkeyN.byteLength()).to.be.equal(pkN.byteLength()); + expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign'); + expect(await subKey.verify(importedPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + }); + + it('create and add a new ec subkey to a ec key', async function() { + const userId = 'test '; + const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]}; + const privateKey = (await openpgp.generateKey(opt)).key; + const total = privateKey.subKeys.length; + const opt2 = {curve: 'curve25519', userIds: [userId], sign: true}; + let newPrivateKey = await privateKey.addSubkey(opt2); + const subKey1 = newPrivateKey.subKeys[total]; + await newPrivateKey.encrypt('12345678'); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + await newPrivateKey.decrypt('12345678'); + const subKey = newPrivateKey.subKeys[total]; + expect(subKey.isDecrypted()).to.be.true; + expect(subKey1.getKeyId().toHex()).to.be.equal(subKey.getKeyId().toHex()); + expect(subKey).to.exist; + expect(newPrivateKey.subKeys.length).to.be.equal(total+1); + const subkeyOid = subKey.keyPacket.params[0]; + const pkOid = privateKey.primaryKey.params[0]; + expect(subkeyOid.getName()).to.be.equal(pkOid.getName()); + expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('eddsa'); + expect(await subKey.verify(privateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + }); + + it('create and add a new ec subkey to a rsa key', async function() { + const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0]; + privateKey.subKeys = []; + await privateKey.decrypt('hello world'); + const total = privateKey.subKeys.length; + const opt2 = {curve: 'curve25519'}; + let newPrivateKey = await privateKey.addSubkey(opt2); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + const subKey = newPrivateKey.subKeys[total]; + expect(subKey).to.exist; + expect(newPrivateKey.subKeys.length).to.be.equal(total+1); + expect(subKey.keyPacket.params[0].getName()).to.be.equal(openpgp.enums.curve.curve25519); + expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('ecdh'); + expect(await subKey.verify(privateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + }); + + it('sign/verify data with the new subkey correctly using curve25519', async function() { + const userId = 'test '; + const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]}; + const privateKey = (await openpgp.generateKey(opt)).key; + const total = privateKey.subKeys.length; + const opt2 = {sign: true}; + let newPrivateKey = await privateKey.addSubkey(opt2); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + const subKey = newPrivateKey.subKeys[total]; + const subkeyOid = subKey.keyPacket.params[0]; + const pkOid = newPrivateKey.primaryKey.params[0]; + expect(subkeyOid.getName()).to.be.equal(pkOid.getName()); + expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('eddsa'); + expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + expect(await newPrivateKey.getSigningKey()).to.be.equal(subKey); + const signed = await openpgp.sign({message: openpgp.cleartext.fromText('the data to signed'), privateKeys: newPrivateKey, armor:false}); + const verified = await signed.message.verify([newPrivateKey.toPublic()]); + expect(verified).to.exist; + expect(verified.length).to.be.equal(1); + expect(await verified[0].keyid).to.be.equal(subKey.getKeyId()); + expect(await verified[0].verified).to.be.true; + }); + + it('encrypt/decrypt data with the new subkey correctly using curve25519', async function() { + const userId = 'test '; + const vData = 'the data to encrypted!'; + const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]}; + const privateKey = (await openpgp.generateKey(opt)).key; + const total = privateKey.subKeys.length; + let newPrivateKey = await privateKey.addSubkey(); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + const subKey = newPrivateKey.subKeys[total]; + const publicKey = newPrivateKey.toPublic(); + expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + expect(await newPrivateKey.getEncryptionKey()).to.be.equal(subKey); + const encrypted = await openpgp.encrypt({message: openpgp.message.fromText(vData), publicKeys: publicKey, armor:false}); + expect(encrypted.message).to.be.exist; + const pkSessionKeys = encrypted.message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey); + expect(pkSessionKeys).to.exist; + expect(pkSessionKeys.length).to.be.equal(1); + expect(pkSessionKeys[0].publicKeyId.toHex()).to.be.equals(subKey.keyPacket.getKeyId().toHex()); + const decrypted = await openpgp.decrypt({message: encrypted.message, privateKeys: newPrivateKey}) + expect(decrypted).to.exist; + expect(decrypted.data).to.be.equal(vData); + }); + + it('sign/verify data with the new subkey correctly using rsa', async function() { + const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0]; + await privateKey.decrypt('hello world'); + const total = privateKey.subKeys.length; + const opt2 = {sign: true}; + let newPrivateKey = await privateKey.addSubkey(opt2); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + const subKey = newPrivateKey.subKeys[total]; + expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign'); + expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid); + expect(await newPrivateKey.getSigningKey()).to.be.equal(subKey); + const signed = await openpgp.sign({message: openpgp.cleartext.fromText('the data to signed'), privateKeys: newPrivateKey, armor:false}); + const verified = await signed.message.verify([newPrivateKey.toPublic()]); + expect(verified).to.exist; + expect(verified.length).to.be.equal(1); + expect(await verified[0].keyid).to.be.equal(subKey.getKeyId()); + expect(await verified[0].verified).to.be.true; + }); + + it('encrypt/decrypt data with the new subkey correctly using rsa', async function() { + const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0]; + await privateKey.decrypt('hello world'); + const total = privateKey.subKeys.length; + let newPrivateKey = await privateKey.addSubkey(); + const armoredKey = newPrivateKey.armor(); + newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0]; + const subKey = newPrivateKey.subKeys[total]; + const publicKey = newPrivateKey.toPublic(); + const vData = 'the data to encrypted!'; + expect(await newPrivateKey.getEncryptionKey()).to.be.equal(subKey); + const encrypted = await openpgp.encrypt({message: openpgp.message.fromText(vData), publicKeys: publicKey, armor:false}); + expect(encrypted.message).to.be.exist; + const pkSessionKeys = encrypted.message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey); + expect(pkSessionKeys).to.exist; + expect(pkSessionKeys.length).to.be.equal(1); + expect(pkSessionKeys[0].publicKeyId.toHex()).to.be.equals(subKey.keyPacket.getKeyId().toHex()); + const decrypted = await openpgp.decrypt({message: encrypted.message, privateKeys: newPrivateKey}) + expect(decrypted).to.exist; + expect(decrypted.data).to.be.equal(vData); + }); +});