From 24119f4fb1a64dc44a0b51032099108d912dd11b Mon Sep 17 00:00:00 2001
From: Sanjana Rajan <srajan1@stanford.edu>
Date: Sun, 15 Apr 2018 12:38:18 -0700
Subject: [PATCH 1/3] keygen update

---
 src/key.js     | 193 ++++++++++++++++++++++++++++---------------------
 src/openpgp.js |  25 ++-----
 2 files changed, 117 insertions(+), 101 deletions(-)

diff --git a/src/key.js b/src/key.js
index 8f9d23b7..8fdebd7c 100644
--- a/src/key.js
+++ b/src/key.js
@@ -291,7 +291,6 @@ Key.prototype.getSigningKeyPacket = async function (keyId=null, date=new Date())
 };
 
 function isValidEncryptionKeyPacket(keyPacket, signature, date=new Date()) {
-  const normDate = util.normalizeDate(date);
   return keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.dsa) &&
          keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.rsa_sign) &&
          keyPacket.algorithm !== enums.read(enums.publicKey, enums.publicKey.ecdsa) &&
@@ -1100,18 +1099,34 @@ export function readArmored(armoredText) {
  *                             Assumes already in form of "User Name <username@email.com>"
  *                             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
+ * @param  {String} curve            (optional) elliptic curve for ECC keys:
  * @param  {Date} date         Override the creation date of the key and the key signatures
+ * @param  {Array<Object>} subkeys   (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
+ *                                              sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
  * @returns {Promise<module:key.Key>}
  * @async
  * @static
  */
-export function generate(options) {
-  let secretKeyPacket;
-  let secretSubkeyPacket;
-  return Promise.resolve().then(() => {
+export async function generate(options) {
+
+  options = sanitizeKeyOptions(options);
+  options.subkeys = options.subkeys.map(function(subkey, index) { return sanitizeKeyOptions(options.subkeys[index], options); });
+
+  let promises = [generateSecretKey(options)];
+  promises = promises.concat(options.subkeys.map(generateSecretSubkey));
+  return Promise.all(promises).then(packets => wrapKeyObject(packets, options));
+
+  function sanitizeKeyOptions(options, subkeyDefaults={}) {
+    options.curve = options.curve || subkeyDefaults.curve;
+    options.numBits = options.numBits || subkeyDefaults.numBits;
+    options.keyExpirationTime = options.keyExpirationTime >= 0 ? 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;
+
     if (options.curve) {
       try {
         options.curve = enums.write(enums.curve, options.curve);
@@ -1119,81 +1134,63 @@ export function generate(options) {
         throw new Error('Not valid curve.');
       }
       if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
-        options.keyType = options.keyType || enums.publicKey.eddsa;
+        if (!subkeyDefaults.algorithm || options.sign) {
+          options.algorithm = options.algorithm || enums.publicKey.eddsa;
+          options.curve = enums.curve.ed25519;
+        } else {
+          options.algorithm = options.algorithm || enums.publicKey.ecdh;
+          options.curve = enums.curve.curve25519;
+        }
       } else {
-        options.keyType = options.keyType || enums.publicKey.ecdsa;
+        options.algorithm = options.algorithm || (!subkeyDefaults.algorithm ? enums.publicKey.ecdsa : enums.publicKey.ecdh);
+        if (options.algorithm !== enums.publicKey.ecdh && options.algorithm !== enums.publicKey.ecdsa) {
+          throw new Error('Invalid algorithm for curve');
+        }
       }
-      options.subkeyType = options.subkeyType || enums.publicKey.ecdh;
     } else if (options.numBits) {
-      options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign;
-      options.subkeyType = options.subkeyType || enums.publicKey.rsa_encrypt_sign;
+      options.algorithm = enums.publicKey.rsa_encrypt_sign;
     } else {
-      throw new Error('Key type not specified.');
+      throw new Error('Unrecognized key type');
     }
-
-    if (options.keyType !== enums.publicKey.rsa_encrypt_sign &&
-        options.keyType !== enums.publicKey.ecdsa &&
-        options.keyType !== enums.publicKey.eddsa) {
-      // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated
-      throw new Error('Unsupported key type');
-    }
-
-    if (options.subkeyType !== enums.publicKey.rsa_encrypt_sign &&
-        options.subkeyType !== enums.publicKey.ecdh) {
-      // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated
-      throw new Error('Unsupported subkey type');
-    }
-
-    if (!options.passphrase) { // Key without passphrase is unlocked by definition
-      options.unlocked = true;
-    }
-    if (util.isString(options.userIds)) {
-      options.userIds = [options.userIds];
-    }
-
-    return Promise.all([generateSecretKey(), generateSecretSubkey()]).then(() => wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options));
-  });
-
-  function generateSecretKey() {
-    secretKeyPacket = new packet.SecretKey(options.date);
-    secretKeyPacket.packets = null;
-    secretKeyPacket.algorithm = enums.read(enums.publicKey, options.keyType);
-    options.curve = options.curve === enums.curve.curve25519 ? enums.curve.ed25519 : options.curve;
-    return secretKeyPacket.generate(options.numBits, options.curve);
+    return options;
   }
 
-  function generateSecretSubkey() {
-    secretSubkeyPacket = new packet.SecretSubkey(options.date);
+  async function generateSecretKey(options) {
+    const secretKeyPacket = new packet.SecretKey(options.date);
     secretKeyPacket.packets = null;
-    secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.subkeyType);
-    options.curve = options.curve === enums.curve.ed25519 ? enums.curve.curve25519 : options.curve;
-    return secretSubkeyPacket.generate(options.numBits, options.curve);
+    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;
   }
 }
 
 /**
- * Reformats and signs an OpenPGP with a given User ID. Currently only supports RSA keys.
+ * Reformats and signs an OpenPGP key 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<String>}  options.userIds
  *                             Assumes already in form of "User Name <username@email.com>"
  *                             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
+ * @param  {Date} date         Override the creation date of the key and the key signatures
+ * @param  {Array<Object>} subkeys   (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
+ *
  * @returns {Promise<module:key.Key>}
  * @async
  * @static
  */
 export async function reformat(options) {
-  let secretKeyPacket;
-  let secretSubkeyPacket;
-  options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign;
-  // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated
-  if (options.keyType !== enums.publicKey.rsa_encrypt_sign) {
-    throw new Error('Only RSA Encrypt or Sign supported');
-  }
+  options = sanitizeKeyOptions(options);
 
   try {
     const isDecrypted = options.privateKey.getKeyPackets().every(keyPacket => keyPacket.isDecrypted);
@@ -1204,37 +1201,56 @@ export async function reformat(options) {
     throw new Error('Key not decrypted');
   }
 
-  if (!options.passphrase) { // Key without passphrase is unlocked by definition
-    options.unlocked = true;
-  }
-  if (util.isString(options.userIds)) {
-    options.userIds = [options.userIds];
-  }
   const packetlist = options.privateKey.toPacketlist();
+  let secretKeyPacket;
+  const secretSubkeyPackets = [];
   for (let i = 0; i < packetlist.length; i++) {
     if (packetlist[i].tag === enums.packet.secretKey) {
       secretKeyPacket = packetlist[i];
-      options.keyType = secretKeyPacket.algorithm;
     } else if (packetlist[i].tag === enums.packet.secretSubkey) {
-      secretSubkeyPacket = packetlist[i];
-      options.subkeyType = secretSubkeyPacket.algorithm;
+      secretSubkeyPackets.push(packetlist[i]);
     }
   }
   if (!secretKeyPacket) {
     throw new Error('Key does not contain a secret key packet');
   }
-  return wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options);
+
+  if (!options.subkeys) {
+    options.subkeys = secretSubkeyPackets.map(() => ({}));
+  }
+
+  if (options.subkeys.length !== secretSubkeyPackets.length) {
+    throw new Error('Number of subkey options does not match number of subkeys');
+  }
+
+  options.subkeys = options.subkeys.map(function(subkey, index) { return sanitizeKeyOptions(options.subkeys[index], options); });
+
+  return wrapKeyObject([secretKeyPacket].concat(secretSubkeyPackets), options);
+
+    function sanitizeKeyOptions(options, subkeyDefaults={}) {
+    options.keyExpirationTime = options.keyExpirationTime || subkeyDefaults.keyExpirationTime;
+    options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase;
+    options.date = options.date || subkeyDefaults.date;
+
+    return options;
+  }
 }
 
-async function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) {
+async function wrapKeyObject(packets, options) {
+  const secretKeyPacket = packets[0];
+  const secretSubkeyPackets = packets.slice(1);
   // set passphrase protection
   if (options.passphrase) {
     await secretKeyPacket.encrypt(options.passphrase);
-    if (secretSubkeyPacket) {
-      await secretSubkeyPacket.encrypt(options.passphrase);
-    }
   }
 
+  await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) {
+    const subkeyPassphrase = options.subkeys[index].passphrase;
+    if (subkeyPassphrase) {
+      await secretSubkeyPacket.encrypt(subkeyPassphrase);
+    }
+  }));
+
   const packetlist = new packet.List();
 
   packetlist.push(secretKeyPacket);
@@ -1248,7 +1264,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) {
     dataToSign.key = secretKeyPacket;
     const signaturePacket = new packet.Signature(options.date);
     signaturePacket.signatureType = enums.signature.cert_generic;
-    signaturePacket.publicKeyAlgorithm = options.keyType;
+    signaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm;
     signaturePacket.hashAlgorithm = await getPreferredHashAlgo(secretKeyPacket);
     signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data];
     signaturePacket.preferredSymmetricAlgorithms = [];
