From 9d59351007d67f7817d74572e7e4200b04ce7db1 Mon Sep 17 00:00:00 2001 From: Sanjana Rajan Date: Mon, 30 Jan 2017 19:38:49 -0800 Subject: [PATCH 1/6] reformatKey function --- src/key.js | 200 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 120 insertions(+), 80 deletions(-) diff --git a/src/key.js b/src/key.js index 5669188b..6d9f0abf 100644 --- a/src/key.js +++ b/src/key.js @@ -897,6 +897,7 @@ SubKey.prototype.update = function(subKey, primaryKey) { } }; + /** * Reads an OpenPGP armored text and returns one or multiple key objects * @param {String} armoredText text to be parsed @@ -949,7 +950,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 +964,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,86 +980,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); - } - - 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(); - } - - return new Key(packetlist); - } } +/** + * Reformats and signs an OpenPGP with a given User ID. Currently only supports RSA keys. + * @param {module:key~Key} options.privateKey The privateKey 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 reformatKey(options) { + var secretKeyPacket, secretSubkeyPacket; + 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'); + } + + 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); +} + +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); +} + + /** * Returns the preferred symmetric algorithm for a set of keys * @param {Array} keys Set of keys From 9de579a29d18e039644356d5ac757dbdbeed82fc Mon Sep 17 00:00:00 2001 From: Sanjana Rajan Date: Mon, 30 Jan 2017 19:41:43 -0800 Subject: [PATCH 2/6] remove extra space, typo --- src/key.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/key.js b/src/key.js index 6d9f0abf..11a8d1cb 100644 --- a/src/key.js +++ b/src/key.js @@ -897,7 +897,6 @@ SubKey.prototype.update = function(subKey, primaryKey) { } }; - /** * Reads an OpenPGP armored text and returns one or multiple key objects * @param {String} armoredText text to be parsed @@ -984,7 +983,7 @@ export function generate(options) { /** * Reformats and signs an OpenPGP with a given User ID. Currently only supports RSA keys. - * @param {module:key~Key} options.privateKey The privateKey to reformat + * @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 @@ -1096,7 +1095,6 @@ function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) { return new Key(packetlist); } - /** * Returns the preferred symmetric algorithm for a set of keys * @param {Array} keys Set of keys From a902c421ebfa8585e85903e10e6a0be9364c6407 Mon Sep 17 00:00:00 2001 From: Sanjana Rajan Date: Tue, 31 Jan 2017 14:14:27 -0800 Subject: [PATCH 3/6] add upper level refactorKey function --- src/key.js | 41 ++++++++++++++++++++++------------------- src/openpgp.js | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/key.js b/src/key.js index 11a8d1cb..9bcc1e68 100644 --- a/src/key.js +++ b/src/key.js @@ -993,28 +993,31 @@ export function generate(options) { * @return {module:key~Key} * @static */ -export function reformatKey(options) { +export function reformat(options) { var secretKeyPacket, secretSubkeyPacket; - 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'); - } + return Promise.resolve().then(() => { - 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]; + 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'); } - } - return wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options); + + 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); + }); } function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) { diff --git a/src/openpgp.js b/src/openpgp.js index 27df2883..aec0f9c4 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -113,6 +113,33 @@ 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 {Number} numBits (optional) number of bits for the key creation. (should be 2048 or 4096) + * @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 From 1c361cd4e15ee5156e8e959fd44c65801f8fcd31 Mon Sep 17 00:00:00 2001 From: Sanjana Rajan Date: Tue, 31 Jan 2017 14:16:34 -0800 Subject: [PATCH 4/6] add some reformatting key tests --- test/general/key.js | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/test/general/key.js b/test/general/key.js index 11e9a594..4ecaeec9 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -890,6 +890,47 @@ 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 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; + opt.passphrase = '123'; + 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.false; + newKey.decrypt('123'); + expect(newKey.primaryKey.isDecrypted).to.be.true; + done(); + }).catch(done); + }).catch(done); + }); }); From 11e1040a307f572614c145176da38f4c21078dd0 Mon Sep 17 00:00:00 2001 From: Sanjana Rajan Date: Tue, 31 Jan 2017 14:18:13 -0800 Subject: [PATCH 5/6] remove extra input param --- src/openpgp.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openpgp.js b/src/openpgp.js index aec0f9c4..b027991e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -117,7 +117,6 @@ export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=fal * 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 {Number} numBits (optional) number of bits for the key creation. (should be 2048 or 4096) * @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: From 37b26cf3ef5732eb704ce1a7502e9727d056b7cb Mon Sep 17 00:00:00 2001 From: Sanjana Rajan Date: Tue, 31 Jan 2017 15:07:51 -0800 Subject: [PATCH 6/6] add test with reformatted key signing and encryption --- test/general/key.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/test/general/key.js b/test/general/key.js index 4ecaeec9..2b424a44 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -913,17 +913,18 @@ var pgp_desktop_priv = }); it('Reformat and encrypt key', function(done) { var userId1 = 'test1 '; - var userId2 = 'test2 '; + 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; + opt.userIds = [userId2, userId3]; opt.passphrase = '123'; openpgp.reformatKey(opt).then(function(newKey) { newKey = newKey.key - expect(newKey.users.length).to.equal(1); + 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'); @@ -932,5 +933,26 @@ var pgp_desktop_priv = }).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); + }); });