From 7b3f51c0d45399ed1b1d67b3362ba7287a51f39d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Sat, 31 Mar 2018 21:45:23 +0200 Subject: [PATCH] Implement AEAD Encrypted Data Packet --- src/config/config.js | 8 ++ src/enums.js | 10 +++ src/packet/sym_encrypted_aead_protected.js | 85 ++++++++++++++++++++-- test/general/openpgp.js | 16 ++++ test/general/packet.js | 76 +++++++++++++++++++ 5 files changed, 189 insertions(+), 6 deletions(-) diff --git a/src/config/config.js b/src/config/config.js index 301e41b8..f1302655 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -51,6 +51,14 @@ export default { * @property {Boolean} aead_protect */ aead_protect: false, + /** + * Chunk Size Byte for Authenticated Encryption with Additional Data (AEAD) mode + * Only has an effect when aead_protect is set to true. + * Must be an integer value from 0 to 56. + * @memberof module:config + * @property {Integer} aead_chunk_size_byte + */ + aead_chunk_size_byte: 46, /** Use integrity protection for symmetric encryption * @memberof module:config * @property {Boolean} integrity_protect diff --git a/src/enums.js b/src/enums.js index ed4f7623..ab9451a6 100644 --- a/src/enums.js +++ b/src/enums.js @@ -167,6 +167,16 @@ export default { 'SHA-512': 10 }, + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-9.6|RFC4880bis-04, section 9.6} + * @enum {Integer} + * @readonly + */ + aead: { + eax: 1, + ocb: 2, + gcm: 100 // Private algorithm + }, + /** A list of packet types and numeric tags associated with them. * @enum {Integer} * @readonly diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 3d0bcc2d..1749e945 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -16,17 +16,18 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA /** + * @requires config * @requires crypto * @requires enums * @requires util */ +import config from '../config'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; const VERSION = 1; // A one-octet version number of the data packet. -const IV_LEN = crypto.gcm.ivLength; // currently only AES-GCM is supported /** * Implementation of the Symmetrically Encrypted Authenticated Encryption with @@ -40,6 +41,9 @@ const IV_LEN = crypto.gcm.ivLength; // currently only AES-GCM is supported function SymEncryptedAEADProtected() { this.tag = enums.packet.symEncryptedAEADProtected; this.version = VERSION; + this.cipherAlgo = null; + this.aeadAlgo = null; + this.chunkSizeByte = null; this.iv = null; this.encrypted = null; this.packets = null; @@ -56,8 +60,16 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) { throw new Error('Invalid packet version.'); } offset++; - this.iv = bytes.subarray(offset, IV_LEN + offset); - offset += IV_LEN; + if (config.aead_protect === 'draft04') { + this.cipherAlgo = bytes[offset++]; + this.aeadAlgo = bytes[offset++]; + this.chunkSizeByte = bytes[offset++]; + } else { + this.aeadAlgo = enums.aead.gcm; + } + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + this.iv = bytes.subarray(offset, mode.ivLength + offset); + offset += mode.ivLength; this.encrypted = bytes.subarray(offset, bytes.length); }; @@ -66,6 +78,9 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) { * @returns {Uint8Array} The encrypted payload */ SymEncryptedAEADProtected.prototype.write = function () { + if (config.aead_protect === 'draft04') { + return util.concatUint8Array([new Uint8Array([this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte]), this.iv, this.encrypted]); + } return util.concatUint8Array([new Uint8Array([this.version]), this.iv, this.encrypted]); }; @@ -77,7 +92,34 @@ SymEncryptedAEADProtected.prototype.write = function () { * @async */ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) { - this.packets.read(await crypto.gcm.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv)); + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + if (config.aead_protect === 'draft04') { + const cipher = enums.read(enums.symmetric, this.cipherAlgo); + let data = this.encrypted.subarray(0, this.encrypted.length - mode.blockLength); + const authTag = this.encrypted.subarray(this.encrypted.length - mode.blockLength); + const chunkSize = 2 ** (this.chunkSizeByte + 6); // ((uint64_t)1 << (c + 6)) + const adataBuffer = new ArrayBuffer(21); + const adataArray = new Uint8Array(adataBuffer, 0, 13); + const adataTagArray = new Uint8Array(adataBuffer); + const adataView = new DataView(adataBuffer); + const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8); + adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0); + adataView.setInt32(13 + 4, data.length - mode.blockLength); // Should be setInt64(13, ...) + const decryptedPromises = []; + for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { + decryptedPromises.push( + mode.decrypt(cipher, data.subarray(0, chunkSize), key, mode.getNonce(this.iv, chunkIndexArray), adataArray) + ); + data = data.subarray(chunkSize); + adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) + } + decryptedPromises.push( + mode.decrypt(cipher, authTag, key, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) + ); + this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises))); + } else { + this.packets.read(await mode.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv)); + } return true; }; @@ -89,7 +131,38 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith * @async */ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { - this.iv = await crypto.random.getRandomBytes(IV_LEN); // generate new random IV - this.encrypted = await crypto.gcm.encrypt(sessionKeyAlgorithm, this.packets.write(), key, this.iv); + this.aeadAlgo = config.aead_protect === 'draft04' ? enums.aead.eax : enums.aead.gcm; + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV + let data = this.packets.write(); + if (config.aead_protect === 'draft04') { + this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); + this.chunkSizeByte = config.aead_chunk_size_byte; + const chunkSize = 2 ** (this.chunkSizeByte + 6); // ((uint64_t)1 << (c + 6)) + const adataBuffer = new ArrayBuffer(21); + const adataArray = new Uint8Array(adataBuffer, 0, 13); + const adataTagArray = new Uint8Array(adataBuffer); + const adataView = new DataView(adataBuffer); + const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8); + 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 = []; + for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { + encryptedPromises.push( + mode.encrypt(sessionKeyAlgorithm, data.subarray(0, chunkSize), key, mode.getNonce(this.iv, chunkIndexArray), adataArray) + ); + // We take a chunk of data, encrypt it, and shift `data` to the + // next chunk. After the final chunk, we encrypt a final, empty + // data chunk to get the final authentication tag. + data = data.subarray(chunkSize); + adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) + } + encryptedPromises.push( + mode.encrypt(sessionKeyAlgorithm, data, key, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) + ); + this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises)); + } else { + this.encrypted = await mode.encrypt(sessionKeyAlgorithm, data, key, this.iv); + } return true; }; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 59901a86..e4f94bd2 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -667,6 +667,22 @@ describe('OpenPGP.js public api tests', function() { } }); + tryTests('EAX mode (asm.js)', tests, { + if: true, + beforeEach: function() { + openpgp.config.use_native = false; + openpgp.config.aead_protect = 'draft04'; + } + }); + + tryTests('EAX mode (native)', tests, { + if: openpgp.util.getWebCryptoAll() || openpgp.util.getNodeCrypto(), + beforeEach: function() { + openpgp.config.use_native = true; + openpgp.config.aead_protect = 'draft04'; + } + }); + function tests() { it('Configuration', function() { openpgp.config.show_version = false; diff --git a/test/general/packet.js b/test/general/packet.js index ec9915e0..4913370d 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -1,5 +1,6 @@ const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp'); +const stub = require('sinon/lib/sinon/stub'); const chai = require('chai'); chai.use(require('chai-as-promised')); @@ -141,6 +142,81 @@ describe("Packet", function() { }); }); + it('Sym. encrypted AEAD protected packet (draft04)', function() { + let aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + + const key = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); + const algo = 'aes256'; + + const literal = new openpgp.packet.Literal(); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + + msg.push(enc); + literal.setText('Hello world!'); + enc.packets.push(literal); + + const msg2 = new openpgp.packet.List(); + + return enc.encrypt(algo, key).then(function() { + msg2.read(msg.write()); + return msg2[0].decrypt(algo, key); + }).then(function() { + expect(msg2[0].packets[0].data).to.deep.equal(literal.data); + }).finally(function() { + openpgp.config.aead_protect = aead_protectVal; + }); + }); + + it('Sym. encrypted AEAD protected packet test vector (draft04)', function() { + // From https://gitlab.com/openpgp-wg/rfc4880bis/commit/00b20923e6233fb6ff1666ecd5acfefceb32907d + + let packetBytes = openpgp.util.hex_to_Uint8Array(` + d4 4a 01 07 01 0e b7 32 37 9f 73 c4 92 8d e2 5f + ac fe 65 17 ec 10 5d c1 1a 81 dc 0c b8 a2 f6 f3 + d9 00 16 38 4a 56 fc 82 1a e1 1a e8 db cb 49 86 + 26 55 de a8 8d 06 a8 14 86 80 1b 0f f3 87 bd 2e + ab 01 3d e1 25 95 86 90 6e ab 24 76 + `.replace(/\s+/g, '')); + + let aead_protectVal = openpgp.config.aead_protect; + let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; + openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_chunk_size_byte = 14; + + const iv = openpgp.util.hex_to_Uint8Array('b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10'.replace(/\s+/g, '')); + const key = openpgp.util.hex_to_Uint8Array('86 f1 ef b8 69 52 32 9f 24 ac d3 bf d0 e5 34 6d'.replace(/\s+/g, '')); + const algo = 'aes128'; + + const literal = new openpgp.packet.Literal(0); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + + msg.push(enc); + literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary); + literal.filename = ''; + enc.packets.push(literal); + + const msg2 = new openpgp.packet.List(); + + let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); + randomBytesStub.returns(resolves(iv)); + + return enc.encrypt(algo, key).then(function() { + const data = msg.write(); + expect(data).to.deep.equal(packetBytes); + msg2.read(data); + return msg2[0].decrypt(algo, key); + }).then(function() { + expect(msg2[0].packets[0].data).to.deep.equal(literal.data); + }).finally(function() { + openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; + randomBytesStub.restore(); + }); + }); + it('Sym encrypted session key with a compressed packet', async function() { const msg = '-----BEGIN PGP MESSAGE-----\n' +