diff --git a/src/crypto/eax.js b/src/crypto/eax.js new file mode 100644 index 00000000..ac139f6a --- /dev/null +++ b/src/crypto/eax.js @@ -0,0 +1,156 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2018 ProtonTech AG +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 3.0 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +/** + * @fileoverview This module implements AES-EAX en/decryption on top of + * native AES-CTR using either the WebCrypto API or Node.js' crypto API. + * @requires asmcrypto.js + * @requires util + * @module crypto/eax + */ + +import { AES_CMAC } from 'asmcrypto.js/src/aes/cmac/exports'; +import { AES_CTR } from 'asmcrypto.js/src/aes/ctr/exports'; +import util from '../util'; + +const webCrypto = util.getWebCryptoAll(); +const nodeCrypto = util.getNodeCrypto(); +const Buffer = util.getNodeBuffer(); + + +const blockLength = 16; +const ivLength = blockLength; +const tagLength = blockLength; + +const zero = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +const one = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); +const two = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + + +/** + * Encrypt plaintext input. + * @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} nonce The nonce (16 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output + */ +async function encrypt(cipher, plaintext, key, nonce, adata) { + if (cipher.substr(0, 3) !== 'aes') { + throw new Error('EAX mode supports only AES cipher'); + } + + const _nonce = OMAC(zero, nonce, key); + const _adata = OMAC(one, adata, key); + const ciphered = await CTR(plaintext, key, _nonce); + const _ciphered = OMAC(two, ciphered, key); + const tag = xor3(_nonce, _ciphered, _adata); // Assumes that OMAC(*).length === tagLength. + return concat(ciphered, tag); +} + +/** + * 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 + * @param {Uint8Array} nonce The nonce (16 bytes) + * @param {Uint8Array} adata Associated data to verify + * @returns {Promise} The plaintext output + */ +async function decrypt(cipher, ciphertext, key, nonce, adata) { + if (cipher.substr(0, 3) !== 'aes') { + throw new Error('EAX mode supports only AES cipher'); + } + + if (ciphertext.length < tagLength) throw new Error('Invalid EAX ciphertext'); + const ciphered = ciphertext.subarray(0, ciphertext.length - tagLength); + const tag = ciphertext.subarray(ciphertext.length - tagLength); + const _nonce = OMAC(zero, nonce, key); + const _adata = OMAC(one, adata, key); + const _ciphered = OMAC(two, ciphered, key); + const _tag = xor3(_nonce, _ciphered, _adata); // Assumes that OMAC(*).length === tagLength. + if (!util.equalsUint8Array(tag, _tag)) throw new Error('Authentication tag mismatch in EAX ciphertext'); + const plaintext = await CTR(ciphered, key, _nonce); + return plaintext; +} + +/** + * Get EAX nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.1|RFC4880bis-04, section 5.16.1}. + * @param {Uint8Array} iv The initialization vector (16 bytes) + * @param {Uint8Array} chunkIndex The chunk index (8 bytes) + */ +function getNonce(iv, chunkIndex) { + const nonce = iv.slice(); + for (let i = 0; i < chunkIndex.length; i++) { + nonce[8 + i] ^= chunkIndex[i]; + } + return nonce; +} + + +export default { + blockLength, + ivLength, + encrypt, + decrypt, + getNonce +}; + + +////////////////////////// +// // +// Helper functions // +// // +////////////////////////// + + +function xor3(a, b, c) { + return a.map((n, i) => n ^ b[i] ^ c[i]); +} + +function concat(...arrays) { + return util.concatUint8Array(arrays); +} + +function OMAC(t, message, key) { + return AES_CMAC.bytes(concat(t, message), key); +} + +function CTR(plaintext, key, iv) { + if (webCrypto && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + return webCtr(plaintext, key, iv); + } else if (nodeCrypto) { // Node crypto library + return nodeCtr(plaintext, key, iv); + } // asm.js fallback + return Promise.resolve(AES_CTR.encrypt(plaintext, key, iv)); +} + +function webCtr(pt, key, iv) { + return webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']) + .then(keyObj => webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, keyObj, pt)) + .then(ct => new Uint8Array(ct)); +} + +function nodeCtr(pt, key, iv) { + pt = new Buffer(pt); + key = new Buffer(key); + iv = new Buffer(iv); + const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-ctr', key, iv); + const ct = Buffer.concat([en.update(pt), en.final()]); + return Promise.resolve(new Uint8Array(ct)); +} diff --git a/src/crypto/index.js b/src/crypto/index.js index cd11f4cf..4c0178a4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -13,6 +13,7 @@ import cipher from './cipher'; import hash from './hash'; import cfb from './cfb'; import gcm from './gcm'; +import eax from './eax'; import publicKey from './public_key'; import signature from './signature'; import random from './random'; @@ -31,6 +32,8 @@ const mod = { cfb: cfb, /** @see module:crypto/gcm */ gcm: gcm, + /** @see module:crypto/eax */ + eax: eax, /** @see module:crypto/public_key */ publicKey: publicKey, /** @see module:crypto/signature */ diff --git a/test/crypto/eax.js b/test/crypto/eax.js new file mode 100644 index 00000000..369dcfd4 --- /dev/null +++ b/test/crypto/eax.js @@ -0,0 +1,150 @@ +// Modified by ProtonTech AG + +// Adapted from https://github.com/artjomb/cryptojs-extension/blob/8c61d159/test/eax.js + +const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp'); + +const chai = require('chai'); +chai.use(require('chai-as-promised')); + +const expect = chai.expect; + +const eax = openpgp.crypto.eax; + +function testAESEAX() { + it('Passes all test vectors', async function() { + var vectors = [ + // From http://www.cs.ucdavis.edu/~rogaway/papers/eax.pdf ... + { + msg: "", + key: "233952DEE4D5ED5F9B9C6D6FF80FF478", + nonce: "62EC67F9C3A4A407FCB2A8C49031A8B3", + header: "6BFB914FD07EAE6B", + ct: "E037830E8389F27B025A2D6527E79D01" + }, + { + msg: "F7FB", + key: "91945D3F4DCBEE0BF45EF52255F095A4", + nonce: "BECAF043B0A23D843194BA972C66DEBD", + header: "FA3BFD4806EB53FA", + ct: "19DD5C4C9331049D0BDAB0277408F67967E5" + }, + { + msg: "1A47CB4933", + key: "01F74AD64077F2E704C0F60ADA3DD523", + nonce: "70C3DB4F0D26368400A10ED05D2BFF5E", + header: "234A3463C1264AC6", + ct: "D851D5BAE03A59F238A23E39199DC9266626C40F80" + }, + { + msg: "481C9E39B1", + key: "D07CF6CBB7F313BDDE66B727AFD3C5E8", + nonce: "8408DFFF3C1A2B1292DC199E46B7D617", + header: "33CCE2EABFF5A79D", + ct: "632A9D131AD4C168A4225D8E1FF755939974A7BEDE" + }, + { + msg: "40D0C07DA5E4", + key: "35B6D0580005BBC12B0587124557D2C2", + nonce: "FDB6B06676EEDC5C61D74276E1F8E816", + header: "AEB96EAEBE2970E9", + ct: "071DFE16C675CB0677E536F73AFE6A14B74EE49844DD" + }, + { + msg: "4DE3B35C3FC039245BD1FB7D", + key: "BD8E6E11475E60B268784C38C62FEB22", + nonce: "6EAC5C93072D8E8513F750935E46DA1B", + header: "D4482D1CA78DCE0F", + ct: "835BB4F15D743E350E728414ABB8644FD6CCB86947C5E10590210A4F" + }, + { + msg: "8B0A79306C9CE7ED99DAE4F87F8DD61636", + key: "7C77D6E813BED5AC98BAA417477A2E7D", + nonce: "1A8C98DCD73D38393B2BF1569DEEFC19", + header: "65D2017990D62528", + ct: "02083E3979DA014812F59F11D52630DA30137327D10649B0AA6E1C181DB617D7F2" + }, + { + msg: "1BDA122BCE8A8DBAF1877D962B8592DD2D56", + key: "5FFF20CAFAB119CA2FC73549E20F5B0D", + nonce: "DDE59B97D722156D4D9AFF2BC7559826", + header: "54B9F04E6A09189A", + ct: "2EC47B2C4954A489AFC7BA4897EDCDAE8CC33B60450599BD02C96382902AEF7F832A" + }, + { + msg: "6CF36720872B8513F6EAB1A8A44438D5EF11", + key: "A4A4782BCFFD3EC5E7EF6D8C34A56123", + nonce: "B781FCF2F75FA5A8DE97A9CA48E522EC", + header: "899A175897561D7E", + ct: "0DE18FD0FDD91E7AF19F1D8EE8733938B1E8E7F6D2231618102FDB7FE55FF1991700" + }, + { + msg: "CA40D7446E545FFAED3BD12A740A659FFBBB3CEAB7", + key: "8395FCF1E95BEBD697BD010BC766AAC3", + nonce: "22E7ADD93CFC6393C57EC0B3C17D6B44", + header: "126735FCC320D25A", + ct: "CB8920F87A6C75CFF39627B56E3ED197C552D295A7CFC46AFC253B4652B1AF3795B124AB6E" + }, + ]; + + const cipher = 'aes128'; + + for(const [i, vec] of vectors.entries()) { + const keyBytes = openpgp.util.hex_to_Uint8Array(vec.key), + msgBytes = openpgp.util.hex_to_Uint8Array(vec.msg), + nonceBytes = openpgp.util.hex_to_Uint8Array(vec.nonce), + headerBytes = openpgp.util.hex_to_Uint8Array(vec.header), + ctBytes = openpgp.util.hex_to_Uint8Array(vec.ct); + + // encryption test + let ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.ct.toLowerCase()); + + // decryption test with verification + let pt = await eax.decrypt(cipher, ctBytes, keyBytes, nonceBytes, headerBytes); + expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); + + // tampering detection test + ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + ct[2] ^= 8; + pt = eax.decrypt(cipher, ct, keyBytes, nonceBytes, headerBytes); + await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in EAX ciphertext') + + // testing without additional data + ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, new Uint8Array()); + pt = await eax.decrypt(cipher, ct, keyBytes, nonceBytes, new Uint8Array()); + expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); + + // testing with multiple additional data + ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + pt = await eax.decrypt(cipher, ct, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); + } + }); +} + +describe('Symmetric AES-EAX (native)', 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; + }); + + testAESEAX(); +}); + +describe('Symmetric AES-EAX (asm.js fallback)', function() { + let use_nativeVal; + beforeEach(function() { + use_nativeVal = openpgp.config.use_native; + openpgp.config.use_native = false; + }); + afterEach(function() { + openpgp.config.use_native = use_nativeVal; + }); + + testAESEAX(); +}); \ No newline at end of file diff --git a/test/crypto/index.js b/test/crypto/index.js index 7ab5c758..9fc50442 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -6,4 +6,5 @@ describe('Crypto', function () { require('./elliptic.js'); require('./pkcs5.js'); require('./aes_kw.js'); + require('./eax.js'); });