From e24b46192dedbe60a29ac8cb886049294d37d303 Mon Sep 17 00:00:00 2001
From: Daniel Huigens <d.huigens@protonmail.com>
Date: Wed, 11 Apr 2018 16:37:40 +0200
Subject: [PATCH] Only AEAD-protect when target keys support it

---
 src/key.js              | 26 +++++++++----
 src/message.js          | 19 ++++++----
 test/general/key.js     | 15 ++++++++
 test/general/openpgp.js | 83 +++++++++++++++++++++++++++++++++++++++--
 4 files changed, 125 insertions(+), 18 deletions(-)

diff --git a/src/key.js b/src/key.js
index 30b94b4b..65da1173 100644
--- a/src/key.js
+++ b/src/key.js
@@ -1402,14 +1402,15 @@ function getExpirationTime(keyPacket, signature) {
 /**
  * Returns the preferred signature hash algorithm of a key
  * @param  {object} key
+ * @param  {Date} date (optional) use the given date for verification instead of the current time
  * @returns {Promise<String>}
  * @async
  */
-export async function getPreferredHashAlgo(key) {
+export async function getPreferredHashAlgo(key, date) {
   let hash_algo = config.prefer_hash_algorithm;
   let pref_algo = hash_algo;
   if (key instanceof Key) {
-    const primaryUser = await key.getPrimaryUser();
+    const primaryUser = await key.getPrimaryUser(date);
     if (primaryUser && primaryUser.selfCertification.preferredHashAlgorithms) {
       [pref_algo] = primaryUser.selfCertification.preferredHashAlgorithms;
       hash_algo = crypto.hash.getHashByteLength(hash_algo) <= crypto.hash.getHashByteLength(pref_algo) ?
@@ -1437,13 +1438,14 @@ export async function getPreferredHashAlgo(key) {
 /**
  * Returns the preferred symmetric algorithm for a set of keys
  * @param  {Array<module:key.Key>} keys Set of keys
+ * @param  {Date} date (optional) use the given date for verification instead of the current time
  * @returns {Promise<module:enums.symmetric>}   Preferred symmetric algorithm
  * @async
  */
-export async function getPreferredSymAlgo(keys) {
+export async function getPreferredSymAlgo(keys, date) {
   const prioMap = {};
   await Promise.all(keys.map(async function(key) {
-    const primaryUser = await key.getPrimaryUser();
+    const primaryUser = await key.getPrimaryUser(date);
     if (!primaryUser || !primaryUser.selfCertification.preferredSymmetricAlgorithms) {
       return config.encryption_cipher;
     }
@@ -1471,13 +1473,20 @@ export async function getPreferredSymAlgo(keys) {
 /**
  * Returns the preferred aead algorithm for a set of keys
  * @param  {Array<module:key.Key>} keys Set of keys
- * @returns {Promise<module:enums.aead>}   Preferred aead algorithm
+ * @param  {Date} date (optional) use the given date for verification instead of the current time
+ * @returns {Promise<module:enums.aead>} Preferred aead algorithm, or null if the public keys do not support aead
  * @async
  */
-export async function getPreferredAeadAlgo(keys) {
+export async function getPreferredAeadAlgo(keys, date) {
+  let supports_aead = true;
   const prioMap = {};
   await Promise.all(keys.map(async function(key) {
-    const primaryUser = await key.getPrimaryUser();
+    const primaryUser = await key.getPrimaryUser(date);
+    if (!primaryUser || !primaryUser.selfCertification.features ||
+        !(primaryUser.selfCertification.features[0] & enums.features.aead)) {
+      supports_aead = false;
+      return;
+    }
     if (!primaryUser || !primaryUser.selfCertification.preferredAeadAlgorithms) {
       return config.aead_mode;
     }
@@ -1487,6 +1496,9 @@ export async function getPreferredAeadAlgo(keys) {
       entry.count++;
     });
   }));
+  if (!supports_aead) {
+    return null;
+  }
   let prefAlgo = { prio: 0, algo: config.aead_mode };
   for (const algo in prioMap) {
     try {
diff --git a/src/message.js b/src/message.js
index 57f43ba4..aca0de85 100644
--- a/src/message.js
+++ b/src/message.js
@@ -93,7 +93,7 @@ Message.prototype.getSigningKeyIds = function() {
  * Decrypt the message. Either a private key, a session key, or a password must be specified.
  * @param  {Array<Key>} privateKeys     (optional) private keys with decrypted secret data
  * @param  {Array<String>} passwords    (optional) passwords used to decrypt
- * @param  {Array<Object>} sessionKeys  (optional) session keys in the form: { data:Uint8Array, algorithm:String }
+ * @param  {Array<Object>} sessionKeys  (optional) session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] }
  * @returns {Promise<Message>}             new message with decrypted content
  * @async
  */
@@ -244,7 +244,7 @@ Message.prototype.getText = function() {
  * Encrypt the message either with public keys, passwords, or both at once.
  * @param  {Array<Key>} keys           (optional) public key(s) for message encryption
  * @param  {Array<String>} passwords   (optional) password(s) for message encryption
- * @param  {Object} sessionKey         (optional) session key in the form: { data:Uint8Array, algorithm:String }
+ * @param  {Object} sessionKey         (optional) session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] }
  * @param  {Boolean} wildcard          (optional) use a key ID of 0 instead of the public key IDs
  * @param  {Date} date                 (optional) override the creation date of the literal package
  * @returns {Promise<Message>}                   new message with encrypted content
@@ -260,11 +260,14 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
       throw new Error('Invalid session key for encryption.');
     }
     symAlgo = sessionKey.algorithm;
-    aeadAlgo = sessionKey.aeadAlgorithm || config.aead_mode;
+    aeadAlgo = sessionKey.aeadAlgorithm;
     sessionKey = sessionKey.data;
   } else if (keys && keys.length) {
-    symAlgo = enums.read(enums.symmetric, await getPreferredSymAlgo(keys));
-    aeadAlgo = enums.read(enums.aead, await getPreferredAeadAlgo(keys));
+    symAlgo = enums.read(enums.symmetric, await getPreferredSymAlgo(keys, date));
+    aeadAlgo = await getPreferredAeadAlgo(keys, date);
+    if (aeadAlgo) {
+      aeadAlgo = enums.read(enums.aead, aeadAlgo);
+    }
   } else if (passwords && passwords.length) {
     symAlgo = enums.read(enums.symmetric, config.encryption_cipher);
     aeadAlgo = enums.read(enums.aead, config.aead_mode);
@@ -278,7 +281,7 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
 
   const msg = await encryptSessionKey(sessionKey, symAlgo, aeadAlgo, keys, passwords, wildcard, date);
 
-  if (config.aead_protect) {
+  if (config.aead_protect && (config.aead_protect_version !== 4 || aeadAlgo)) {
     symEncryptedPacket = new packet.SymEncryptedAEADProtected();
     symEncryptedPacket.aeadAlgorithm = aeadAlgo;
   } else if (config.integrity_protect) {
@@ -423,7 +426,7 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new
     }
     const onePassSig = new packet.OnePassSignature();
     onePassSig.type = signatureType;
-    onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey);
+    onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, date);
     onePassSig.publicKeyAlgorithm = signingKeyPacket.algorithm;
     onePassSig.signingKeyId = signingKeyPacket.getKeyId();
     if (i === privateKeys.length - 1) {
@@ -507,7 +510,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig
     const signaturePacket = new packet.Signature(date);
     signaturePacket.signatureType = signatureType;
     signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
-    signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey);
+    signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, date);
     await signaturePacket.sign(signingKeyPacket, literalDataPacket);
     return signaturePacket;
   })).then(signatureList => {
diff --git a/test/general/key.js b/test/general/key.js
index 1e51bc37..ec21ac20 100644
--- a/test/general/key.js
+++ b/test/general/key.js
@@ -1199,6 +1199,7 @@ p92yZgB3r2+f6/GIe2+7
   it('getPreferredAeadAlgo() - one key - OCB', async function() {
     const key1 = openpgp.key.readArmored(twoKeys).keys[0];
     const primaryUser = await key1.getPrimaryUser();
+    primaryUser.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
     primaryUser.selfCertification.preferredAeadAlgorithms = [2,1];
     const prefAlgo = await openpgp.key.getPreferredAeadAlgo([key1]);
     expect(prefAlgo).to.equal(openpgp.enums.aead.ocb);
@@ -1209,11 +1210,25 @@ p92yZgB3r2+f6/GIe2+7
     const key1 = keys[0];
     const key2 = keys[1];
     const primaryUser = await key1.getPrimaryUser();
+    primaryUser.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
     primaryUser.selfCertification.preferredAeadAlgorithms = [2,1];
+    const primaryUser2 = await key2.getPrimaryUser();
+    primaryUser2.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
     const prefAlgo = await openpgp.key.getPreferredAeadAlgo([key1, key2]);
     expect(prefAlgo).to.equal(openpgp.config.aead_mode);
   });
 
+  it('getPreferredAeadAlgo() - two key - one with no support', async function() {
+    const keys = openpgp.key.readArmored(twoKeys).keys;
+    const key1 = keys[0];
+    const key2 = keys[1];
+    const primaryUser = await key1.getPrimaryUser();
+    primaryUser.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
+    primaryUser.selfCertification.preferredAeadAlgorithms = [2,1];
+    const prefAlgo = await openpgp.key.getPreferredAeadAlgo([key1, key2]);
+    expect(prefAlgo).to.be.null;
+  });
+
   it('Preferences of generated key', function() {
     const testPref = function(key) {
       // key flags
diff --git a/test/general/openpgp.js b/test/general/openpgp.js
index a6898a23..d08bffa9 100644
--- a/test/general/openpgp.js
+++ b/test/general/openpgp.js
@@ -604,6 +604,7 @@ describe('OpenPGP.js public api tests', function() {
       publicKey = openpgp.key.readArmored(pub_key);
       expect(publicKey.keys).to.have.length(1);
       expect(publicKey.err).to.not.exist;
+      publicKeyNoAEAD = openpgp.key.readArmored(pub_key);
       privateKey = openpgp.key.readArmored(priv_key);
       expect(privateKey.keys).to.have.length(1);
       expect(privateKey.err).to.not.exist;
@@ -679,6 +680,11 @@ describe('OpenPGP.js public api tests', function() {
         openpgp.config.use_native = false;
         openpgp.config.aead_protect = true;
         openpgp.config.aead_protect_version = 4;
+
+        // Monkey-patch AEAD feature flag
+        publicKey.keys[0].users[0].selfCertifications[0].features = [7];
+        publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
+        publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
       }
     });
 
@@ -688,6 +694,11 @@ describe('OpenPGP.js public api tests', function() {
         openpgp.config.use_native = true;
         openpgp.config.aead_protect = true;
         openpgp.config.aead_protect_version = 4;
+
+        // Monkey-patch AEAD feature flag
+        publicKey.keys[0].users[0].selfCertifications[0].features = [7];
+        publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
+        publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
       }
     });
 
@@ -697,6 +708,11 @@ describe('OpenPGP.js public api tests', function() {
         openpgp.config.aead_protect = true;
         openpgp.config.aead_protect_version = 4;
         openpgp.config.aead_mode = openpgp.enums.aead.ocb;
+
+        // Monkey-patch AEAD feature flag
+        publicKey.keys[0].users[0].selfCertifications[0].features = [7];
+        publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
+        publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
       }
     });
 
@@ -1020,20 +1036,21 @@ describe('OpenPGP.js public api tests', function() {
           return openpgp.encrypt(encOpt).then(function (encrypted) {
             expect(encrypted.data).to.match(/^-----BEGIN PGP MESSAGE/);
             decOpt.message = openpgp.message.readArmored(encrypted.data);
+            expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect && openpgp.config.aead_protect_version !== 4);
             return openpgp.decrypt(decOpt);
           }).then(function (decrypted) {
             expect(decrypted.data).to.equal(plaintext);
           });
         });
 
-        it('should encrypt using custom session key and decrypt using private key', function () {
+        it('should encrypt using custom session key and decrypt using private key', async function () {
           const sessionKey = {
-            data: openpgp.crypto.generateSessionKey('aes128'),
+            data: await openpgp.crypto.generateSessionKey('aes128'),
             algorithm: 'aes128'
           };
           const encOpt = {
             data: plaintext,
-            sessionKeys: sessionKey,
+            sessionKey: sessionKey,
             publicKeys: publicKey.keys
           };
           const decOpt = {
@@ -1042,6 +1059,7 @@ describe('OpenPGP.js public api tests', function() {
           return openpgp.encrypt(encOpt).then(function (encrypted) {
             expect(encrypted.data).to.match(/^-----BEGIN PGP MESSAGE/);
             decOpt.message = openpgp.message.readArmored(encrypted.data);
+            expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect && openpgp.config.aead_protect_version !== 4);
             return openpgp.decrypt(decOpt);
           }).then(function (decrypted) {
             expect(decrypted.data).to.equal(plaintext);
@@ -1060,6 +1078,7 @@ describe('OpenPGP.js public api tests', function() {
           };
           return openpgp.encrypt(encOpt).then(function (encrypted) {
             decOpt.message = openpgp.message.readArmored(encrypted.data);
+            expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect);
             return openpgp.decrypt(decOpt);
           }).then(async function (decrypted) {
             expect(decrypted.data).to.equal(plaintext);
@@ -1070,6 +1089,63 @@ describe('OpenPGP.js public api tests', function() {
           });
         });
 
+        it('should encrypt/sign and decrypt/verify (no AEAD support)', function () {
+          const encOpt = {
+            data: plaintext,
+            publicKeys: publicKeyNoAEAD.keys,
+            privateKeys: privateKey.keys
+          };
+          const decOpt = {
+            privateKeys: privateKey.keys[0],
+            publicKeys: publicKeyNoAEAD.keys
+          };
+          return openpgp.encrypt(encOpt).then(function (encrypted) {
+            decOpt.message = openpgp.message.readArmored(encrypted.data);
+            expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect && openpgp.config.aead_protect_version !== 4);
+            return openpgp.decrypt(decOpt);
+          }).then(async function (decrypted) {
+            expect(decrypted.data).to.equal(plaintext);
+            expect(decrypted.signatures[0].valid).to.be.true;
+            const keyPacket = await privateKey.keys[0].getSigningKeyPacket();
+            expect(decrypted.signatures[0].keyid.toHex()).to.equal(keyPacket.getKeyId().toHex());
+            expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
+          });
+        });
+
+        it('should encrypt/sign and decrypt/verify with generated key', function () {
+          const genOpt = {
+            userIds: [{ name: 'Test User', email: 'text@example.com' }],
+            numBits: 512
+          };
+          if (openpgp.util.getWebCryptoAll()) { genOpt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys
+
+          return openpgp.generateKey(genOpt).then(function(newKey) {
+            const newPublicKey = openpgp.key.readArmored(newKey.publicKeyArmored);
+            const newPrivateKey = openpgp.key.readArmored(newKey.privateKeyArmored);
+
+            const encOpt = {
+              data: plaintext,
+              publicKeys: newPublicKey.keys,
+              privateKeys: newPrivateKey.keys
+            };
+            const decOpt = {
+              privateKeys: newPrivateKey.keys[0],
+              publicKeys: newPublicKey.keys
+            };
+            return openpgp.encrypt(encOpt).then(function (encrypted) {
+              decOpt.message = openpgp.message.readArmored(encrypted.data);
+              expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect);
+              return openpgp.decrypt(decOpt);
+            }).then(async function (decrypted) {
+              expect(decrypted.data).to.equal(plaintext);
+              expect(decrypted.signatures[0].valid).to.be.true;
+              const keyPacket = await newPrivateKey.keys[0].getSigningKeyPacket();
+              expect(decrypted.signatures[0].keyid.toHex()).to.equal(keyPacket.getKeyId().toHex());
+              expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
+            });
+          });
+        });
+
         it('should encrypt/sign and decrypt/verify with null string input', function () {
           const encOpt = {
             data: '',
@@ -1719,6 +1795,7 @@ describe('OpenPGP.js public api tests', function() {
           const pubKeyDE = openpgp.key.readArmored(pub_key_de).keys[0];
           const privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0];
           await privKeyDE.decrypt(passphrase);
+          pubKeyDE.users[0].selfCertifications[0].features = [7]; // Monkey-patch AEAD feature flag
           return openpgp.encrypt({
             publicKeys: pubKeyDE,
             privateKeys: privKeyDE,