diff --git a/src/key.js b/src/key.js index 5669188b..9bcc1e68 100644 --- a/src/key.js +++ b/src/key.js @@ -949,7 +949,7 @@ export function readArmored(armoredText) { * @static */ export function generate(options) { - var packetlist, secretKeyPacket, userIdPacket, dataToSign, signaturePacket, secretSubkeyPacket, subkeySignaturePacket; + var secretKeyPacket, secretSubkeyPacket; return Promise.resolve().then(() => { options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated @@ -963,7 +963,9 @@ export function generate(options) { options.userIds = [options.userIds]; } - return Promise.all([generateSecretKey(), generateSecretSubkey()]).then(wrapKeyObject); + return Promise.all([generateSecretKey(), generateSecretSubkey()]).then(() => { + return wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options); + }); }); function generateSecretKey() { @@ -977,84 +979,123 @@ export function generate(options) { secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.keyType); return secretSubkeyPacket.generate(options.numBits); } +} - function wrapKeyObject() { - // set passphrase protection - if (options.passphrase) { - secretKeyPacket.encrypt(options.passphrase); - secretSubkeyPacket.encrypt(options.passphrase); +/** + * Reformats and signs an OpenPGP with a given User ID. Currently only supports RSA keys. + * @param {module:key~Key} options.privateKey The private key to reformat + * @param {module:enums.publicKey} [options.keyType=module:enums.publicKey.rsa_encrypt_sign] + * @param {String|Array} options.userIds assumes already in form of "User Name " + If array is used, the first userId is set as primary user Id + * @param {String} options.passphrase The passphrase used to encrypt the resulting private key + * @param {Boolean} [options.unlocked=false] The secret part of the generated key is unlocked + * @param {Number} [options.keyExpirationTime=0] The number of seconds after the key creation time that the key expires + * @return {module:key~Key} + * @static + */ +export function reformat(options) { + var secretKeyPacket, secretSubkeyPacket; + return Promise.resolve().then(() => { + + options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; + if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated + throw new Error('Only RSA Encrypt or Sign supported'); } - packetlist = new packet.List(); - - packetlist.push(secretKeyPacket); - - options.userIds.forEach(function(userId, index) { - - userIdPacket = new packet.Userid(); - userIdPacket.read(util.str2Uint8Array(userId)); - - dataToSign = {}; - dataToSign.userid = userIdPacket; - dataToSign.key = secretKeyPacket; - signaturePacket = new packet.Signature(); - signaturePacket.signatureType = enums.signature.cert_generic; - signaturePacket.publicKeyAlgorithm = options.keyType; - signaturePacket.hashAlgorithm = config.prefer_hash_algorithm; - signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data]; - signaturePacket.preferredSymmetricAlgorithms = []; - // prefer aes256, aes128, then aes192 (no WebCrypto support: https://www.chromium.org/blink/webcrypto#TOC-AES-support) - signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes256); - signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes128); - signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192); - signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.cast5); - signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.tripledes); - signaturePacket.preferredHashAlgorithms = []; - // prefer fast asm.js implementations (SHA-256, SHA-1) - signaturePacket.preferredHashAlgorithms.push(enums.hash.sha256); - signaturePacket.preferredHashAlgorithms.push(enums.hash.sha1); - signaturePacket.preferredHashAlgorithms.push(enums.hash.sha512); - signaturePacket.preferredCompressionAlgorithms = []; - signaturePacket.preferredCompressionAlgorithms.push(enums.compression.zlib); - signaturePacket.preferredCompressionAlgorithms.push(enums.compression.zip); - if (index === 0) { - signaturePacket.isPrimaryUserID = true; - } - if (config.integrity_protect) { - signaturePacket.features = []; - signaturePacket.features.push(1); // Modification Detection - } - if (options.keyExpirationTime > 0) { - signaturePacket.keyExpirationTime = options.keyExpirationTime; - signaturePacket.keyNeverExpires = false; - } - signaturePacket.sign(secretKeyPacket, dataToSign); - - packetlist.push(userIdPacket); - packetlist.push(signaturePacket); - - }); - - dataToSign = {}; - dataToSign.key = secretKeyPacket; - dataToSign.bind = secretSubkeyPacket; - subkeySignaturePacket = new packet.Signature(); - subkeySignaturePacket.signatureType = enums.signature.subkey_binding; - subkeySignaturePacket.publicKeyAlgorithm = options.keyType; - subkeySignaturePacket.hashAlgorithm = config.prefer_hash_algorithm; - subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage]; - subkeySignaturePacket.sign(secretKeyPacket, dataToSign); - - packetlist.push(secretSubkeyPacket); - packetlist.push(subkeySignaturePacket); - - if (!options.unlocked) { - secretKeyPacket.clearPrivateMPIs(); - secretSubkeyPacket.clearPrivateMPIs(); + if (!options.passphrase) { // Key without passphrase is unlocked by definition + options.unlocked = true; } + if (String.prototype.isPrototypeOf(options.userIds) || typeof options.userIds === 'string') { + options.userIds = [options.userIds]; + } + var packetlist = options.privateKey.toPacketlist(); + for (var i = 0; i < packetlist.length; i++) { + if (packetlist[i].tag === enums.packet.secretKey) { + secretKeyPacket = packetlist[i]; + } else if (packetlist[i].tag === enums.packet.secretSubkey) { + secretSubkeyPacket = packetlist[i]; + } + } + return wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options); + }); +} - return new Key(packetlist); +function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) { + // set passphrase protection + if (options.passphrase) { + secretKeyPacket.encrypt(options.passphrase); + secretSubkeyPacket.encrypt(options.passphrase); } + + var packetlist = new packet.List(); + + packetlist.push(secretKeyPacket); + + options.userIds.forEach(function(userId, index) { + + var userIdPacket = new packet.Userid(); + userIdPacket.read(util.str2Uint8Array(userId)); + + var dataToSign = {}; + dataToSign.userid = userIdPacket; + dataToSign.key = secretKeyPacket; + var signaturePacket = new packet.Signature(); + signaturePacket.signatureType = enums.signature.cert_generic; + signaturePacket.publicKeyAlgorithm = options.keyType; + signaturePacket.hashAlgorithm = config.prefer_hash_algorithm; + signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data]; + signaturePacket.preferredSymmetricAlgorithms = []; + // prefer aes256, aes128, then aes192 (no WebCrypto support: https://www.chromium.org/blink/webcrypto#TOC-AES-support) + signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes256); + signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes128); + signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192); + signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.cast5); + signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.tripledes); + signaturePacket.preferredHashAlgorithms = []; + // prefer fast asm.js implementations (SHA-256, SHA-1) + signaturePacket.preferredHashAlgorithms.push(enums.hash.sha256); + signaturePacket.preferredHashAlgorithms.push(enums.hash.sha1); + signaturePacket.preferredHashAlgorithms.push(enums.hash.sha512); + signaturePacket.preferredCompressionAlgorithms = []; + signaturePacket.preferredCompressionAlgorithms.push(enums.compression.zlib); + signaturePacket.preferredCompressionAlgorithms.push(enums.compression.zip); + if (index === 0) { + signaturePacket.isPrimaryUserID = true; + } + if (config.integrity_protect) { + signaturePacket.features = []; + signaturePacket.features.push(1); // Modification Detection + } + if (options.keyExpirationTime > 0) { + signaturePacket.keyExpirationTime = options.keyExpirationTime; + signaturePacket.keyNeverExpires = false; + } + signaturePacket.sign(secretKeyPacket, dataToSign); + + packetlist.push(userIdPacket); + packetlist.push(signaturePacket); + + }); + + var dataToSign = {}; + dataToSign.key = secretKeyPacket; + dataToSign.bind = secretSubkeyPacket; + var subkeySignaturePacket = new packet.Signature(); + subkeySignaturePacket.signatureType = enums.signature.subkey_binding; + subkeySignaturePacket.publicKeyAlgorithm = options.keyType; + subkeySignaturePacket.hashAlgorithm = config.prefer_hash_algorithm; + subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage]; + subkeySignaturePacket.sign(secretKeyPacket, dataToSign); + + packetlist.push(secretSubkeyPacket); + packetlist.push(subkeySignaturePacket); + + if (!options.unlocked) { + secretKeyPacket.clearPrivateMPIs(); + secretSubkeyPacket.clearPrivateMPIs(); + } + + return new Key(packetlist); } /** diff --git a/src/openpgp.js b/src/openpgp.js index 27df2883..b027991e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -113,6 +113,32 @@ export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=fal })).catch(onError.bind(null, 'Error generating keypair')); } +/** + * Generates a new OpenPGP key pair. Currently only supports RSA keys. Primary and subkey will be of same type. + * @param {Array} userIds array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }] + * @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key + * @param {Boolean} unlocked (optional) If the returned secret part of the generated key is unlocked + * @param {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires + * @return {Promise} The generated key object in the form: + * { key:Key, privateKeyArmored:String, publicKeyArmored:String } + * @static + */ +export function reformatKey({ privateKey, userIds=[], passphrase="", unlocked=false, keyExpirationTime=0 } = {}) { + const options = formatUserIds({ privateKey, userIds, passphrase, unlocked, keyExpirationTime }); + + if (!util.getWebCryptoAll() && asyncProxy) { // use web worker if web crypto apis are not supported + return asyncProxy.delegate('reformatKey', options); + } + + return key.reformat(options).then(newKey => ({ + + key: newKey, + privateKeyArmored: newKey.armor(), + publicKeyArmored: newKey.toPublic().armor() + + })).catch(onError.bind(null, 'Error reformatting keypair')); +} + /** * Unlock a private key with your passphrase. * @param {Key} privateKey the private key that is to be decrypted diff --git a/test/general/key.js b/test/general/key.js index 11e9a594..2b424a44 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -890,6 +890,69 @@ var pgp_desktop_priv = done(); }).catch(done); }); - + it('Reformat key without passphrase', function(done) { + var userId1 = 'test1 '; + var userId2 = 'test2 '; + var opt = {numBits: 512, userIds: userId1}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + openpgp.generateKey(opt).then(function(key) { + key = key.key + expect(key.users.length).to.equal(1); + expect(key.users[0].userId.userid).to.equal(userId1); + expect(key.primaryKey.isDecrypted).to.be.true; + opt.privateKey = key; + opt.userIds = userId2; + openpgp.reformatKey(opt).then(function(newKey) { + newKey = newKey.key + expect(newKey.users.length).to.equal(1); + expect(newKey.users[0].userId.userid).to.equal(userId2); + expect(newKey.primaryKey.isDecrypted).to.be.true; + done(); + }).catch(done); + }).catch(done); + }); + it('Reformat and encrypt key', function(done) { + var userId1 = 'test1 '; + var userId2 = 'test2 '; + var userId3 = 'test3 '; + var opt = {numBits: 512, userIds: userId1}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + openpgp.generateKey(opt).then(function(key) { + key = key.key + opt.privateKey = key; + opt.userIds = [userId2, userId3]; + opt.passphrase = '123'; + openpgp.reformatKey(opt).then(function(newKey) { + newKey = newKey.key + expect(newKey.users.length).to.equal(2); + expect(newKey.users[0].userId.userid).to.equal(userId2); + expect(newKey.primaryKey.isDecrypted).to.be.false; + newKey.decrypt('123'); + expect(newKey.primaryKey.isDecrypted).to.be.true; + done(); + }).catch(done); + }).catch(done); + }); + it('Sign and encrypt with reformatted key', function(done) { + var userId1 = 'test1 '; + var userId2 = 'test2 '; + var opt = {numBits: 512, userIds: userId1}; + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + openpgp.generateKey(opt).then(function(key) { + key = key.key + opt.privateKey = key; + opt.userIds = userId2; + openpgp.reformatKey(opt).then(function(newKey) { + newKey = newKey.key + openpgp.encrypt({data: 'hello', publicKeys: newKey.toPublic(), privateKeys: newKey, armor: true}).then(function(encrypted) { + openpgp.decrypt({message: openpgp.message.readArmored(encrypted.data), privateKey: newKey, publicKeys: newKey.toPublic()}).then(function(decrypted) { + expect(decrypted.data).to.equal('hello'); + expect(decrypted.signatures[0].valid).to.be.true; + done(); + }).catch(done); + }).catch(done); + }).catch(done); + }).catch(done); + }); });