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} * @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} keys Set of keys + * @param {Date} date (optional) use the given date for verification instead of the current time * @returns {Promise} 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} keys Set of keys - * @returns {Promise} Preferred aead algorithm + * @param {Date} date (optional) use the given date for verification instead of the current time + * @returns {Promise} 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} privateKeys (optional) private keys with decrypted secret data * @param {Array} passwords (optional) passwords used to decrypt - * @param {Array} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String } + * @param {Array} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] } * @returns {Promise} 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} keys (optional) public key(s) for message encryption * @param {Array} 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} 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,