diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index 7d5f4a91..c7af046c 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -32,93 +32,93 @@ const webCrypto = util.getWebCrypto(); // no GCM support in IE11, Safari 9 const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); +const blockLength = 16; const ivLength = 12; // size of the IV in bytes -const TAG_LEN = 16; // size of the tag in bytes +const tagLength = 16; // size of the tag in bytes const ALGO = 'AES-GCM'; /** - * Encrypt plaintext input. + * Class to en/decrypt using GCM mode. * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' - * @param {Uint8Array} plaintext The cleartext input to be encrypted * @param {Uint8Array} key The encryption key - * @param {Uint8Array} iv The initialization vector (12 bytes) - * @returns {Promise} The ciphertext output */ -function encrypt(cipher, plaintext, key, iv) { +async function GCM(cipher, key) { if (cipher.substr(0, 3) !== 'aes') { - return Promise.reject(new Error('GCM mode supports only AES cipher')); + throw new Error('GCM mode supports only AES cipher'); } if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support - return webEncrypt(plaintext, key, iv); - } else if (util.getNodeCrypto()) { // Node crypto library - return nodeEncrypt(plaintext, key, iv); - } // asm.js fallback - return Promise.resolve(AES_GCM.encrypt(plaintext, key, iv)); + key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']); + + return { + encrypt: async function(pt, iv, adata=new Uint8Array()) { + const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata }, key, pt); + return new Uint8Array(ct); + }, + + decrypt: async function(ct, iv, adata=new Uint8Array()) { + const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata }, key, ct); + return new Uint8Array(pt); + } + }; + } + + if (util.getNodeCrypto()) { // Node crypto library + key = new Buffer(key); + + return { + encrypt: async function(pt, iv, adata=new Uint8Array()) { + pt = new Buffer(pt); + iv = new Buffer(iv); + adata = new Buffer(adata); + const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-gcm', key, iv); + en.setAAD(adata); + const ct = Buffer.concat([en.update(pt), en.final(), en.getAuthTag()]); // append auth tag to ciphertext + return new Uint8Array(ct); + }, + + decrypt: async function(ct, iv, adata=new Uint8Array()) { + ct = new Buffer(ct); + iv = new Buffer(iv); + adata = new Buffer(adata); + const de = new nodeCrypto.createDecipheriv('aes-' + (key.length * 8) + '-gcm', key, iv); + de.setAAD(adata); + de.setAuthTag(ct.slice(ct.length - tagLength, ct.length)); // read auth tag at end of ciphertext + const pt = Buffer.concat([de.update(ct.slice(0, ct.length - tagLength)), de.final()]); + return new Uint8Array(pt); + } + }; + } + + return { + encrypt: async function(pt, iv, adata) { + return AES_GCM.encrypt(pt, key, iv, adata); + }, + + decrypt: async function(ct, iv, adata) { + return AES_GCM.decrypt(ct, key, iv, adata); + } + }; } + /** - * Decrypt ciphertext input. - * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' - * @param {Uint8Array} ciphertext The ciphertext input to be decrypted - * @param {Uint8Array} key The encryption key + * Get GCM nonce. Note: this operation is not defined by the standard. + * A future version of the standard may define GCM mode differently, + * hopefully under a different ID (we use Private/Experimental algorithm + * ID 100) so that we can maintain backwards compatibility. * @param {Uint8Array} iv The initialization vector (12 bytes) - * @returns {Promise} The plaintext output + * @param {Uint8Array} chunkIndex The chunk index (8 bytes) */ -function decrypt(cipher, ciphertext, key, iv) { - if (cipher.substr(0, 3) !== 'aes') { - return Promise.reject(new Error('GCM mode supports only AES cipher')); +GCM.getNonce = function(iv, chunkIndex) { + const nonce = iv.slice(); + for (let i = 0; i < chunkIndex.length; i++) { + nonce[4 + i] ^= chunkIndex[i]; } - - if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support - return webDecrypt(ciphertext, key, iv); - } else if (util.getNodeCrypto()) { // Node crypto library - return nodeDecrypt(ciphertext, key, iv); - } // asm.js fallback - return Promise.resolve(AES_GCM.decrypt(ciphertext, key, iv)); -} - -export default { - ivLength, - encrypt, - decrypt + return nonce; }; +GCM.blockLength = blockLength; +GCM.ivLength = ivLength; -////////////////////////// -// // -// Helper functions // -// // -////////////////////////// - - -function webEncrypt(pt, key, iv) { - return webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt']) - .then(keyObj => webCrypto.encrypt({ name: ALGO, iv }, keyObj, pt)) - .then(ct => new Uint8Array(ct)); -} - -function webDecrypt(ct, key, iv) { - return webCrypto.importKey('raw', key, { name: ALGO }, false, ['decrypt']) - .then(keyObj => webCrypto.decrypt({ name: ALGO, iv }, keyObj, ct)) - .then(pt => new Uint8Array(pt)); -} - -function nodeEncrypt(pt, key, iv) { - pt = new Buffer(pt); - key = new Buffer(key); - iv = new Buffer(iv); - const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-gcm', key, iv); - const ct = Buffer.concat([en.update(pt), en.final(), en.getAuthTag()]); // append auth tag to ciphertext - return Promise.resolve(new Uint8Array(ct)); -} - -function nodeDecrypt(ct, key, iv) { - ct = new Buffer(ct); - key = new Buffer(key); - iv = new Buffer(iv); - const de = new nodeCrypto.createDecipheriv('aes-' + (key.length * 8) + '-gcm', key, iv); - de.setAuthTag(ct.slice(ct.length - TAG_LEN, ct.length)); // read auth tag at end of ciphertext - const pt = Buffer.concat([de.update(ct.slice(0, ct.length - TAG_LEN)), de.final()]); - return Promise.resolve(new Uint8Array(pt)); -} +export default GCM; diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index eb31238a..8068c89c 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -120,7 +120,8 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith ); this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises))); } else { - this.packets.read(await mode.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv)); + const modeInstance = await mode(sessionKeyAlgorithm, key); + this.packets.read(await modeInstance.decrypt(this.encrypted, this.iv)); } return true; }; @@ -135,6 +136,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { this.aeadAlgo = config.aead_protect_version === 4 ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.gcm; const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + const modeInstance = await mode(sessionKeyAlgorithm, key); this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV let data = this.packets.write(); if (config.aead_protect_version === 4) { @@ -149,7 +151,6 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0); adataView.setInt32(13 + 4, data.length); // Should be setInt64(13, ...) const encryptedPromises = []; - const modeInstance = await mode(sessionKeyAlgorithm, key); for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { encryptedPromises.push( modeInstance.encrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray) @@ -165,7 +166,7 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith ); this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises)); } else { - this.encrypted = await mode.encrypt(sessionKeyAlgorithm, data, key, this.iv); + this.encrypted = await modeInstance.encrypt(data, this.iv); } return true; }; diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 212a38a3..7f9e7f43 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -304,21 +304,22 @@ describe('API functional testing', function() { }); } - function testAESGCM(plaintext) { + function testAESGCM(plaintext, nativeDecrypt) { symmAlgos.forEach(function(algo) { if(algo.substr(0,3) === 'aes') { it(algo, async function() { const key = await crypto.generateSessionKey(algo); const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength); + let modeInstance = await crypto.gcm(algo, key); - return crypto.gcm.encrypt( - algo, util.str_to_Uint8Array(plaintext), key, iv - ).then(function(ciphertext) { - return crypto.gcm.decrypt(algo, ciphertext, key, iv); - }).then(function(decrypted) { - const decryptedStr = util.Uint8Array_to_str(decrypted); - expect(decryptedStr).to.equal(plaintext); - }); + const ciphertext = await modeInstance.encrypt(util.str_to_Uint8Array(plaintext), iv); + + openpgp.config.use_native = nativeDecrypt; + modeInstance = await crypto.gcm(algo, key); + + const decrypted = await modeInstance.decrypt(util.str_to_Uint8Array(util.Uint8Array_to_str(ciphertext)), iv); + const decryptedStr = util.Uint8Array_to_str(decrypted); + expect(decryptedStr).to.equal(plaintext); }); } }); @@ -355,7 +356,7 @@ describe('API functional testing', function() { openpgp.config.use_native = use_nativeVal; }); - testAESGCM("12345678901234567890123456789012345678901234567890"); + testAESGCM("12345678901234567890123456789012345678901234567890", true); }); describe('Symmetric AES-GCM (asm.js fallback)', function() { @@ -368,7 +369,20 @@ describe('API functional testing', function() { openpgp.config.use_native = use_nativeVal; }); - testAESGCM("12345678901234567890123456789012345678901234567890"); + testAESGCM("12345678901234567890123456789012345678901234567890", false); + }); + + describe('Symmetric AES-GCM (native encrypt, asm.js decrypt)', function() { + let use_nativeVal; + beforeEach(function() { + use_nativeVal = openpgp.config.use_native; + openpgp.config.use_native = true; + }); + afterEach(function() { + openpgp.config.use_native = use_nativeVal; + }); + + testAESGCM("12345678901234567890123456789012345678901234567890", false); }); it('Asymmetric using RSA with eme_pkcs1 padding', function () { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index fff0f5de..b0e6f570 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -674,6 +674,36 @@ describe('OpenPGP.js public api tests', function() { } }); + tryTests('GCM mode (draft04, asm.js)', tests, { + if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), + beforeEach: function() { + openpgp.config.use_native = false; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; + openpgp.config.aead_mode = openpgp.enums.aead.gcm; + + // 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]; + } + }); + + tryTests('GCM mode (draft04, native)', tests, { + if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), + beforeEach: function() { + openpgp.config.use_native = true; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; + openpgp.config.aead_mode = openpgp.enums.aead.gcm; + + // 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]; + } + }); + tryTests('EAX mode (asm.js)', tests, { if: true, beforeEach: function() {