@@ -1277,6 +1293,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) {
       signaturePacket.keyExpirationTime = options.keyExpirationTime;
       signaturePacket.keyNeverExpires = false;
     }
+
     await signaturePacket.sign(secretKeyPacket, dataToSign);
 
     return { userIdPacket, signaturePacket };
@@ -1287,31 +1304,41 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) {
     });
   });
 
-  if (secretSubkeyPacket) {
+  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(options.date);
+    const subkeySignaturePacket = new packet.Signature(subkeyOptions.date);
     subkeySignaturePacket.signatureType = enums.signature.subkey_binding;
-    subkeySignaturePacket.publicKeyAlgorithm = options.keyType;
+    subkeySignaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm;
     subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(secretSubkeyPacket);
-    subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage];
-    if (options.keyExpirationTime > 0) {
-      subkeySignaturePacket.keyExpirationTime = options.keyExpirationTime;
+    subkeySignaturePacket.keyFlags = subkeyOptions.sign ? enums.keyFlags.sign_data : [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage];
+    if (subkeyOptions.keyExpirationTime > 0) {
+      subkeySignaturePacket.keyExpirationTime = subkeyOptions.keyExpirationTime;
       subkeySignaturePacket.keyNeverExpires = false;
     }
     await subkeySignaturePacket.sign(secretKeyPacket, dataToSign);
 
-    packetlist.push(secretSubkeyPacket);
-    packetlist.push(subkeySignaturePacket);
+    return { secretSubkeyPacket, subkeySignaturePacket};
+  })).then(packets => {
+    packets.forEach(({ secretSubkeyPacket, subkeySignaturePacket }) => {
+      packetlist.push(secretSubkeyPacket);
+      packetlist.push(subkeySignaturePacket);
+    });
+  });
+
+  // set passphrase protection
+  if (options.passphrase) {
+    secretKeyPacket.clearPrivateParams();
   }
 
-  if (!options.unlocked) {
-    secretKeyPacket.clearPrivateParams();
-    if (secretSubkeyPacket) {
+  await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) {
+    const subkeyPassphrase = options.subkeys[index].passphrase;
+    if (subkeyPassphrase) {
       secretSubkeyPacket.clearPrivateParams();
     }
-  }
+  }));
 
   return new Key(packetlist);
 }
diff --git a/src/openpgp.js b/src/openpgp.js
index a1ef6550..f13d347d 100644
--- a/src/openpgp.js
+++ b/src/openpgp.js
@@ -99,26 +99,22 @@ export function destroyWorker() {
  * @param  {Array<Object>} 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 RSA keys: 2048 or 4096.
+ * @param  {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires
  * @param  {String} curve            (optional) elliptic curve for ECC keys:
  *                                              curve25519, p256, p384, p521, secp256k1,
  *                                              brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1.
- * @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
  * @param  {Date} date               (optional) override the creation date of the key and the key signatures
+ * @param  {Array<Object>} subkeys   (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
+ *                                              sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
  * @returns {Promise<Object>}         The generated key object in the form:
  *                                     { key:Key, privateKeyArmored:String, publicKeyArmored:String }
  * @async
  * @static
  */
 
-export function generateKey({
-  userIds=[], passphrase, numBits=2048, unlocked=false, keyExpirationTime=0, curve="", date=new Date()
-} = {}) {
+export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpirationTime=0, curve="", date=new Date(), subkeys=[{}] }) {
   userIds = formatUserIds(userIds);
-  const options = {
-    userIds, passphrase, numBits, unlocked, keyExpirationTime, curve, date
-  };
-
+  const options = { userIds, passphrase, numBits, keyExpirationTime, curve, date, subkeys };
   if (util.getWebCryptoAll() && numBits < 2048) {
     throw new Error('numBits should be 2048 or 4096, found: ' + numBits);
   }
@@ -141,22 +137,15 @@ export function generateKey({
  * @param  {Key} privateKey          private key to reformat
  * @param  {Array<Object>} 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
  * @returns {Promise<Object>}         The generated key object in the form:
  *                                     { key:Key, privateKeyArmored:String, publicKeyArmored:String }
  * @async
  * @static
  */
-export function reformatKey({
-  privateKey, userIds=[], passphrase="", unlocked=false, keyExpirationTime=0
-} = {}) {
+export function reformatKey({privateKey, userIds=[], passphrase="", keyExpirationTime=0, date}) {
   userIds = formatUserIds(userIds);
-
-  const options = {
-    privateKey, userIds, passphrase, unlocked, keyExpirationTime
-  };
-
+  const options = { privateKey, userIds, passphrase, keyExpirationTime, date};
   if (asyncProxy) {
     return asyncProxy.delegate('reformatKey', options);
   }

From e4bd27ce2f7336851d89f378b749abef144e1f1a Mon Sep 17 00:00:00 2001
From: Sanjana Rajan <srajan1@stanford.edu>
Date: Sun, 15 Apr 2018 12:38:36 -0700
Subject: [PATCH 2/3] tests

---
 src/key.js              | 28 +++++++++---------
 test/general/key.js     | 64 ++++++++++++++++++++++++++++++++++++++++-
 test/general/openpgp.js | 34 +++++++---------------
 3 files changed, 87 insertions(+), 39 deletions(-)

diff --git a/src/key.js b/src/key.js
index 8fdebd7c..83246c8f 100644
--- a/src/key.js
+++ b/src/key.js
@@ -1101,7 +1101,7 @@ export function readArmored(armoredText) {
  * @param {String}  options.passphrase The passphrase used to encrypt the resulting private key
  * @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  {String} curve            (optional) elliptic curve for ECC keys
  * @param  {Date} date         Override the creation date of the key and the key signatures
  * @param  {Array<Object>} subkeys   (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
  *                                              sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
@@ -1110,13 +1110,13 @@ export function readArmored(armoredText) {
  * @static
  */
 export async function generate(options) {
-
+  options.sign = true; // primary key is always a signing key
   options = sanitizeKeyOptions(options);
   options.subkeys = options.subkeys.map(function(subkey, index) { return sanitizeKeyOptions(options.subkeys[index], options); });
 
   let promises = [generateSecretKey(options)];
   promises = promises.concat(options.subkeys.map(generateSecretSubkey));
-  return Promise.all(promises).then(packets => wrapKeyObject(packets, options));
+  return Promise.all(promises).then(packets => wrapKeyObject(packets[0], packets.slice(1), options));
 
   function sanitizeKeyOptions(options, subkeyDefaults={}) {
     options.curve = options.curve || subkeyDefaults.curve;
@@ -1134,17 +1134,18 @@ export async function generate(options) {
         throw new Error('Not valid curve.');
       }
       if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
-        if (!subkeyDefaults.algorithm || options.sign) {
-          options.algorithm = options.algorithm || enums.publicKey.eddsa;
+        if (options.sign) {
+          options.algorithm = enums.publicKey.eddsa;
           options.curve = enums.curve.ed25519;
         } else {
-          options.algorithm = options.algorithm || enums.publicKey.ecdh;
+          options.algorithm = enums.publicKey.ecdh;
           options.curve = enums.curve.curve25519;
         }
       } else {
-        options.algorithm = options.algorithm || (!subkeyDefaults.algorithm ? enums.publicKey.ecdsa : enums.publicKey.ecdh);
-        if (options.algorithm !== enums.publicKey.ecdh && options.algorithm !== enums.publicKey.ecdsa) {
-          throw new Error('Invalid algorithm for curve');
+        if (options.sign) {
+          options.algorithm = enums.publicKey.ecdsa;
+        } else {
+          options.algorithm = enums.publicKey.ecdh;
         }
       }
     } else if (options.numBits) {
@@ -1225,9 +1226,9 @@ export async function reformat(options) {
 
   options.subkeys = options.subkeys.map(function(subkey, index) { return sanitizeKeyOptions(options.subkeys[index], options); });
 
-  return wrapKeyObject([secretKeyPacket].concat(secretSubkeyPackets), options);
+  return wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options);
 
-    function sanitizeKeyOptions(options, subkeyDefaults={}) {
+  function sanitizeKeyOptions(options, subkeyDefaults={}) {
     options.keyExpirationTime = options.keyExpirationTime || subkeyDefaults.keyExpirationTime;
     options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase;
     options.date = options.date || subkeyDefaults.date;
@@ -1236,9 +1237,7 @@ export async function reformat(options) {
   }
 }
 
-async function wrapKeyObject(packets, options) {
-  const secretKeyPacket = packets[0];
-  const secretSubkeyPackets = packets.slice(1);
+async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
   // set passphrase protection
   if (options.passphrase) {
     await secretKeyPacket.encrypt(options.passphrase);
@@ -1293,7 +1292,6 @@ async function wrapKeyObject(packets, options) {
       signaturePacket.keyExpirationTime = options.keyExpirationTime;
       signaturePacket.keyNeverExpires = false;
     }
-
     await signaturePacket.sign(secretKeyPacket, dataToSign);
 
     return { userIdPacket, signaturePacket };
diff --git a/test/general/key.js b/test/general/key.js
index 425962a6..e9d09db5 100644
--- a/test/general/key.js
+++ b/test/general/key.js
@@ -1081,7 +1081,6 @@ describe('Key', function() {
     const opt = {
       userIds: { name: 'Test User', email: 'text@example.com' },
       passphrase: 'secret',
-      unlocked: true,
       date: past
     };
 
@@ -1108,6 +1107,47 @@ describe('Key', function() {
     });
   });
 
+  it('Generate key - two subkeys with default values', function() {
+    const userId = 'test <a@b.com>';
+    const opt = {curve: 'curve25519', userIds: [userId], passphrase: '123', subkeys:[{},{}]};
+    return openpgp.generateKey(opt).then(function(key) {
+      key = key.key;
+      expect(key.users.length).to.equal(1);
+      expect(key.users[0].userId.userid).to.equal(userId);
+      expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true;
+      expect(key.subKeys).to.have.lengthOf(2);
+      expect(key.subKeys[0].subKey.algorithm).to.equal('ecdh');
+      expect(key.subKeys[1].subKey.algorithm).to.equal('ecdh');
+    });
+  });
+
+  it('Generate key - one signing subkey', function() {
+    const userId = 'test <a@b.com>';
+    const opt = {curve: 'curve25519', userIds: [userId], passphrase: '123', subkeys:[{}, {sign: true}]};
+    return openpgp.generateKey(opt).then(function(key) {
+      key = key.key;
+      expect(key.users.length).to.equal(1);
+      expect(key.users[0].userId.userid).to.equal(userId);
+      expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true;
+      expect(key.subKeys).to.have.lengthOf(2);
+      expect(key.subKeys[0].subKey.algorithm).to.equal('ecdh');
+      expect(key.subKeys[1].subKey.algorithm).to.equal('eddsa');
+    });
+  });
+
+  it('Generate key - override main key options for subkey', function() {
+    const userId = 'test <a@b.com>';
+    const opt = {numBits: 2048, userIds: [userId], passphrase: '123', subkeys:[{curve: 'curve25519'}]};
+    return openpgp.generateKey(opt).then(function(key) {
+      key = key.key;
+      expect(key.users.length).to.equal(1);
+      expect(key.users[0].userId.userid).to.equal(userId);
+      expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true;
+      expect(key.primaryKey.algorithm).to.equal('rsa_encrypt_sign');
+      expect(key.subKeys[0].subKey.algorithm).to.equal('ecdh');
+    });
+  });
+
   it('Encrypt key with new passphrase', async function() {
     const userId = 'test <a@b.com>';
     const opt = {numBits: 512, userIds: userId, passphrase: 'passphrase'};
@@ -1261,6 +1301,28 @@ describe('Key', function() {
     });
   });
 
+  it('Reformat key with two subkeys with passphrase', function() {
+    const userId1 = 'test <a@b.com>';
+    const userId2 = 'test <b@c.com>';
+    const now = openpgp.util.normalizeDate(new Date());
+    const before = openpgp.util.normalizeDate(new Date(0));
+    const opt1 = {curve: 'curve25519', userIds: [userId1], date: now};
+    return openpgp.generateKey(opt1).then(function(newKey) {
+      newKey = newKey.key;
+      expect(newKey.users[0].userId.userid).to.equal(userId1);
+      expect(+newKey.primaryKey.created).to.equal(+now);
+      expect(+newKey.subKeys[0].subKey.created).to.equal(+now);
+      expect(+newKey.subKeys[0].bindingSignatures[0].created).to.equal(+now);
+      const opt2 = {privateKey: newKey, userIds: [userId2], date: before};
+      return openpgp.reformatKey(opt2).then(function(refKey) {
+        refKey = refKey.key;
+        expect(refKey.users.length).to.equal(1);
+        expect(refKey.users[0].userId.userid).to.equal(userId2);
+        expect(+refKey.subKeys[0].bindingSignatures[0].created).to.equal(+before);
+      });
+    });
+  });
+
   it('Reformat key with no subkey without passphrase', function() {
     const userId = 'test1 <a@b.com>';
     const keys = openpgp.key.readArmored(key_without_subkey).keys;
diff --git a/test/general/openpgp.js b/test/general/openpgp.js
index 2ff92576..59901a86 100644
--- a/test/general/openpgp.js
+++ b/test/general/openpgp.js
@@ -483,22 +483,22 @@ describe('OpenPGP.js public api tests', function() {
     });
 
     it('should have default params set', function() {
-      const now = new Date();
+      const now = openpgp.util.normalizeDate(new Date());
       const opt = {
         userIds: { name: 'Test User', email: 'text@example.com' },
         passphrase: 'secret',
-        unlocked: true,
-        date: now
+        date: now,
+        subkeys: []
       };
       return openpgp.generateKey(opt).then(function(newKey) {
         expect(keyGenStub.withArgs({
           userIds: ['Test User <text@example.com>'],
           passphrase: 'secret',
           numBits: 2048,
-          unlocked: true,
           keyExpirationTime: 0,
           curve: "",
-          date: now
+          date: now,
+          subkeys: [],
         }).calledOnce).to.be.true;
         expect(newKey.key).to.exist;
         expect(newKey.privateKeyArmored).to.exist;
@@ -506,23 +506,6 @@ describe('OpenPGP.js public api tests', function() {
       });
     });
 
-    it('should work for no params', function() {
-      const now = new Date();
-
-      return openpgp.generateKey({date: now}).then(function(newKey) {
-        expect(keyGenStub.withArgs({
-          userIds: [],
-          passphrase: undefined,
-          numBits: 2048,
-          unlocked: false,
-          keyExpirationTime: 0,
-          curve: "",
-          date: now
-        }).calledOnce).to.be.true;
-        expect(newKey.key).to.exist;
-      });
-    });
-
     it('should delegate to async proxy', function() {
       const workerStub = {
         postMessage: function() {}
@@ -533,7 +516,12 @@ describe('OpenPGP.js public api tests', function() {
       const proxyGenStub = stub(openpgp.getWorker(), 'delegate');
       getWebCryptoAllStub.returns();
 
-      openpgp.generateKey();
+      const opt = {
+        userIds: { name: 'Test User', email: 'text@example.com' },
+        passphrase: 'secret',
+        subkeys: []
+      };
+      openpgp.generateKey(opt);
       expect(proxyGenStub.calledOnce).to.be.true;
       expect(keyGenStub.calledOnce).to.be.false;
     });

From 2bd540026ff30995e8093be33024277d24b4bbcf Mon Sep 17 00:00:00 2001
From: Sanjana Rajan <srajan1@stanford.edu>
Date: Mon, 16 Apr 2018 12:15:11 -0700
Subject: [PATCH 3/3] add multiple passphrase options to key encrypt and
 decrypt

---
 src/key.js     | 36 ++++++++++++++++++++++++++++--------
 src/openpgp.js | 12 ++++++------
 2 files changed, 34 insertions(+), 14 deletions(-)

diff --git a/src/key.js b/src/key.js
index 83246c8f..888be85d 100644
--- a/src/key.js
+++ b/src/key.js
@@ -341,18 +341,24 @@ Key.prototype.getEncryptionKeyPacket = async function(keyId, date=new Date()) {
 
 /**
  * Encrypts all secret key and subkey packets matching keyId
+ * @param  {String|Array<String>} passphrases - if multiple passphrases, then should be in same order as packets each should encrypt
  * @param  {module:type/keyid} keyId
- * @param  {String} passphrase
  * @returns {Promise<Array<module:packet.SecretKey|module:packet.SecretSubkey>>}
  * @async
  */
-Key.prototype.encrypt = async function(passphrase, keyId=null) {
+Key.prototype.encrypt = async function(passphrases, keyId=null) {
   if (!this.isPrivate()) {
     throw new Error("Nothing to encrypt in a public key");
   }
 
-  return Promise.all(this.getKeyPackets(keyId).map(async function(keyPacket) {
-    await keyPacket.encrypt(passphrase);
+  const keyPackets = this.getKeyPackets(keyId);
+  passphrases = util.isArray(passphrases) ? passphrases : new Array(keyPackets.length).fill(passphrases);
+  if (passphrases.length !== keyPackets.length) {
+    throw new Error("Invalid number of passphrases for key");
+  }
+
+  return Promise.all(keyPackets.map(async function(keyPacket, i) {
+    await keyPacket.encrypt(passphrases[i]);
     await keyPacket.clearPrivateParams();
     return keyPacket;
   }));
@@ -360,18 +366,32 @@ Key.prototype.encrypt = async function(passphrase, keyId=null) {
 
 /**
  * Decrypts all secret key and subkey packets matching keyId
- * @param  {String} passphrase
+ * @param  {String|Array<String>} passphrases
  * @param  {module:type/keyid} keyId
  * @returns {Promise<Boolean>} true if all matching key and subkey packets decrypted successfully
  * @async
  */
-Key.prototype.decrypt = async function(passphrase, keyId=null) {
+Key.prototype.decrypt = async function(passphrases, keyId=null) {
   if (!this.isPrivate()) {
     throw new Error("Nothing to decrypt in a public key");
   }
+  passphrases = util.isArray(passphrases) ? passphrases : [passphrases];
 
   const results = await Promise.all(this.getKeyPackets(keyId).map(async function(keyPacket) {
-    return keyPacket.decrypt(passphrase);
+    let decrypted = false;
+    let error = null;
+    await Promise.all(passphrases.map(async function(passphrase) {
+      try {
+        await keyPacket.decrypt(passphrase);
+        decrypted = true;
+      } catch (e) {
+        error = e;
+      }
+    }));
+    if (!decrypted) {
+      throw error;
+    }
+    return decrypted;
   }));
   return results.every(result => result === true);
 };
@@ -1121,7 +1141,7 @@ export async function generate(options) {
   function sanitizeKeyOptions(options, subkeyDefaults={}) {
     options.curve = options.curve || subkeyDefaults.curve;
     options.numBits = options.numBits || subkeyDefaults.numBits;
-    options.keyExpirationTime = options.keyExpirationTime >= 0 ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime;
+    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;
 
diff --git a/src/openpgp.js b/src/openpgp.js
index f13d347d..6a7aa6ed 100644
--- a/src/openpgp.js
+++ b/src/openpgp.js
@@ -161,9 +161,9 @@ export function reformatKey({privateKey, userIds=[], passphrase="", keyExpiratio
 
 /**
  * Unlock a private key with your passphrase.
- * @param  {Key} privateKey      the private key that is to be decrypted
- * @param  {String} passphrase   the user's passphrase chosen during key generation
- * @returns {Promise<Object>}     the unlocked key object in the form: { key:Key }
+ * @param  {Key} privateKey                    the private key that is to be decrypted
+ * @param  {String|Array<String>} passphrase   the user's passphrase(s) chosen during key generation
+ * @returns {Promise<Object>}                  the unlocked key object in the form: { key:Key }
  * @async
  */
 export function decryptKey({ privateKey, passphrase }) {
@@ -182,9 +182,9 @@ export function decryptKey({ privateKey, passphrase }) {
 
 /**
  * Lock a private key with your passphrase.
- * @param  {Key} privateKey      the private key that is to be decrypted
- * @param  {String} passphrase   the user's passphrase chosen during key generation
- * @returns {Promise<Object>}     the locked key object in the form: { key:Key }
+ * @param  {Key} privateKey                      the private key that is to be decrypted
+ * @param  {String|Array<String>} passphrase     the user's passphrase(s) chosen during key generation
+ * @returns {Promise<Object>}                    the locked key object in the form: { key:Key }
  * @async
  */
 export function encryptKey({ privateKey, passphrase }) {