From 85a1b9859b1ad6243a748ae599c33b279963af65 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 28 Mar 2018 14:40:22 +0200 Subject: [PATCH 01/51] Implement EAX mode --- src/crypto/eax.js | 156 +++++++++++++++++++++++++++++++++++++++++++ src/crypto/index.js | 3 + test/crypto/eax.js | 150 +++++++++++++++++++++++++++++++++++++++++ test/crypto/index.js | 1 + 4 files changed, 310 insertions(+) create mode 100644 src/crypto/eax.js create mode 100644 test/crypto/eax.js 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'); }); From 7b3f51c0d45399ed1b1d67b3362ba7287a51f39d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Sat, 31 Mar 2018 21:45:23 +0200 Subject: [PATCH 02/51] 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' + From 17ad654d60218494a8fdb05d6182b6ca45c2729c Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 4 Apr 2018 15:37:48 +0200 Subject: [PATCH 03/51] Implement version 5 Symmetric-Key Encrypted Session Key packet --- src/config/config.js | 7 ++ src/packet/sym_encrypted_session_key.js | 74 ++++++++++++---- src/type/s2k.js | 7 +- test/general/packet.js | 113 ++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 21 deletions(-) diff --git a/src/config/config.js b/src/config/config.js index f1302655..1373430d 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -59,6 +59,13 @@ export default { * @property {Integer} aead_chunk_size_byte */ aead_chunk_size_byte: 46, + /** + * {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3|RFC4880 3.7.1.3}: + * Iteration Count Byte for S2K (String to Key) + * @memberof module:config + * @property {Integer} s2k_iteration_count_byte + */ + s2k_iteration_count_byte: 96, /** Use integrity protection for symmetric encryption * @memberof module:config * @property {Boolean} integrity_protect diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index cdd0a136..eea69222 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -17,12 +17,14 @@ /** * @requires type/s2k + * @requires config * @requires crypto * @requires enums * @requires util */ import type_s2k from '../type/s2k'; +import config from '../config'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; @@ -47,12 +49,14 @@ import util from '../util'; */ function SymEncryptedSessionKey() { this.tag = enums.packet.symEncryptedSessionKey; - this.version = 4; + this.version = config.aead_protect === 'draft04' ? 5 : 4; this.sessionKey = null; this.sessionKeyEncryptionAlgorithm = null; this.sessionKeyAlgorithm = 'aes256'; + this.aeadAlgorithm = 'eax'; this.encrypted = null; this.s2k = null; + this.iv = null; } /** @@ -66,22 +70,35 @@ function SymEncryptedSessionKey() { * @returns {module:packet.SymEncryptedSessionKey} Object representation */ SymEncryptedSessionKey.prototype.read = function(bytes) { + let offset = 0; + // A one-octet version number. The only currently defined version is 4. - [this.version] = bytes; + this.version = bytes[offset++]; // A one-octet number describing the symmetric algorithm used. - const algo = enums.read(enums.symmetric, bytes[1]); + const algo = enums.read(enums.symmetric, bytes[offset++]); + + if (this.version === 5) { + // A one-octet AEAD algorithm. + this.aeadAlgorithm = enums.read(enums.aead, bytes[offset++]); + } // A string-to-key (S2K) specifier, length as defined above. this.s2k = new type_s2k(); - const s2klength = this.s2k.read(bytes.subarray(2, bytes.length)); + offset += this.s2k.read(bytes.subarray(offset, bytes.length)); - // Optionally, the encrypted session key itself, which is decrypted - // with the string-to-key object. - const done = s2klength + 2; + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; - if (done < bytes.length) { - this.encrypted = bytes.subarray(done, bytes.length); + // A starting initialization vector of size specified by the AEAD + // algorithm. + this.iv = bytes.subarray(offset, offset += mode.ivLength); + } + + // The encrypted session key itself, which is decrypted with the + // string-to-key object. This is optional in version 4. + if (this.version === 5 || offset < bytes.length) { + this.encrypted = bytes.subarray(offset, bytes.length); this.sessionKeyEncryptionAlgorithm = algo; } else { this.sessionKeyAlgorithm = algo; @@ -93,11 +110,18 @@ SymEncryptedSessionKey.prototype.write = function() { this.sessionKeyAlgorithm : this.sessionKeyEncryptionAlgorithm; - let bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]); + let bytes; - if (this.encrypted !== null) { - bytes = util.concatUint8Array([bytes, this.encrypted]); + if (this.version === 5) { + bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo), enums.write(enums.aead, this.aeadAlgorithm)]), this.s2k.write(), this.iv, this.encrypted]); + } else { + bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]); + + if (this.encrypted !== null) { + bytes = util.concatUint8Array([bytes, this.encrypted]); + } } + return bytes; }; @@ -115,14 +139,19 @@ SymEncryptedSessionKey.prototype.decrypt = async function(passphrase) { const length = crypto.cipher[algo].keySize; const key = this.s2k.produce_key(passphrase, length); - if (this.encrypted === null) { - this.sessionKey = key; - } else { + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; + const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); + this.sessionKey = await mode.decrypt(algo, this.encrypted, key, this.iv, adata); + } else if (this.encrypted !== null) { const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null); this.sessionKeyAlgorithm = enums.read(enums.symmetric, decrypted[0]); this.sessionKey = decrypted.subarray(1, decrypted.length); + } else { + this.sessionKey = key; } + return true; }; @@ -145,14 +174,21 @@ SymEncryptedSessionKey.prototype.encrypt = async function(passphrase) { const length = crypto.cipher[algo].keySize; const key = this.s2k.produce_key(passphrase, length); - const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); - if (this.sessionKey === null) { this.sessionKey = await crypto.generateSessionKey(this.sessionKeyAlgorithm); } - const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); - this.encrypted = crypto.cfb.normalEncrypt(algo, key, private_key, null); + if (this.version === 5) { + const mode = crypto[this.aeadAlgorithm]; + this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV + const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); + this.encrypted = await mode.encrypt(algo, this.sessionKey, key, this.iv, adata); + } else { + const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); + const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); + this.encrypted = crypto.cfb.normalEncrypt(algo, key, private_key, null); + } + return true; }; diff --git a/src/type/s2k.js b/src/type/s2k.js index 340243e0..223f2f41 100644 --- a/src/type/s2k.js +++ b/src/type/s2k.js @@ -24,15 +24,17 @@ * places, currently: to encrypt the secret part of private keys in the * private keyring, and to convert passphrases to encryption keys for * symmetrically encrypted messages. + * @requires config * @requires crypto * @requires enums * @requires util * @module type/s2k */ +import config from '../config'; +import crypto from '../crypto'; import enums from '../enums.js'; import util from '../util.js'; -import crypto from '../crypto'; /** * @constructor @@ -42,7 +44,8 @@ function S2K() { this.algorithm = 'sha256'; /** @type {module:enums.s2k} */ this.type = 'iterated'; - this.c = 96; + /** @type {Integer} */ + this.c = config.s2k_iteration_count_byte; /** Eight bytes of salt in a binary string. * @type {String} */ diff --git a/test/general/packet.js b/test/general/packet.js index 4913370d..2aba6164 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -415,6 +415,119 @@ describe("Packet", function() { expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); }); + it('Sym. encrypted session key reading/writing (draft04)', async function() { + let aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + + try { + const passphrase = 'hello'; + const algo = 'aes256'; + + const literal = new openpgp.packet.Literal(); + const key_enc = new openpgp.packet.SymEncryptedSessionKey(); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + + msg.push(key_enc); + msg.push(enc); + + key_enc.sessionKeyAlgorithm = algo; + await key_enc.encrypt(passphrase); + + const key = key_enc.sessionKey; + + literal.setText('Hello world!'); + enc.packets.push(literal); + await enc.encrypt(algo, key); + + const msg2 = new openpgp.packet.List(); + msg2.read(msg.write()); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aead_protect = aead_protectVal; + } + }); + + it('Sym. encrypted session key reading/writing test vector (draft04)', async function() { + // From https://gitlab.com/openpgp-wg/rfc4880bis/commit/00b20923e6233fb6ff1666ecd5acfefceb32907d + + let aead_protectVal = openpgp.config.aead_protect; + let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; + let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte; + openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_chunk_size_byte = 14; + openpgp.config.s2k_iteration_count_byte = 0x90; + + let salt = openpgp.util.hex_to_Uint8Array(`cd5a9f70fbe0bc65`); + let sessionKey = openpgp.util.hex_to_Uint8Array(`86 f1 ef b8 69 52 32 9f 24 ac d3 bf d0 e5 34 6d`.replace(/\s+/g, '')); + let sessionIV = openpgp.util.hex_to_Uint8Array(`bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35`.replace(/\s+/g, '')); + let dataIV = openpgp.util.hex_to_Uint8Array(`b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10`.replace(/\s+/g, '')); + + let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); + randomBytesStub.onCall(0).returns(resolves(salt)); + randomBytesStub.onCall(1).returns(resolves(sessionKey)); + randomBytesStub.onCall(2).returns(resolves(sessionIV)); + randomBytesStub.onCall(3).returns(resolves(dataIV)); + + let packetBytes = openpgp.util.hex_to_Uint8Array(` + c3 3e 05 07 01 03 08 cd 5a 9f 70 fb e0 bc 65 90 + bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35 + 9d ee 19 d0 7c 34 46 c4 31 2a 34 ae 19 67 a2 fb + 7e 92 8e a5 b4 fa 80 12 bd 45 6d 17 38 c6 3c 36 + + 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, '')); + + try { + const passphrase = 'password'; + const algo = 'aes128'; + + const literal = new openpgp.packet.Literal(0); + const key_enc = new openpgp.packet.SymEncryptedSessionKey(); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + + msg.push(key_enc); + msg.push(enc); + + key_enc.sessionKeyAlgorithm = algo; + await key_enc.encrypt(passphrase); + + const key = key_enc.sessionKey; + + literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary); + literal.filename = ''; + enc.packets.push(literal); + await enc.encrypt(algo, key); + + const data = msg.write(); + expect(data).to.deep.equal(packetBytes); + + const msg2 = new openpgp.packet.List(); + msg2.read(data); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; + openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal; + randomBytesStub.restore(); + } + }); + it('Secret key encryption/decryption test', async function() { const armored_msg = '-----BEGIN PGP MESSAGE-----\n' + From 7c3bbe9278469c9aac231ebf89916253bdb994e9 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 4 Apr 2018 18:48:04 +0200 Subject: [PATCH 04/51] Don't auto-scroll unit tests if you scrolled up --- test/unittests.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/unittests.js b/test/unittests.js index 9c1c00a6..e5f89866 100644 --- a/test/unittests.js +++ b/test/unittests.js @@ -32,7 +32,13 @@ if (typeof Promise === 'undefined') { describe('Unit Tests', function () { - if (typeof window !== 'undefined') { afterEach(function () { window.scrollTo(0, document.body.scrollHeight); }); } + if (typeof window !== 'undefined') { + afterEach(function () { + if (window.scrollY >= document.body.scrollHeight - window.innerHeight - 100) { + window.scrollTo(0, document.body.scrollHeight); + } + }); + } require('./crypto'); require('./general'); From 5d43b44e50cca738d7de97c2a41fa1ce9ebdd8af Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 11 Apr 2018 13:42:06 +0200 Subject: [PATCH 05/51] Log swallowed errors in debug mode --- src/message.js | 8 ++++++-- src/openpgp.js | 2 +- src/packet/packetlist.js | 1 + src/util.js | 12 ++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/message.js b/src/message.js index 76f58c4b..f0e1211f 100644 --- a/src/message.js +++ b/src/message.js @@ -157,7 +157,9 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) { try { await keyPacket.decrypt(password); keyPackets.push(keyPacket); - } catch (err) {} + } catch (err) { + util.print_debug_error(err); + } })); })); } else if (privateKeys) { @@ -180,7 +182,9 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) { try { await keyPacket.decrypt(privateKeyPacket); keyPackets.push(keyPacket); - } catch (err) {} + } catch (err) { + util.print_debug_error(err); + } })); })); } else { diff --git a/src/openpgp.js b/src/openpgp.js index 612d05c2..4b32fdf0 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -568,7 +568,7 @@ function parseMessage(message, format) { */ function onError(message, error) { // log the stack trace - if (config.debug) { console.error(error.stack); } + util.print_debug_error(error); // update error message error.message = message + ': ' + error.message; diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index 3392825a..0631cd0d 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -54,6 +54,7 @@ List.prototype.read = function (bytes) { parsed.tag === enums.packet.compressed) { throw e; } + util.print_debug_error(e); if (pushed) { this.pop(); // drop unsupported packet } diff --git a/src/util.js b/src/util.js index a9afee83..cd3bf2d1 100644 --- a/src/util.js +++ b/src/util.js @@ -376,6 +376,18 @@ export default { } }, + /** + * Helper function to print a debug error. Debug + * messages are only printed if + * @link module:config/config.debug is set to true. + * @param {String} str String of the debug message + */ + print_debug_error: function (error) { + if (config.debug) { + console.error(error); + } + }, + // TODO rewrite getLeftNBits to work with Uint8Arrays getLeftNBits: function (string, bitcount) { const rest = bitcount % 8; From c2f898279bb4b275a9fb5f7428652ede9cc93094 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 4 Apr 2018 19:40:46 +0200 Subject: [PATCH 06/51] Implement version 5 Secret-Key Packet Format --- src/key.js | 4 +- src/packet/public_key.js | 56 +++++++++++++-------- src/packet/secret_key.js | 103 ++++++++++++++++++++++++++++----------- test/general/key.js | 34 ++++++++++++- test/general/packet.js | 32 ++++++++++++ 5 files changed, 179 insertions(+), 50 deletions(-) diff --git a/src/key.js b/src/key.js index 6587bd72..5f21d9f0 100644 --- a/src/key.js +++ b/src/key.js @@ -468,7 +468,7 @@ Key.prototype.getExpirationTime = async function() { if (this.primaryKey.version === 3) { return getExpirationTime(this.primaryKey); } - if (this.primaryKey.version === 4) { + if (this.primaryKey.version >= 4) { const primaryUser = await this.getPrimaryUser(null); const selfCert = primaryUser.selfCertification; const keyExpiry = getExpirationTime(this.primaryKey, selfCert); @@ -1383,7 +1383,7 @@ function getExpirationTime(keyPacket, signature) { expirationTime = keyPacket.created.getTime() + keyPacket.expirationTimeV3*24*3600*1000; } // check V4 expiration time - if (keyPacket.version === 4 && signature.keyNeverExpires === false) { + if (keyPacket.version >= 4 && signature.keyNeverExpires === false) { expirationTime = keyPacket.created.getTime() + signature.keyExpirationTime*1000; } return expirationTime ? new Date(expirationTime) : Infinity; diff --git a/src/packet/public_key.js b/src/packet/public_key.js index de2bb5fc..f76e16fa 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -18,6 +18,7 @@ /** * @requires type/keyid * @requires type/mpi + * @requires config * @requires crypto * @requires enums * @requires util @@ -25,6 +26,7 @@ import type_keyid from '../type/keyid'; import type_mpi from '../type/mpi'; +import config from '../config'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; @@ -52,7 +54,7 @@ function PublicKey(date=new Date()) { * Packet version * @type {Integer} */ - this.version = 4; + this.version = config.aead_protect === 'draft04' ? 5 : 4; /** * Key creation date. * @type {Date} @@ -88,10 +90,10 @@ function PublicKey(date=new Date()) { */ PublicKey.prototype.read = function (bytes) { let pos = 0; - // A one-octet version number (3 or 4). + // A one-octet version number (3, 4 or 5). this.version = bytes[pos++]; - if (this.version === 3 || this.version === 4) { + if (this.version === 3 || this.version === 4 || this.version === 5) { // - A four-octet number denoting the time that the key was created. this.created = util.readDate(bytes.subarray(pos, pos + 4)); pos += 4; @@ -106,20 +108,25 @@ PublicKey.prototype.read = function (bytes) { // - A one-octet number denoting the public-key algorithm of this key. this.algorithm = enums.read(enums.publicKey, bytes[pos++]); const algo = enums.write(enums.publicKey, this.algorithm); + + if (this.version === 5) { + // - A four-octet scalar octet count for the following key material. + pos += 4; + } + + // - A series of values comprising the key material. This is + // algorithm-specific and described in section XXXX. const types = crypto.getPubKeyParamTypes(algo); this.params = crypto.constructParams(types); - const b = bytes.subarray(pos, bytes.length); - let p = 0; - - for (let i = 0; i < types.length && p < b.length; i++) { - p += this.params[i].read(b.subarray(p, b.length)); - if (p > b.length) { - throw new Error('Error reading MPI @:' + p); + for (let i = 0; i < types.length && pos < bytes.length; i++) { + pos += this.params[i].read(bytes.subarray(pos, bytes.length)); + if (pos > bytes.length) { + throw new Error('Error reading MPI @:' + pos); } } - return p + 6; + return pos; } throw new Error('Version ' + this.version + ' of the key packet is unsupported.'); }; @@ -143,14 +150,18 @@ PublicKey.prototype.write = function () { if (this.version === 3) { arr.push(util.writeNumber(this.expirationTimeV3, 2)); } - // Algorithm-specific params + // A one-octet number denoting the public-key algorithm of this key const algo = enums.write(enums.publicKey, this.algorithm); - const paramCount = crypto.getPubKeyParamTypes(algo).length; arr.push(new Uint8Array([algo])); - for (let i = 0; i < paramCount; i++) { - arr.push(this.params[i].write()); - } + const paramCount = crypto.getPubKeyParamTypes(algo).length; + const params = util.concatUint8Array(this.params.slice(0, paramCount).map(param => param.write())); + if (this.version === 5) { + // A four-octet scalar octet count for the following key material + arr.push(util.writeNumber(params.length, 4)); + } + // Algorithm-specific params + arr.push(params); return util.concatUint8Array(arr); }; @@ -178,7 +189,9 @@ PublicKey.prototype.getKeyId = function () { return this.keyid; } this.keyid = new type_keyid(); - if (this.version === 4) { + if (this.version === 5) { + this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(0, 8)); + } else if (this.version === 4) { this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(12, 20)); } else if (this.version === 3) { const arr = this.params[0].write(); @@ -195,13 +208,18 @@ PublicKey.prototype.getFingerprint = function () { if (this.fingerprint) { return this.fingerprint; } - let toHash = ''; - if (this.version === 4) { + let toHash; + if (this.version === 5) { + const bytes = this.writePublicKey(); + toHash = util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]); + this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha256(toHash)); + } else if (this.version === 4) { toHash = this.writeOld(); this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha1(toHash)); } else if (this.version === 3) { const algo = enums.write(enums.publicKey, this.algorithm); const paramCount = crypto.getPubKeyParamTypes(algo).length; + toHash = ''; for (let i = 0; i < paramCount; i++) { toHash += this.params[i].toString(); } diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 80135a98..f764ff80 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -78,15 +78,17 @@ function get_hash_fn(hash) { // Helper function function parse_cleartext_params(hash_algorithm, cleartext, algorithm) { - const hashlen = get_hash_len(hash_algorithm); - const hashfn = get_hash_fn(hash_algorithm); + if (hash_algorithm) { + const hashlen = get_hash_len(hash_algorithm); + const hashfn = get_hash_fn(hash_algorithm); - const hashtext = util.Uint8Array_to_str(cleartext.subarray(cleartext.length - hashlen, cleartext.length)); - cleartext = cleartext.subarray(0, cleartext.length - hashlen); - const hash = util.Uint8Array_to_str(hashfn(cleartext)); + const hashtext = util.Uint8Array_to_str(cleartext.subarray(cleartext.length - hashlen, cleartext.length)); + cleartext = cleartext.subarray(0, cleartext.length - hashlen); + const hash = util.Uint8Array_to_str(hashfn(cleartext)); - if (hash !== hashtext) { - return new Error("Incorrect key passphrase"); + if (hash !== hashtext) { + throw new Error("Incorrect key passphrase"); + } } const algo = enums.write(enums.publicKey, algorithm); @@ -115,9 +117,13 @@ function write_cleartext_params(hash_algorithm, algorithm, params) { const bytes = util.concatUint8Array(arr); - const hash = get_hash_fn(hash_algorithm)(bytes); + if (hash_algorithm) { + const hash = get_hash_fn(hash_algorithm)(bytes); - return util.concatUint8Array([bytes, hash]); + return util.concatUint8Array([bytes, hash]); + } + + return bytes; } @@ -125,7 +131,7 @@ function write_cleartext_params(hash_algorithm, algorithm, params) { /** * Internal parser for private keys as specified in - * {@link https://tools.ietf.org/html/rfc4880#section-5.5.3|RFC 4880 section 5.5.3} + * {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3} * @param {String} bytes Input string to read the packet from */ SecretKey.prototype.read = function (bytes) { @@ -148,9 +154,6 @@ SecretKey.prototype.read = function (bytes) { // key data. These algorithm-specific fields are as described // below. const privParams = parse_cleartext_params('mod', bytes.subarray(1, bytes.length), this.algorithm); - if (privParams instanceof Error) { - throw privParams; - } this.params = this.params.concat(privParams); this.isDecrypted = true; } @@ -194,15 +197,29 @@ SecretKey.prototype.encrypt = async function (passphrase) { const s2k = new type_s2k(); s2k.salt = await crypto.random.getRandomBytes(8); const symmetric = 'aes256'; - const cleartext = write_cleartext_params('sha1', this.algorithm, this.params); + const hash = this.version === 5 ? null : 'sha1'; + const cleartext = write_cleartext_params(hash, this.algorithm, this.params); const key = produceEncryptionKey(s2k, passphrase, symmetric); const blockLen = crypto.cipher[symmetric].blockSize; const iv = await crypto.random.getRandomBytes(blockLen); - const arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])]; - arr.push(s2k.write()); - arr.push(iv); - arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv)); + let arr; + + if (this.version === 5) { + const aead = 'eax'; + const optionalFields = util.concatUint8Array([new Uint8Array([enums.write(enums.symmetric, symmetric), enums.write(enums.aead, aead)]), s2k.write(), iv]); + arr = [new Uint8Array([253, optionalFields.length])]; + arr.push(optionalFields); + const mode = crypto[aead]; + const encrypted = await mode.encrypt(symmetric, cleartext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); + arr.push(util.writeNumber(encrypted.length, 4)); + arr.push(encrypted); + } else { + arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])]; + arr.push(s2k.write()); + arr.push(iv); + arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv)); + } this.encrypted = util.concatUint8Array(arr); return true; @@ -230,17 +247,31 @@ SecretKey.prototype.decrypt = async function (passphrase) { let i = 0; let symmetric; + let aead; let key; const s2k_usage = this.encrypted[i++]; - // - [Optional] If string-to-key usage octet was 255 or 254, a one- - // octet symmetric encryption algorithm. - if (s2k_usage === 255 || s2k_usage === 254) { + // - Only for a version 5 packet, a one-octet scalar octet count of + // the next 4 optional fields. + if (this.version === 5) { + i++; + } + + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a + // one-octet symmetric encryption algorithm. + if (s2k_usage === 255 || s2k_usage === 254 || s2k_usage === 253) { symmetric = this.encrypted[i++]; symmetric = enums.read(enums.symmetric, symmetric); - // - [Optional] If string-to-key usage octet was 255 or 254, a + // - [Optional] If string-to-key usage octet was 253, a one-octet + // AEAD algorithm. + if (s2k_usage === 253) { + aead = this.encrypted[i++]; + aead = enums.read(enums.aead, aead); + } + + // - [Optional] If string-to-key usage octet was 255, 254, or 253, a // string-to-key specifier. The length of the string-to-key // specifier is implied by its type, as described above. const s2k = new type_s2k(); @@ -263,16 +294,32 @@ SecretKey.prototype.decrypt = async function (passphrase) { i += iv.length; + // - Only for a version 5 packet, a four-octet scalar octet count for + // the following key material. + if (this.version === 5) { + i += 4; + } + const ciphertext = this.encrypted.subarray(i, this.encrypted.length); - const cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv); - const hash = s2k_usage === 254 ? - 'sha1' : + let cleartext; + if (aead) { + const mode = crypto[aead]; + try { + cleartext = await mode.decrypt(symmetric, ciphertext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); + } catch(err) { + if (err.message.startsWith('Authentication tag mismatch')) { + throw new Error('Incorrect key passphrase: ' + err.message); + } + } + } else { + cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv); + } + const hash = + s2k_usage === 253 ? null : + s2k_usage === 254 ? 'sha1' : 'mod'; const privParams = parse_cleartext_params(hash, cleartext, this.algorithm); - if (privParams instanceof Error) { - throw privParams; - } this.params = this.params.concat(privParams); this.isDecrypted = true; this.encrypted = null; diff --git a/test/general/key.js b/test/general/key.js index 6839f963..3248edcd 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -6,6 +6,23 @@ chai.use(require('chai-as-promised')); const { expect } = chai; describe('Key', function() { + describe('V4', tests); + + describe('V5', function() { + let aead_protectVal; + beforeEach(function() { + aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + }); + afterEach(function() { + openpgp.config.aead_protect = aead_protectVal; + }); + + tests(); + }); +}); + +function tests() { const twoKeys = ['-----BEGIN PGP PUBLIC KEY BLOCK-----', 'Version: GnuPG v2.0.19 (GNU/Linux)', @@ -893,6 +910,21 @@ p92yZgB3r2+f6/GIe2+7 done(); }); + it('Parsing V5 public key packet', function() { + // Manually modified from https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b2092/back.mkd#sample-eddsa-key + let packetBytes = openpgp.util.hex_to_Uint8Array(` + 98 37 05 53 f3 5f 0b 16 00 00 00 2d 09 2b 06 01 04 01 da 47 + 0f 01 01 07 40 3f 09 89 94 bd d9 16 ed 40 53 19 + 79 34 e4 a8 7c 80 73 3a 12 80 d6 2f 80 10 99 2e + 43 ee 3b 24 06 + `.replace(/\s+/g, '')); + + let packetlist = new openpgp.packet.List(); + packetlist.read(packetBytes); + let key = packetlist[0]; + expect(key).to.exist; + }); + it('Testing key ID and fingerprint for V3 and V4 keys', function(done) { const pubKeysV4 = openpgp.key.readArmored(twoKeys); expect(pubKeysV4).to.exist; @@ -1574,4 +1606,4 @@ p92yZgB3r2+f6/GIe2+7 expect(error.message).to.equal('Error encrypting message: Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex()); }); }); -}); +} diff --git a/test/general/packet.js b/test/general/packet.js index 2aba6164..3759a6fa 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -637,6 +637,38 @@ describe("Packet", function() { }); }); + it('Writing and encryption of a secret key packet. (draft04)', function() { + let aead_protectVal = openpgp.config.aead_protect; + openpgp.config.aead_protect = 'draft04'; + + const key = new openpgp.packet.List(); + key.push(new openpgp.packet.SecretKey()); + + const rsa = openpgp.crypto.publicKey.rsa; + const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys + + return rsa.generate(keySize, "10001").then(async function(mpiGen) { + let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; + mpi = mpi.map(function(k) { + return new openpgp.MPI(k); + }); + + key[0].params = mpi; + key[0].algorithm = "rsa_sign"; + await key[0].encrypt('hello'); + + const raw = key.write(); + + const key2 = new openpgp.packet.List(); + key2.read(raw); + await key2[0].decrypt('hello'); + + expect(key[0].params.toString()).to.equal(key2[0].params.toString()); + }).finally(function() { + openpgp.config.aead_protect = aead_protectVal; + }); + }); + it('Writing and verification of a signature packet.', function() { const key = new openpgp.packet.SecretKey(); From 5f891d28d69d073f2595e479fd8e4badbdab028e Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 14:46:13 +0200 Subject: [PATCH 07/51] Switch cipher/aes.js to Uint8Arrays --- src/crypto/cipher/aes.js | 8 +++----- test/crypto/cipher/aes.js | 23 ++++++++--------------- test/general/packet.js | 2 +- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/crypto/cipher/aes.js b/src/crypto/cipher/aes.js index 55505cfd..dcd2bd03 100644 --- a/src/crypto/cipher/aes.js +++ b/src/crypto/cipher/aes.js @@ -7,16 +7,14 @@ import { AES_ECB } from 'asmcrypto.js/src/aes/ecb/exports'; // TODO use webCrypto or nodeCrypto when possible. function aes(length) { const c = function(key) { - this.key = Uint8Array.from(key); + this.key = key; this.encrypt = function(block) { - block = Uint8Array.from(block); - return Array.from(AES_ECB.encrypt(block, this.key, false)); + return AES_ECB.encrypt(block, this.key, false); }; this.decrypt = function(block) { - block = Uint8Array.from(block); - return Array.from(AES_ECB.decrypt(block, this.key, false)); + return AES_ECB.decrypt(block, this.key, false); }; }; diff --git a/test/crypto/cipher/aes.js b/test/crypto/cipher/aes.js index 806f7114..c381befc 100644 --- a/test/crypto/cipher/aes.js +++ b/test/crypto/cipher/aes.js @@ -7,11 +7,13 @@ const { expect } = chai; describe('AES Rijndael cipher test with test vectors from ecb_tbl.txt', function() { function test_aes(input, key, output) { - const aes = new openpgp.crypto.cipher.aes128(key); + const aes = new openpgp.crypto.cipher.aes128(new Uint8Array(key)); - const result = util.Uint8Array_to_str(aes.encrypt(new Uint8Array(input))); + const encrypted = aes.encrypt(new Uint8Array(input)); + expect(encrypted).to.deep.equal(new Uint8Array(output)); - return util.str_to_hex(result) === util.str_to_hex(util.Uint8Array_to_str(output)); + const decrypted = aes.decrypt(new Uint8Array(output)); + expect(decrypted).to.deep.equal(new Uint8Array(input)); } const testvectors128 = [[[0x00,0x01,0x02,0x03,0x05,0x06,0x07,0x08,0x0A,0x0B,0x0C,0x0D,0x0F,0x10,0x11,0x12],[0x50,0x68,0x12,0xA4,0x5F,0x08,0xC8,0x89,0xB9,0x7F,0x59,0x80,0x03,0x8B,0x83,0x59],[0xD8,0xF5,0x32,0x53,0x82,0x89,0xEF,0x7D,0x06,0xB5,0x06,0xA4,0xFD,0x5B,0xE9,0xC9]], @@ -64,30 +66,21 @@ describe('AES Rijndael cipher test with test vectors from ecb_tbl.txt', function it('128 bit key', function (done) { for (let i = 0; i < testvectors128.length; i++) { - const res = test_aes(testvectors128[i][1],testvectors128[i][0],testvectors128[i][2]); - expect(res, 'block ' + util.Uint8Array_to_hex(testvectors128[i][1]) + - ' and key '+util.Uint8Array_to_hex(testvectors128[i][0]) + - ' should be '+util.Uint8Array_to_hex(testvectors128[i][2])).to.be.true; + test_aes(testvectors128[i][1],testvectors128[i][0],testvectors128[i][2]); } done(); }); it('192 bit key', function (done) { for (let i = 0; i < testvectors192.length; i++) { - const res = test_aes(testvectors192[i][1],testvectors192[i][0],testvectors192[i][2]); - expect(res, 'block ' + util.Uint8Array_to_hex(testvectors192[i][1]) + - ' and key ' + util.Uint8Array_to_hex(testvectors192[i][0])+ - ' should be ' + util.Uint8Array_to_hex(testvectors192[i][2])).to.be.true; + test_aes(testvectors192[i][1],testvectors192[i][0],testvectors192[i][2]); } done(); }); it('256 bit key', function (done) { for (let i = 0; i < testvectors256.length; i++) { - const res = test_aes(testvectors256[i][1],testvectors256[i][0],testvectors256[i][2]); - expect(res, 'block ' + util.Uint8Array_to_hex(testvectors256[i][1]) + - ' and key ' + util.Uint8Array_to_hex(testvectors256[i][0]) + - ' should be ' + util.Uint8Array_to_hex(testvectors256[i][2])).to.be.true; + test_aes(testvectors256[i][1],testvectors256[i][0],testvectors256[i][2]); } done(); }); diff --git a/test/general/packet.js b/test/general/packet.js index 3759a6fa..c6dc7144 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -89,7 +89,7 @@ describe("Packet", function() { message.push(enc); await enc.packets.push(literal); - const key = '12345678901234567890123456789012'; + 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'; await enc.encrypt(algo, key); From f40489aa43d55de1fce9fbd2845e8731af32790d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 14:50:07 +0200 Subject: [PATCH 08/51] Implement getLeftNBits, shiftLeft and shiftRight for Uint8Arrays --- src/crypto/public_key/dsa.js | 10 +++---- src/util.js | 53 +++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index c686b7f6..a6c1da95 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -67,9 +67,8 @@ export default { // truncated) hash function result is treated as a number and used // directly in the DSA signature algorithm. const h = new BN( - util.str_to_Uint8Array( - util.getLeftNBits( - util.Uint8Array_to_str(hash.digest(hash_algo, m)), q.bitLength()))); + util.getLeftNBits( + hash.digest(hash_algo, m), q.bitLength())); // FIPS-186-4, section 4.6: // The values of r and s shall be checked to determine if r = 0 or s = 0. // If either r = 0 or s = 0, a new value of k shall be generated, and the @@ -116,9 +115,8 @@ export default { const redp = new BN.red(p); const redq = new BN.red(q); const h = new BN( - util.str_to_Uint8Array( - util.getLeftNBits( - util.Uint8Array_to_str(hash.digest(hash_algo, m)), q.bitLength()))); + util.getLeftNBits( + hash.digest(hash_algo, m), q.bitLength())); const w = s.toRed(redq).redInvm(); // s**-1 mod q if (zero.cmp(w) === 0) { util.print_debug("invalid DSA Signature"); diff --git a/src/util.js b/src/util.js index cd3bf2d1..ca062489 100644 --- a/src/util.js +++ b/src/util.js @@ -388,14 +388,13 @@ export default { } }, - // TODO rewrite getLeftNBits to work with Uint8Arrays - getLeftNBits: function (string, bitcount) { + getLeftNBits: function (array, bitcount) { const rest = bitcount % 8; if (rest === 0) { - return string.substring(0, bitcount / 8); + return array.subarray(0, bitcount / 8); } const bytes = (bitcount - rest) / 8 + 1; - const result = string.substring(0, bytes); + const result = array.subarray(0, bytes); return util.shiftRight(result, 8 - rest); // +String.fromCharCode(string.charCodeAt(bytes -1) << (8-rest) & 0xFF); }, @@ -431,25 +430,41 @@ export default { }, /** - * Shifting a string to n bits right - * @param {String} value The string to shift - * @param {Integer} bitcount Amount of bits to shift (MUST be smaller - * than 9) - * @returns {String} Resulting string. + * Shift a Uint8Array to the left by n bits + * @param {Uint8Array} array The array to shift + * @param {Integer} bits Amount of bits to shift (MUST be smaller + * than 8) + * @returns {String} Resulting array. */ - shiftRight: function (value, bitcount) { - const temp = util.str_to_Uint8Array(value); - if (bitcount % 8 !== 0) { - for (let i = temp.length - 1; i >= 0; i--) { - temp[i] >>= bitcount % 8; - if (i > 0) { - temp[i] |= (temp[i - 1] << (8 - (bitcount % 8))) & 0xFF; + shiftLeft: function (array, bits) { + if (bits) { + for (let i = 0; i < array.length; i++) { + array[i] <<= bits; + if (i + 1 < array.length) { + array[i] |= array[i + 1] >> (8 - bits); } } - } else { - return value; } - return util.Uint8Array_to_str(temp); + return array; + }, + + /** + * Shift a Uint8Array to the right by n bits + * @param {Uint8Array} array The array to shift + * @param {Integer} bits Amount of bits to shift (MUST be smaller + * than 8) + * @returns {String} Resulting array. + */ + shiftRight: function (array, bits) { + if (bits) { + for (let i = array.length - 1; i >= 0; i--) { + array[i] >>= bits; + if (i > 0) { + array[i] |= (array[i - 1] << (8 - bits)); + } + } + } + return array; }, /** From cc4cc38fe7a451fb7c44b1617309ec891577d19b Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 14:57:19 +0200 Subject: [PATCH 09/51] Add util.print_debug_hexarray_dump --- src/util.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/util.js b/src/util.js index ca062489..af8a93cd 100644 --- a/src/util.js +++ b/src/util.js @@ -362,6 +362,20 @@ export default { } }, + /** + * Helper function to print a debug message. Debug + * messages are only printed if + * @link module:config/config.debug is set to true. + * Different than print_debug because will call Uint8Array_to_hex iff necessary. + * @param {String} str String of the debug message + */ + print_debug_hexarray_dump: function (str, arrToHex) { + if (config.debug) { + str += ': ' + util.Uint8Array_to_hex(arrToHex); + console.log(str); + } + }, + /** * Helper function to print a debug message. Debug * messages are only printed if From ba2b761da4615385fd2492bc5acc7d4d758dedcf Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 6 Apr 2018 15:29:17 +0200 Subject: [PATCH 10/51] Implement OCB mode --- src/crypto/index.js | 3 + src/crypto/ocb.js | 312 +++++++++++++++++++++++++++++++++++++++++++ test/crypto/index.js | 1 + test/crypto/ocb.js | 152 +++++++++++++++++++++ 4 files changed, 468 insertions(+) create mode 100644 src/crypto/ocb.js create mode 100644 test/crypto/ocb.js diff --git a/src/crypto/index.js b/src/crypto/index.js index 4c0178a4..0626df39 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -14,6 +14,7 @@ import hash from './hash'; import cfb from './cfb'; import gcm from './gcm'; import eax from './eax'; +import ocb from './ocb'; import publicKey from './public_key'; import signature from './signature'; import random from './random'; @@ -34,6 +35,8 @@ const mod = { gcm: gcm, /** @see module:crypto/eax */ eax: eax, + /** @see module:crypto/ocb */ + ocb: ocb, /** @see module:crypto/public_key */ publicKey: publicKey, /** @see module:crypto/signature */ diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js new file mode 100644 index 00000000..c0467772 --- /dev/null +++ b/src/crypto/ocb.js @@ -0,0 +1,312 @@ +// 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-OCB en/decryption. + * @requires crypto/cipher + * @requires util + * @module crypto/ocb + */ + +import ciphers from './cipher'; +import util from '../util'; + + +const blockLength = 16; +const ivLength = 15; + +// https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2: +// While OCB [RFC7253] allows the authentication tag length to be of any +// number up to 128 bits long, this document requires a fixed +// authentication tag length of 128 bits (16 octets) for simplicity. +const tagLength = 16; + + +const { shiftLeft, shiftRight } = util; + + +function zeros(bytes) { + return new Uint8Array(bytes); +} + +function ntz(n) { + let ntz = 0; + for(let i = 1; (n & i) === 0; i <<= 1) { + ntz++; + } + return ntz; +} + +function set_xor(S, T) { + for (let i = 0; i < S.length; i++) { + S[i] ^= T[i]; + } + return S; +} + +function xor(S, T) { + return set_xor(S.slice(), T); +} + +function concat(...arrays) { + return util.concatUint8Array(arrays); +} + +function double(S) { + const double = S.slice(); + shiftLeft(double, 1); + if (S[0] & 0b10000000) { + double[15] ^= 0b10000111; + } + return double; +} + + +function constructKeyVariables(cipher, key, text, adata) { + const aes = new ciphers[cipher](key); + const encipher = aes.encrypt.bind(aes); + const decipher = aes.decrypt.bind(aes); + + const L_x = encipher(zeros(16)); + const L_$ = double(L_x); + const L = []; + L[0] = double(L_$); + + const max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; + for (let i = 1; i <= max_ntz; i++) { + L[i] = double(L[i - 1]); + } + + L.x = L_x; + L.$ = L_$; + + return { encipher, decipher, L }; +} + +function hash(kv, key, adata) { + if (!adata.length) { + // Fast path + return zeros(16); + } + + const { encipher, L } = kv; + + // + // Consider A as a sequence of 128-bit blocks + // + const m = adata.length >> 4; + + const offset = zeros(16); + const sum = zeros(16); + for (let i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + set_xor(sum, encipher(xor(offset, adata))); + adata = adata.subarray(16); + } + + // + // Process any final partial block; compute final hash value + // + if (adata.length) { + set_xor(offset, L.x); + + const cipherInput = zeros(16); + cipherInput.set(adata, 0); + cipherInput[adata.length] = 0b10000000; + set_xor(cipherInput, offset); + + set_xor(sum, encipher(cipherInput)); + } + + return sum; +} + + +/** + * 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 (15 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output + */ +async function encrypt(cipher, plaintext, key, nonce, adata) { + // + // Consider P as a sequence of 128-bit blocks + // + const m = plaintext.length >> 4; + + // + // Key-dependent variables + // + const kv = constructKeyVariables(cipher, key, plaintext, adata); + const { encipher, L } = kv; + + // + // Nonce-dependent and per-encryption variables + // + // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). + const Nonce = concat(zeros(15 - nonce.length), new Uint8Array([1]), nonce); + const bottom = Nonce[15] & 0b111111; + Nonce[15] &= 0b11000000; + const Ktop = encipher(Nonce); + const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = zeros(16); + + const C = new Uint8Array(plaintext.length + tagLength); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + C.set(xor(offset, encipher(xor(offset, plaintext))), pos); + set_xor(checksum, plaintext); + + plaintext = plaintext.subarray(16); + pos += 16; + } + + // + // Process any final partial block and compute raw tag + // + if (plaintext.length) { + set_xor(offset, L.x); + const Pad = encipher(offset); + C.set(xor(plaintext, Pad), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) + const xorInput = zeros(16); + xorInput.set(plaintext, 0); + xorInput[plaintext.length] = 0b10000000; + set_xor(checksum, xorInput); + pos += plaintext.length; + } + const Tag = xor(encipher(xor(xor(checksum, offset), L.$)), hash(kv, key, adata)); + + // + // Assemble ciphertext + // + C.set(Tag, pos); + return C; +} + + +/** + * 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 (15 bytes) + * @param {Uint8Array} adata Associated data to verify + * @returns {Promise} The plaintext output + */ +async function decrypt(cipher, ciphertext, key, nonce, adata) { + // + // Consider C as a sequence of 128-bit blocks + // + const T = ciphertext.subarray(ciphertext.length - tagLength); + ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); + const m = ciphertext.length >> 4; + + // + // Key-dependent variables + // + const kv = constructKeyVariables(cipher, key, ciphertext, adata); + const { encipher, decipher, L } = kv; + + // + // Nonce-dependent and per-encryption variables + // + // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). + const Nonce = concat(zeros(15 - nonce.length), new Uint8Array([1]), nonce); + const bottom = Nonce[15] & 0b111111; + Nonce[15] &= 0b11000000; + const Ktop = encipher(Nonce); + const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = zeros(16); + + const P = new Uint8Array(ciphertext.length); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + P.set(xor(offset, decipher(xor(offset, ciphertext))), pos); + set_xor(checksum, P.subarray(pos)); + + ciphertext = ciphertext.subarray(16); + pos += 16; + } + + // + // Process any final partial block and compute raw tag + // + if (ciphertext.length) { + set_xor(offset, L.x); + const Pad = encipher(offset); + P.set(xor(ciphertext, Pad), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) + const xorInput = zeros(16); + xorInput.set(P.subarray(pos), 0); + xorInput[ciphertext.length] = 0b10000000; + set_xor(checksum, xorInput); + pos += ciphertext.length; + } + const Tag = xor(encipher(xor(xor(checksum, offset), L.$)), hash(kv, key, adata)); + + // + // Check for validity and assemble plaintext + // + if (!util.equalsUint8Array(Tag, T)) { + throw new Error('Authentication tag mismatch in OCB ciphertext'); + } + return P; +} + +/** + * Get OCB nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2|RFC4880bis-04, section 5.16.2}. + * @param {Uint8Array} iv The initialization vector (15 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[7 + i] ^= chunkIndex[i]; + } + return nonce; +} + + +export default { + blockLength, + ivLength, + encrypt, + decrypt, + getNonce +}; diff --git a/test/crypto/index.js b/test/crypto/index.js index 9fc50442..762d9c95 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -7,4 +7,5 @@ describe('Crypto', function () { require('./pkcs5.js'); require('./aes_kw.js'); require('./eax.js'); + require('./ocb.js'); }); diff --git a/test/crypto/ocb.js b/test/crypto/ocb.js new file mode 100644 index 00000000..a3358913 --- /dev/null +++ b/test/crypto/ocb.js @@ -0,0 +1,152 @@ +// 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 ocb = openpgp.crypto.ocb; + +describe('Symmetric AES-OCB', function() { + it('Passes all test vectors', async function() { + const K = '000102030405060708090A0B0C0D0E0F'; + const keyBytes = openpgp.util.hex_to_Uint8Array(K); + + var vectors = [ + // From https://tools.ietf.org/html/rfc7253#appendix-A + { + N: 'BBAA99887766554433221100', + A: '', + P: '', + C: '785407BFFFC8AD9EDCC5520AC9111EE6' + }, + { + N: 'BBAA99887766554433221101', + A: '0001020304050607', + P: '0001020304050607', + C: '6820B3657B6F615A5725BDA0D3B4EB3A257C9AF1F8F03009' + }, + { + N: 'BBAA99887766554433221102', + A: '0001020304050607', + P: '', + C: '81017F8203F081277152FADE694A0A00' + }, + { + N: 'BBAA99887766554433221103', + A: '', + P: '0001020304050607', + C: '45DD69F8F5AAE72414054CD1F35D82760B2CD00D2F99BFA9' + }, + { + N: 'BBAA99887766554433221104', + A: '000102030405060708090A0B0C0D0E0F', + P: '000102030405060708090A0B0C0D0E0F', + C: '571D535B60B277188BE5147170A9A22C3AD7A4FF3835B8C5701C1CCEC8FC3358' + }, + { + N: 'BBAA99887766554433221105', + A: '000102030405060708090A0B0C0D0E0F', + P: '', + C: '8CF761B6902EF764462AD86498CA6B97' + }, + { + N: 'BBAA99887766554433221106', + A: '', + P: '000102030405060708090A0B0C0D0E0F', + C: '5CE88EC2E0692706A915C00AEB8B2396F40E1C743F52436BDF06D8FA1ECA343D' + }, + { + N: 'BBAA99887766554433221107', + A: '000102030405060708090A0B0C0D0E0F1011121314151617', + P: '000102030405060708090A0B0C0D0E0F1011121314151617', + C: '1CA2207308C87C010756104D8840CE1952F09673A448A122C92C62241051F57356D7F3C90BB0E07F' + }, + { + N: 'BBAA99887766554433221108', + A: '000102030405060708090A0B0C0D0E0F1011121314151617', + P: '', + C: '6DC225A071FC1B9F7C69F93B0F1E10DE' + }, + { + N: 'BBAA99887766554433221109', + A: '', + P: '000102030405060708090A0B0C0D0E0F1011121314151617', + C: '221BD0DE7FA6FE993ECCD769460A0AF2D6CDED0C395B1C3CE725F32494B9F914D85C0B1EB38357FF' + }, + { + N: 'BBAA9988776655443322110A', + A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', + P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', + C: 'BD6F6C496201C69296C11EFD138A467ABD3C707924B964DEAFFC40319AF5A48540FBBA186C5553C68AD9F592A79A4240' + }, + { + N: 'BBAA9988776655443322110B', + A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', + P: '', + C: 'FE80690BEE8A485D11F32965BC9D2A32' + }, + { + N: 'BBAA9988776655443322110C', + A: '', + P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F', + C: '2942BFC773BDA23CABC6ACFD9BFD5835BD300F0973792EF46040C53F1432BCDFB5E1DDE3BC18A5F840B52E653444D5DF' + }, + { + N: 'BBAA9988776655443322110D', + A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627', + P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627', + C: 'D5CA91748410C1751FF8A2F618255B68A0A12E093FF454606E59F9C1D0DDC54B65E8628E568BAD7AED07BA06A4A69483A7035490C5769E60' + }, + { + N: 'BBAA9988776655443322110E', + A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627', + P: '', + C: 'C5CD9D1850C141E358649994EE701B68' + }, + { + N: 'BBAA9988776655443322110F', + A: '', + P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627', + C: '4412923493C57D5DE0D700F753CCE0D1D2D95060122E9F15A5DDBFC5787E50B5CC55EE507BCB084E479AD363AC366B95A98CA5F3000B1479' + } + ]; + + const cipher = 'aes128'; + + for(const [i, vec] of vectors.entries()) { + const msgBytes = openpgp.util.hex_to_Uint8Array(vec.P), + nonceBytes = openpgp.util.hex_to_Uint8Array(vec.N), + headerBytes = openpgp.util.hex_to_Uint8Array(vec.A), + ctBytes = openpgp.util.hex_to_Uint8Array(vec.C); + + // encryption test + let ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.C.toLowerCase()); + + // decryption test with verification + let pt = await ocb.decrypt(cipher, ctBytes, keyBytes, nonceBytes, headerBytes); + expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); + + // tampering detection test + ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + ct[2] ^= 8; + pt = ocb.decrypt(cipher, ct, keyBytes, nonceBytes, headerBytes); + await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in OCB ciphertext') + + // testing without additional data + ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, new Uint8Array()); + pt = await ocb.decrypt(cipher, ct, keyBytes, nonceBytes, new Uint8Array()); + expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); + + // testing with multiple additional data + ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + pt = await ocb.decrypt(cipher, ct, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); + } + }); +}); From c6ba83c4a34b2ad9ab53752c04b57ae76d512c5d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 14:41:35 +0200 Subject: [PATCH 11/51] Allow configuring openpgp in unit tests using query params (e.g. ?debug=true&use_native=false) --- src/packet/packet.js | 3 --- test/unittests.js | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/packet/packet.js b/src/packet/packet.js index 0be5a759..ac7b6521 100644 --- a/src/packet/packet.js +++ b/src/packet/packet.js @@ -177,15 +177,12 @@ export default { // 4.2.2.1. One-Octet Lengths if (input[mypos] < 192) { packet_length = input[mypos++]; - util.print_debug("1 byte length:" + packet_length); // 4.2.2.2. Two-Octet Lengths } else if (input[mypos] >= 192 && input[mypos] < 224) { packet_length = ((input[mypos++] - 192) << 8) + (input[mypos++]) + 192; - util.print_debug("2 byte length:" + packet_length); // 4.2.2.4. Partial Body Lengths } else if (input[mypos] > 223 && input[mypos] < 255) { packet_length = 1 << (input[mypos++] & 0x1F); - util.print_debug("4 byte length:" + packet_length); // EEEK, we're reading the full data here... let mypos2 = mypos + packet_length; bodydata = [input.subarray(mypos, mypos + packet_length)]; diff --git a/test/unittests.js b/test/unittests.js index e5f89866..f25c0998 100644 --- a/test/unittests.js +++ b/test/unittests.js @@ -38,6 +38,13 @@ describe('Unit Tests', function () { window.scrollTo(0, document.body.scrollHeight); } }); + + window.location.search.substr(1).split('&').forEach(param => { + const [key, value] = param.split('='); + if (key && key !== 'grep') { + openpgp.config[key] = JSON.parse(value); + } + }); } require('./crypto'); From 627a6ef46e1f9e8b9243d65019257b5158c85a99 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 16:06:11 +0200 Subject: [PATCH 12/51] Only calculate AES key schedules once in cipher/aes.js --- src/crypto/cipher/aes.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/crypto/cipher/aes.js b/src/crypto/cipher/aes.js index dcd2bd03..8749a718 100644 --- a/src/crypto/cipher/aes.js +++ b/src/crypto/cipher/aes.js @@ -2,19 +2,20 @@ * @requires asmcrypto.js */ -import { AES_ECB } from 'asmcrypto.js/src/aes/ecb/exports'; +import { _AES_asm_instance, _AES_heap_instance } from 'asmcrypto.js/src/aes/exports'; +import { AES_ECB } from 'asmcrypto.js/src/aes/ecb/ecb'; // TODO use webCrypto or nodeCrypto when possible. function aes(length) { const c = function(key) { - this.key = key; + const aes_ecb = new AES_ECB(key, _AES_heap_instance, _AES_asm_instance); this.encrypt = function(block) { - return AES_ECB.encrypt(block, this.key, false); + return aes_ecb.encrypt(block).result; }; this.decrypt = function(block) { - return AES_ECB.decrypt(block, this.key, false); + return aes_ecb.decrypt(block).result; }; }; From 93f75f398fe9efcdd6a37b97d72bbd3eb4119d21 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 16:47:58 +0200 Subject: [PATCH 13/51] Reuse CMAC in EAX mode --- src/crypto/cmac.js | 21 +++++++++++++++++++++ src/crypto/eax.js | 31 ++++++++++++++++++------------- 2 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 src/crypto/cmac.js diff --git a/src/crypto/cmac.js b/src/crypto/cmac.js new file mode 100644 index 00000000..34061258 --- /dev/null +++ b/src/crypto/cmac.js @@ -0,0 +1,21 @@ +/** + * @requires asmcrypto.js + */ + +import { AES_CMAC } from 'asmcrypto.js/src/aes/cmac/cmac'; + +export default class CMAC extends AES_CMAC { + constructor(key) { + super(key); + this._k = this.k.slice(); + } + + mac(data) { + if (this.result) { + this.bufferLength = 0; + this.k.set(this._k, 0); + this.cbc.AES_reset(undefined, new Uint8Array(16), false); + } + return this.process(data).finish().result; + } +} diff --git a/src/crypto/eax.js b/src/crypto/eax.js index ac139f6a..6a8dcad8 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -19,12 +19,13 @@ * @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 crypto/cmac * @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 CMAC from './cmac'; import util from '../util'; const webCrypto = util.getWebCryptoAll(); @@ -40,6 +41,12 @@ 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]); +class OMAC extends CMAC { + mac(t, message) { + return super.mac(concat(t, message)); + } +} + /** * Encrypt plaintext input. @@ -55,11 +62,12 @@ async function encrypt(cipher, plaintext, key, nonce, adata) { throw new Error('EAX mode supports only AES cipher'); } - const _nonce = OMAC(zero, nonce, key); - const _adata = OMAC(one, adata, key); + const omac = new OMAC(key); + const _nonce = omac.mac(zero, nonce); + const _adata = omac.mac(one, adata); const ciphered = await CTR(plaintext, key, _nonce); - const _ciphered = OMAC(two, ciphered, key); - const tag = xor3(_nonce, _ciphered, _adata); // Assumes that OMAC(*).length === tagLength. + const _ciphered = omac.mac(two, ciphered); + const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac.mac(*).length === tagLength. return concat(ciphered, tag); } @@ -80,10 +88,11 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) { 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. + const omac = new OMAC(key); + const _nonce = omac.mac(zero, nonce); + const _adata = omac.mac(one, adata); + const _ciphered = omac.mac(two, ciphered); + const _tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac.mac(*).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; @@ -127,10 +136,6 @@ 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); From 5f97a8c9377394d681b3094a2bbac9dadd954d82 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 18:34:24 +0200 Subject: [PATCH 14/51] Implement preferred AEAD algorithms --- src/config/config.js | 7 ++++ src/enums.js | 3 +- src/key.js | 37 ++++++++++++++++++++++ src/message.js | 22 +++++++++---- src/openpgp.js | 7 ++-- src/packet/signature.js | 9 ++++++ src/packet/sym_encrypted_aead_protected.js | 3 +- src/packet/sym_encrypted_session_key.js | 2 +- test/general/key.js | 22 +++++++++++++ 9 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/config/config.js b/src/config/config.js index 1373430d..e2cfef90 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -51,6 +51,13 @@ export default { * @property {Boolean} aead_protect */ aead_protect: false, + /** + * Default Authenticated Encryption with Additional Data (AEAD) encryption mode + * Only has an effect when aead_protect is set to true. + * @memberof module:config + * @property {Integer} aead_mode Default AEAD mode {@link module:enums.aead} + */ + aead_mode: enums.aead.eax, /** * Chunk Size Byte for Authenticated Encryption with Additional Data (AEAD) mode * Only has an effect when aead_protect is set to true. diff --git a/src/enums.js b/src/enums.js index ab9451a6..62297ca1 100644 --- a/src/enums.js +++ b/src/enums.js @@ -366,7 +366,8 @@ export default { reason_for_revocation: 29, features: 30, signature_target: 31, - embedded_signature: 32 + embedded_signature: 32, + preferred_aead_algorithms: 34 }, /** Key flags diff --git a/src/key.js b/src/key.js index 5f21d9f0..c6784f0e 100644 --- a/src/key.js +++ b/src/key.js @@ -1261,6 +1261,11 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.cast5); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.tripledes); + if (config.aead_protect === 'draft04') { + signaturePacket.preferredAeadAlgorithms = []; + signaturePacket.preferredAeadAlgorithms.push(enums.aead.eax); + signaturePacket.preferredAeadAlgorithms.push(enums.aead.ocb); + } signaturePacket.preferredHashAlgorithms = []; // prefer fast asm.js implementations (SHA-256). SHA-1 will not be secure much longer...move to bottom of list signaturePacket.preferredHashAlgorithms.push(enums.hash.sha256); @@ -1457,3 +1462,35 @@ export async function getPreferredSymAlgo(keys) { } return prefAlgo.algo; } + +/** + * Returns the preferred aead algorithm for a set of keys + * @param {Array} keys Set of keys + * @returns {Promise} Preferred aead algorithm + * @async + */ +export async function getPreferredAeadAlgo(keys) { + const prioMap = {}; + await Promise.all(keys.map(async function(key) { + const primaryUser = await key.getPrimaryUser(); + if (!primaryUser || !primaryUser.selfCertification.preferredAeadAlgorithms) { + return config.aead_mode; + } + primaryUser.selfCertification.preferredAeadAlgorithms.forEach(function(algo, index) { + const entry = prioMap[algo] || (prioMap[algo] = { prio: 0, count: 0, algo: algo }); + entry.prio += 64 >> index; + entry.count++; + }); + })); + let prefAlgo = { prio: 0, algo: config.aead_mode }; + for (const algo in prioMap) { + try { + if (enums.read(enums.aead, algo) && // known algorithm + prioMap[algo].count === keys.length && // available for all keys + prioMap[algo].prio > prefAlgo.prio) { + prefAlgo = prioMap[algo]; + } + } catch (e) {} + } + return prefAlgo.algo; +} diff --git a/src/message.js b/src/message.js index f0e1211f..57f43ba4 100644 --- a/src/message.js +++ b/src/message.js @@ -36,7 +36,7 @@ import enums from './enums'; import util from './util'; import packet from './packet'; import { Signature } from './signature'; -import { getPreferredHashAlgo, getPreferredSymAlgo } from './key'; +import { getPreferredHashAlgo, getPreferredSymAlgo, getPreferredAeadAlgo } from './key'; /** @@ -252,6 +252,7 @@ Message.prototype.getText = function() { */ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard=false, date=new Date()) { let symAlgo; + let aeadAlgo; let symEncryptedPacket; if (sessionKey) { @@ -259,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; sessionKey = sessionKey.data; } else if (keys && keys.length) { symAlgo = enums.read(enums.symmetric, await getPreferredSymAlgo(keys)); + aeadAlgo = enums.read(enums.aead, await getPreferredAeadAlgo(keys)); } else if (passwords && passwords.length) { symAlgo = enums.read(enums.symmetric, config.encryption_cipher); + aeadAlgo = enums.read(enums.aead, config.aead_mode); } else { throw new Error('No keys, passwords, or session key provided.'); } @@ -272,10 +276,11 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard sessionKey = await crypto.generateSessionKey(symAlgo); } - const msg = await encryptSessionKey(sessionKey, symAlgo, keys, passwords, wildcard, date); + const msg = await encryptSessionKey(sessionKey, symAlgo, aeadAlgo, keys, passwords, wildcard, date); if (config.aead_protect) { symEncryptedPacket = new packet.SymEncryptedAEADProtected(); + symEncryptedPacket.aeadAlgorithm = aeadAlgo; } else if (config.integrity_protect) { symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); } else { @@ -291,7 +296,8 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard message: msg, sessionKey: { data: sessionKey, - algorithm: symAlgo + algorithm: symAlgo, + aeadAlgorithm: aeadAlgo } }; }; @@ -300,6 +306,7 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard * Encrypt a session key either with public keys, passwords, or both at once. * @param {Uint8Array} sessionKey session key for encryption * @param {String} symAlgo session key algorithm + * @param {String} aeadAlgo (optional) aead algorithm, e.g. 'eax' or 'ocb' * @param {Array} publicKeys (optional) public key(s) for message encryption * @param {Array} passwords (optional) for message encryption * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs @@ -307,7 +314,7 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard * @returns {Promise} new message with encrypted content * @async */ -export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwords, wildcard=false, date=new Date()) { +export async function encryptSessionKey(sessionKey, symAlgo, aeadAlgo, publicKeys, passwords, wildcard=false, date=new Date()) { const packetlist = new packet.List(); if (publicKeys) { @@ -340,10 +347,13 @@ export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwor const sum = (accumulator, currentValue) => accumulator + currentValue; - const encryptPassword = async function(sessionKey, symAlgo, password) { + const encryptPassword = async function(sessionKey, symAlgo, aeadAlgo, password) { const symEncryptedSessionKeyPacket = new packet.SymEncryptedSessionKey(); symEncryptedSessionKeyPacket.sessionKey = sessionKey; symEncryptedSessionKeyPacket.sessionKeyAlgorithm = symAlgo; + if (aeadAlgo) { + symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgo; + } await symEncryptedSessionKeyPacket.encrypt(password); if (config.password_collision_check) { @@ -357,7 +367,7 @@ export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwor return symEncryptedSessionKeyPacket; }; - const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symAlgo, pwd))); + const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symAlgo, aeadAlgo, pwd))); packetlist.concat(results); } diff --git a/src/openpgp.js b/src/openpgp.js index 4b32fdf0..fbbb4148 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -395,6 +395,7 @@ export function verify({ message, publicKeys, signature=null, date=new Date() }) * or passwords must be specified. * @param {Uint8Array} data the session key to be encrypted e.g. 16 random bytes (for aes128) * @param {String} algorithm algorithm of the symmetric session key e.g. 'aes128' or 'aes256' + * @param {String} aeadAlgorithm (optional) aead algorithm, e.g. 'eax' or 'ocb' * @param {Key|Array} publicKeys (optional) array of public keys or single key, used to encrypt the key * @param {String|Array} passwords (optional) passwords for the message * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs @@ -402,16 +403,16 @@ export function verify({ message, publicKeys, signature=null, date=new Date() }) * @async * @static */ -export function encryptSessionKey({ data, algorithm, publicKeys, passwords, wildcard=false }) { +export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard=false }) { checkBinary(data); checkString(algorithm, 'algorithm'); publicKeys = toArray(publicKeys); passwords = toArray(passwords); if (asyncProxy) { // use web worker if available - return asyncProxy.delegate('encryptSessionKey', { data, algorithm, publicKeys, passwords, wildcard }); + return asyncProxy.delegate('encryptSessionKey', { data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard }); } return Promise.resolve().then(async function() { - return { message: await messageLib.encryptSessionKey(data, algorithm, publicKeys, passwords, wildcard) }; + return { message: await messageLib.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard) }; }).catch(onError.bind(null, 'Error encrypting session key')); } diff --git a/src/packet/signature.js b/src/packet/signature.js index 1b629cce..e3210e01 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -84,6 +84,7 @@ function Signature(date=new Date()) { this.signatureTargetHashAlgorithm = null; this.signatureTargetHash = null; this.embeddedSignature = null; + this.preferredAeadAlgorithms = null; this.verified = null; this.revoked = null; @@ -355,6 +356,10 @@ Signature.prototype.write_all_sub_packets = function () { if (this.embeddedSignature !== null) { arr.push(write_sub_packet(sub.embedded_signature, this.embeddedSignature.write())); } + if (this.preferredAeadAlgorithms !== null) { + bytes = util.str_to_Uint8Array(util.Uint8Array_to_str(this.preferredAeadAlgorithms)); + arr.push(write_sub_packet(sub.preferred_aead_algorithms, bytes)); + } const result = util.concatUint8Array(arr); const length = util.writeNumber(result.length, 2); @@ -531,6 +536,10 @@ Signature.prototype.read_sub_packet = function (bytes) { this.embeddedSignature = new Signature(); this.embeddedSignature.read(bytes.subarray(mypos, bytes.length)); break; + case 34: + // Preferred AEAD Algorithms + read_array.call(this, 'preferredAeadAlgorithms', bytes.subarray(mypos, bytes.length)); + break; default: util.print_debug("Unknown signature subpacket type " + type + " @:" + mypos); } diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 1749e945..766a3bb8 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -42,6 +42,7 @@ function SymEncryptedAEADProtected() { this.tag = enums.packet.symEncryptedAEADProtected; this.version = VERSION; this.cipherAlgo = null; + this.aeadAlgorithm = 'eax'; this.aeadAlgo = null; this.chunkSizeByte = null; this.iv = null; @@ -131,7 +132,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith * @async */ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { - this.aeadAlgo = config.aead_protect === 'draft04' ? enums.aead.eax : enums.aead.gcm; + this.aeadAlgo = config.aead_protect === 'draft04' ? enums.write(enums.aead, this.aeadAlgorithm) : 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(); diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index eea69222..9e4d6de0 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -53,7 +53,7 @@ function SymEncryptedSessionKey() { this.sessionKey = null; this.sessionKeyEncryptionAlgorithm = null; this.sessionKeyAlgorithm = 'aes256'; - this.aeadAlgorithm = 'eax'; + this.aeadAlgorithm = enums.read(enums.aead, config.aead_mode); this.encrypted = null; this.s2k = null; this.iv = null; diff --git a/test/general/key.js b/test/general/key.js index 3248edcd..eb396e38 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1192,6 +1192,24 @@ p92yZgB3r2+f6/GIe2+7 expect(prefAlgo).to.equal(openpgp.config.encryption_cipher); }); + it('getPreferredAeadAlgo() - one key - OCB', async function() { + const key1 = openpgp.key.readArmored(twoKeys).keys[0]; + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.preferredAeadAlgorithms = [2,1]; + const prefAlgo = await openpgp.key.getPreferredAeadAlgo([key1]); + expect(prefAlgo).to.equal(openpgp.enums.aead.ocb); + }); + + it('getPreferredAeadAlgo() - two key - one without pref', async function() { + const keys = openpgp.key.readArmored(twoKeys).keys; + const key1 = keys[0]; + const key2 = keys[1]; + const primaryUser = await key1.getPrimaryUser(); + primaryUser.selfCertification.preferredAeadAlgorithms = [2,1]; + const prefAlgo = await openpgp.key.getPreferredAeadAlgo([key1, key2]); + expect(prefAlgo).to.equal(openpgp.config.aead_mode); + }); + it('Preferences of generated key', function() { const testPref = function(key) { // key flags @@ -1202,6 +1220,10 @@ p92yZgB3r2+f6/GIe2+7 expect(key.subKeys[0].bindingSignatures[0].keyFlags[0] & keyFlags.encrypt_storage).to.equal(keyFlags.encrypt_storage); const sym = openpgp.enums.symmetric; expect(key.users[0].selfCertifications[0].preferredSymmetricAlgorithms).to.eql([sym.aes256, sym.aes128, sym.aes192, sym.cast5, sym.tripledes]); + if (openpgp.config.aead_protect === 'draft04') { + const aead = openpgp.enums.aead; + expect(key.users[0].selfCertifications[0].preferredAeadAlgorithms).to.eql([aead.eax, aead.ocb]); + } const hash = openpgp.enums.hash; expect(key.users[0].selfCertifications[0].preferredHashAlgorithms).to.eql([hash.sha256, hash.sha512, hash.sha1]); const compr = openpgp.enums.compression; From f225f994ec71b6150d9f133b43069b48f263e725 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 17:32:53 +0200 Subject: [PATCH 15/51] Add AEAD-OCB test vector --- test/general/packet.js | 80 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/test/general/packet.js b/test/general/packet.js index c6dc7144..4f951e8c 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -453,8 +453,8 @@ describe("Packet", function() { } }); - it('Sym. encrypted session key reading/writing test vector (draft04)', async function() { - // From https://gitlab.com/openpgp-wg/rfc4880bis/commit/00b20923e6233fb6ff1666ecd5acfefceb32907d + it('Sym. encrypted session key reading/writing test vector (EAX, draft04)', async function() { + // From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-eax-encryption-and-decryption let aead_protectVal = openpgp.config.aead_protect; let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; @@ -528,6 +528,82 @@ describe("Packet", function() { } }); + it('Sym. encrypted session key reading/writing test vector (OCB, draft04)', async function() { + // From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-ocb-encryption-and-decryption + + let aead_protectVal = openpgp.config.aead_protect; + let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; + let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte; + openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_chunk_size_byte = 14; + openpgp.config.s2k_iteration_count_byte = 0x90; + + let salt = openpgp.util.hex_to_Uint8Array(`9f0b7da3e5ea6477`); + let sessionKey = openpgp.util.hex_to_Uint8Array(`d1 f0 1b a3 0e 13 0a a7 d2 58 2c 16 e0 50 ae 44`.replace(/\s+/g, '')); + let sessionIV = openpgp.util.hex_to_Uint8Array(`99 e3 26 e5 40 0a 90 93 6c ef b4 e8 eb a0 8c`.replace(/\s+/g, '')); + let dataIV = openpgp.util.hex_to_Uint8Array(`5e d2 bc 1e 47 0a be 8f 1d 64 4c 7a 6c 8a 56`.replace(/\s+/g, '')); + + let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); + randomBytesStub.onCall(0).returns(resolves(salt)); + randomBytesStub.onCall(1).returns(resolves(sessionKey)); + randomBytesStub.onCall(2).returns(resolves(sessionIV)); + randomBytesStub.onCall(3).returns(resolves(dataIV)); + + let packetBytes = openpgp.util.hex_to_Uint8Array(` + c3 3d 05 07 02 03 08 9f 0b 7d a3 e5 ea 64 77 90 + 99 e3 26 e5 40 0a 90 93 6c ef b4 e8 eb a0 8c 67 + 73 71 6d 1f 27 14 54 0a 38 fc ac 52 99 49 da c5 + 29 d3 de 31 e1 5b 4a eb 72 9e 33 00 33 db ed + + d4 49 01 07 02 0e 5e d2 bc 1e 47 0a be 8f 1d 64 + 4c 7a 6c 8a 56 7b 0f 77 01 19 66 11 a1 54 ba 9c + 25 74 cd 05 62 84 a8 ef 68 03 5c 62 3d 93 cc 70 + 8a 43 21 1b b6 ea f2 b2 7f 7c 18 d5 71 bc d8 3b + 20 ad d3 a0 8b 73 af 15 b9 a0 98 + `.replace(/\s+/g, '')); + + try { + const passphrase = 'password'; + const algo = 'aes128'; + + const literal = new openpgp.packet.Literal(0); + const key_enc = new openpgp.packet.SymEncryptedSessionKey(); + const enc = new openpgp.packet.SymEncryptedAEADProtected(); + const msg = new openpgp.packet.List(); + enc.aeadAlgorithm = key_enc.aeadAlgorithm = 'ocb'; + + msg.push(key_enc); + msg.push(enc); + + key_enc.sessionKeyAlgorithm = algo; + await key_enc.encrypt(passphrase); + + const key = key_enc.sessionKey; + + literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary); + literal.filename = ''; + enc.packets.push(literal); + await enc.encrypt(algo, key); + + const data = msg.write(); + expect(data).to.deep.equal(packetBytes); + + const msg2 = new openpgp.packet.List(); + msg2.read(data); + + await msg2[0].decrypt(passphrase); + const key2 = msg2[0].sessionKey; + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + + expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); + } finally { + openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; + openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal; + randomBytesStub.restore(); + } + }); + it('Secret key encryption/decryption test', async function() { const armored_msg = '-----BEGIN PGP MESSAGE-----\n' + From 997ec1c8dbd9c3e503764592977e0b18b23ac342 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 9 Apr 2018 18:51:38 +0200 Subject: [PATCH 16/51] Add AEAD feature flags --- src/enums.js | 15 +++++++++++++++ src/key.js | 9 +++++++-- test/general/key.js | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/enums.js b/src/enums.js index 62297ca1..12df0646 100644 --- a/src/enums.js +++ b/src/enums.js @@ -419,6 +419,21 @@ export default { signature: 6 }, + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.2.3.25|RFC4880bis-04, section 5.2.3.25} + * @enum {Integer} + * @readonly + */ + features: { + /** 0x01 - Modification Detection (packets 18 and 19) */ + modification_detection: 1, + /** 0x02 - AEAD Encrypted Data Packet (packet 20) and version 5 + * Symmetric-Key Encrypted Session Key Packets (packet 3) */ + aead: 2, + /** 0x04 - Version 5 Public-Key Packet format and corresponding new + * fingerprint format */ + v5_keys: 4 + }, + /** Asserts validity and converts from string/integer to integer. */ write: function(type, e) { if (typeof e === 'number') { diff --git a/src/key.js b/src/key.js index c6784f0e..7e4dfe42 100644 --- a/src/key.js +++ b/src/key.js @@ -1278,8 +1278,13 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { signaturePacket.isPrimaryUserID = true; } if (config.integrity_protect) { - signaturePacket.features = []; - signaturePacket.features.push(1); // Modification Detection + signaturePacket.features = [0]; + signaturePacket.features[0] |= enums.features.modification_detection; + } + if (config.aead_protect === 'draft04') { + signaturePacket.features || (signaturePacket.features = [0]); + signaturePacket.features[0] |= enums.features.aead; + signaturePacket.features[0] |= enums.features.v5_keys; } if (options.keyExpirationTime > 0) { signaturePacket.keyExpirationTime = options.keyExpirationTime; diff --git a/test/general/key.js b/test/general/key.js index eb396e38..ec7a6e9b 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1228,7 +1228,7 @@ p92yZgB3r2+f6/GIe2+7 expect(key.users[0].selfCertifications[0].preferredHashAlgorithms).to.eql([hash.sha256, hash.sha512, hash.sha1]); const compr = openpgp.enums.compression; expect(key.users[0].selfCertifications[0].preferredCompressionAlgorithms).to.eql([compr.zlib, compr.zip]); - expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.integrity_protect ? [1] : null); // modification detection + expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.aead_protect === 'draft04' ? [7] : [1]); }; const opt = {numBits: 512, userIds: 'test ', passphrase: 'hello'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys From 53d6f20b720a928c0bd5da2b0529c5a43a2c81f0 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 10 Apr 2018 11:54:44 +0200 Subject: [PATCH 17/51] Reduce allocations in OCB mode --- src/crypto/ocb.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index c0467772..590192c3 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -76,12 +76,15 @@ function double(S) { } +const zeros_16 = zeros(16); +const one = new Uint8Array([1]); + function constructKeyVariables(cipher, key, text, adata) { const aes = new ciphers[cipher](key); const encipher = aes.encrypt.bind(aes); const decipher = aes.decrypt.bind(aes); - const L_x = encipher(zeros(16)); + const L_x = encipher(zeros_16); const L_$ = double(L_x); const L = []; L[0] = double(L_$); @@ -100,7 +103,7 @@ function constructKeyVariables(cipher, key, text, adata) { function hash(kv, key, adata) { if (!adata.length) { // Fast path - return zeros(16); + return zeros_16; } const { encipher, L } = kv; @@ -161,7 +164,7 @@ async function encrypt(cipher, plaintext, key, nonce, adata) { // Nonce-dependent and per-encryption variables // // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros(15 - nonce.length), new Uint8Array([1]), nonce); + const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); const bottom = Nonce[15] & 0b111111; Nonce[15] &= 0b11000000; const Ktop = encipher(Nonce); @@ -179,7 +182,7 @@ async function encrypt(cipher, plaintext, key, nonce, adata) { let pos = 0; for (i = 0; i < m; i++) { set_xor(offset, L[ntz(i + 1)]); - C.set(xor(offset, encipher(xor(offset, plaintext))), pos); + C.set(set_xor(encipher(xor(offset, plaintext)), offset), pos); set_xor(checksum, plaintext); plaintext = plaintext.subarray(16); @@ -201,7 +204,7 @@ async function encrypt(cipher, plaintext, key, nonce, adata) { set_xor(checksum, xorInput); pos += plaintext.length; } - const Tag = xor(encipher(xor(xor(checksum, offset), L.$)), hash(kv, key, adata)); + const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(kv, key, adata)); // // Assemble ciphertext @@ -238,7 +241,7 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) { // Nonce-dependent and per-encryption variables // // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros(15 - nonce.length), new Uint8Array([1]), nonce); + const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); const bottom = Nonce[15] & 0b111111; Nonce[15] &= 0b11000000; const Ktop = encipher(Nonce); @@ -256,7 +259,7 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) { let pos = 0; for (i = 0; i < m; i++) { set_xor(offset, L[ntz(i + 1)]); - P.set(xor(offset, decipher(xor(offset, ciphertext))), pos); + P.set(set_xor(decipher(xor(offset, ciphertext)), offset), pos); set_xor(checksum, P.subarray(pos)); ciphertext = ciphertext.subarray(16); @@ -278,7 +281,7 @@ async function decrypt(cipher, ciphertext, key, nonce, adata) { set_xor(checksum, xorInput); pos += ciphertext.length; } - const Tag = xor(encipher(xor(xor(checksum, offset), L.$)), hash(kv, key, adata)); + const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(kv, key, adata)); // // Check for validity and assemble plaintext From d5d4c972287c6aeed8938e51b4dd5c51952b52b2 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 10 Apr 2018 14:23:37 +0200 Subject: [PATCH 18/51] Fix config.use_native --- src/crypto/eax.js | 4 ++-- src/crypto/gcm.js | 8 ++++---- src/crypto/public_key/elliptic/curves.js | 4 ++-- src/crypto/public_key/elliptic/key.js | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/crypto/eax.js b/src/crypto/eax.js index 6a8dcad8..28d9cce5 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -137,9 +137,9 @@ function concat(...arrays) { } 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 + if (util.getWebCryptoAll() && 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 + } else if (util.getNodeCrypto()) { // Node crypto library return nodeCtr(plaintext, key, iv); } // asm.js fallback return Promise.resolve(AES_CTR.encrypt(plaintext, key, iv)); diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index 6f357fb6..7d5f4a91 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -49,9 +49,9 @@ function encrypt(cipher, plaintext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - if (webCrypto && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + 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 (nodeCrypto) { // Node crypto library + } else if (util.getNodeCrypto()) { // Node crypto library return nodeEncrypt(plaintext, key, iv); } // asm.js fallback return Promise.resolve(AES_GCM.encrypt(plaintext, key, iv)); @@ -70,9 +70,9 @@ function decrypt(cipher, ciphertext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - if (webCrypto && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + 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 (nodeCrypto) { // Node crypto library + } else if (util.getNodeCrypto()) { // Node crypto library return nodeDecrypt(ciphertext, key, iv); } // asm.js fallback return Promise.resolve(AES_GCM.decrypt(ciphertext, key, iv)); diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index 46290620..f19f6662 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -182,14 +182,14 @@ Curve.prototype.keyFromPublic = function (pub) { Curve.prototype.genKeyPair = async function () { let keyPair; - if (webCrypto && this.web) { + if (this.web && util.getWebCrypto()) { // If browser doesn't support a curve, we'll catch it try { keyPair = await webGenKeyPair(this.name); } catch (err) { util.print_debug("Browser did not support signing: " + err.message); } - } else if (nodeCrypto && this.node) { + } else if (this.node && util.getNodeCrypto()) { keyPair = await nodeGenKeyPair(this.name); } diff --git a/src/crypto/public_key/elliptic/key.js b/src/crypto/public_key/elliptic/key.js index 5bffe80b..f74e0a9b 100644 --- a/src/crypto/public_key/elliptic/key.js +++ b/src/crypto/public_key/elliptic/key.js @@ -45,7 +45,7 @@ function KeyPair(curve, options) { } KeyPair.prototype.sign = async function (message, hash_algo) { - if (webCrypto && this.curve.web) { + if (this.curve.web && util.getWebCrypto()) { // If browser doesn't support a curve, we'll catch it try { // need to await to make sure browser succeeds @@ -54,7 +54,7 @@ KeyPair.prototype.sign = async function (message, hash_algo) { } catch (err) { util.print_debug("Browser did not support signing: " + err.message); } - } else if (nodeCrypto && this.curve.node) { + } else if (this.curve.node && util.getNodeCrypto()) { return nodeSign(this.curve, hash_algo, message, this.keyPair); } const digest = (typeof hash_algo === 'undefined') ? message : hash.digest(hash_algo, message); @@ -62,7 +62,7 @@ KeyPair.prototype.sign = async function (message, hash_algo) { }; KeyPair.prototype.verify = async function (message, signature, hash_algo) { - if (webCrypto && this.curve.web) { + if (this.curve.web && util.getWebCrypto()) { // If browser doesn't support a curve, we'll catch it try { // need to await to make sure browser succeeds @@ -71,7 +71,7 @@ KeyPair.prototype.verify = async function (message, signature, hash_algo) { } catch (err) { util.print_debug("Browser did not support signing: " + err.message); } - } else if (nodeCrypto && this.curve.node) { + } else if (this.curve.node && util.getNodeCrypto()) { return nodeVerify(this.curve, hash_algo, signature, message, this.keyPair.getPublic()); } const digest = (typeof hash_algo === 'undefined') ? message : hash.digest(hash_algo, message); From 28dbbadcff32c3d178c7f7df815f40d7c9f5533a Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 10 Apr 2018 16:44:52 +0200 Subject: [PATCH 19/51] Add config.aead_protect_version option --- src/config/config.js | 9 +++++++ src/key.js | 4 +-- src/packet/public_key.js | 2 +- src/packet/sym_encrypted_aead_protected.js | 10 ++++---- src/packet/sym_encrypted_session_key.js | 2 +- test/general/key.js | 10 +++++--- test/general/openpgp.js | 9 +++++-- test/general/packet.js | 30 +++++++++++++++++----- 8 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/config/config.js b/src/config/config.js index e2cfef90..5446b291 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -51,6 +51,15 @@ export default { * @property {Boolean} aead_protect */ aead_protect: false, + /** + * Use Authenticated Encryption with Additional Data (AEAD) protection for symmetric encryption. + * 0 means we implement a variant of {@link https://tools.ietf.org/html/draft-ford-openpgp-format-00|this IETF draft}. + * 4 means we implement {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04|RFC4880bis-04}. + * Only has an effect when aead_protect is set to true. + * @memberof module:config + * @property {Integer} aead_protect_version + */ + aead_protect_version: 0, /** * Default Authenticated Encryption with Additional Data (AEAD) encryption mode * Only has an effect when aead_protect is set to true. diff --git a/src/key.js b/src/key.js index 7e4dfe42..30b94b4b 100644 --- a/src/key.js +++ b/src/key.js @@ -1261,7 +1261,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.cast5); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.tripledes); - if (config.aead_protect === 'draft04') { + if (config.aead_protect && config.aead_protect_version === 4) { signaturePacket.preferredAeadAlgorithms = []; signaturePacket.preferredAeadAlgorithms.push(enums.aead.eax); signaturePacket.preferredAeadAlgorithms.push(enums.aead.ocb); @@ -1281,7 +1281,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) { signaturePacket.features = [0]; signaturePacket.features[0] |= enums.features.modification_detection; } - if (config.aead_protect === 'draft04') { + if (config.aead_protect && config.aead_protect_version === 4) { signaturePacket.features || (signaturePacket.features = [0]); signaturePacket.features[0] |= enums.features.aead; signaturePacket.features[0] |= enums.features.v5_keys; diff --git a/src/packet/public_key.js b/src/packet/public_key.js index f76e16fa..491d3ddd 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -54,7 +54,7 @@ function PublicKey(date=new Date()) { * Packet version * @type {Integer} */ - this.version = config.aead_protect === 'draft04' ? 5 : 4; + this.version = config.aead_protect && config.aead_protect_version === 4 ? 5 : 4; /** * Key creation date. * @type {Date} diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 766a3bb8..e5268cc5 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -61,7 +61,7 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) { throw new Error('Invalid packet version.'); } offset++; - if (config.aead_protect === 'draft04') { + if (config.aead_protect_version === 4) { this.cipherAlgo = bytes[offset++]; this.aeadAlgo = bytes[offset++]; this.chunkSizeByte = bytes[offset++]; @@ -79,7 +79,7 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) { * @returns {Uint8Array} The encrypted payload */ SymEncryptedAEADProtected.prototype.write = function () { - if (config.aead_protect === 'draft04') { + if (config.aead_protect_version === 4) { 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]); @@ -94,7 +94,7 @@ SymEncryptedAEADProtected.prototype.write = function () { */ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) { const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; - if (config.aead_protect === 'draft04') { + if (config.aead_protect_version === 4) { 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); @@ -132,11 +132,11 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith * @async */ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { - this.aeadAlgo = config.aead_protect === 'draft04' ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.gcm; + 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)]; this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV let data = this.packets.write(); - if (config.aead_protect === 'draft04') { + if (config.aead_protect_version === 4) { 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)) diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index 9e4d6de0..275f6431 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -49,7 +49,7 @@ import util from '../util'; */ function SymEncryptedSessionKey() { this.tag = enums.packet.symEncryptedSessionKey; - this.version = config.aead_protect === 'draft04' ? 5 : 4; + this.version = config.aead_protect && config.aead_protect_version === 4 ? 5 : 4; this.sessionKey = null; this.sessionKeyEncryptionAlgorithm = null; this.sessionKeyAlgorithm = 'aes256'; diff --git a/test/general/key.js b/test/general/key.js index ec7a6e9b..1e51bc37 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -10,12 +10,16 @@ describe('Key', function() { describe('V5', function() { let aead_protectVal; + let aead_protect_versionVal; beforeEach(function() { aead_protectVal = openpgp.config.aead_protect; - openpgp.config.aead_protect = 'draft04'; + aead_protect_versionVal = openpgp.config.aead_protect_version; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; }); afterEach(function() { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; }); tests(); @@ -1220,7 +1224,7 @@ p92yZgB3r2+f6/GIe2+7 expect(key.subKeys[0].bindingSignatures[0].keyFlags[0] & keyFlags.encrypt_storage).to.equal(keyFlags.encrypt_storage); const sym = openpgp.enums.symmetric; expect(key.users[0].selfCertifications[0].preferredSymmetricAlgorithms).to.eql([sym.aes256, sym.aes128, sym.aes192, sym.cast5, sym.tripledes]); - if (openpgp.config.aead_protect === 'draft04') { + if (openpgp.config.aead_protect && openpgp.config.aead_protect_version === 4) { const aead = openpgp.enums.aead; expect(key.users[0].selfCertifications[0].preferredAeadAlgorithms).to.eql([aead.eax, aead.ocb]); } @@ -1228,7 +1232,7 @@ p92yZgB3r2+f6/GIe2+7 expect(key.users[0].selfCertifications[0].preferredHashAlgorithms).to.eql([hash.sha256, hash.sha512, hash.sha1]); const compr = openpgp.enums.compression; expect(key.users[0].selfCertifications[0].preferredCompressionAlgorithms).to.eql([compr.zlib, compr.zip]); - expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.aead_protect === 'draft04' ? [7] : [1]); + expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.aead_protect && openpgp.config.aead_protect_version === 4 ? [7] : [1]); }; const opt = {numBits: 512, userIds: 'test ', passphrase: 'hello'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys diff --git a/test/general/openpgp.js b/test/general/openpgp.js index e4f94bd2..6ad80330 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -597,6 +597,7 @@ describe('OpenPGP.js public api tests', function() { let zero_copyVal; let use_nativeVal; let aead_protectVal; + let aead_protect_versionVal; beforeEach(function(done) { publicKey = openpgp.key.readArmored(pub_key); @@ -620,6 +621,7 @@ describe('OpenPGP.js public api tests', function() { zero_copyVal = openpgp.config.zero_copy; use_nativeVal = openpgp.config.use_native; aead_protectVal = openpgp.config.aead_protect; + aead_protect_versionVal = openpgp.config.aead_protect_version; done(); }); @@ -627,6 +629,7 @@ describe('OpenPGP.js public api tests', function() { openpgp.config.zero_copy = zero_copyVal; openpgp.config.use_native = use_nativeVal; openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; }); it('Decrypting key with wrong passphrase rejected', async function () { @@ -671,7 +674,8 @@ describe('OpenPGP.js public api tests', function() { if: true, beforeEach: function() { openpgp.config.use_native = false; - openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; } }); @@ -679,7 +683,8 @@ describe('OpenPGP.js public api tests', function() { if: openpgp.util.getWebCryptoAll() || openpgp.util.getNodeCrypto(), beforeEach: function() { openpgp.config.use_native = true; - openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; } }); diff --git a/test/general/packet.js b/test/general/packet.js index 4f951e8c..32ba3b25 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -144,7 +144,9 @@ describe("Packet", function() { it('Sym. encrypted AEAD protected packet (draft04)', function() { let aead_protectVal = openpgp.config.aead_protect; - openpgp.config.aead_protect = 'draft04'; + let aead_protect_versionVal = openpgp.config.aead_protect_version; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; 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'; @@ -166,6 +168,7 @@ describe("Packet", function() { expect(msg2[0].packets[0].data).to.deep.equal(literal.data); }).finally(function() { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; }); }); @@ -181,8 +184,10 @@ describe("Packet", function() { `.replace(/\s+/g, '')); let aead_protectVal = openpgp.config.aead_protect; + let aead_protect_versionVal = openpgp.config.aead_protect_version; let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; - openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; 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, '')); @@ -212,6 +217,7 @@ describe("Packet", function() { expect(msg2[0].packets[0].data).to.deep.equal(literal.data); }).finally(function() { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; randomBytesStub.restore(); }); @@ -417,7 +423,9 @@ describe("Packet", function() { it('Sym. encrypted session key reading/writing (draft04)', async function() { let aead_protectVal = openpgp.config.aead_protect; - openpgp.config.aead_protect = 'draft04'; + let aead_protect_versionVal = openpgp.config.aead_protect_version; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; try { const passphrase = 'hello'; @@ -450,6 +458,7 @@ describe("Packet", function() { expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); } finally { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; } }); @@ -457,9 +466,11 @@ describe("Packet", function() { // From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-eax-encryption-and-decryption let aead_protectVal = openpgp.config.aead_protect; + let aead_protect_versionVal = openpgp.config.aead_protect_version; let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte; - openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; openpgp.config.aead_chunk_size_byte = 14; openpgp.config.s2k_iteration_count_byte = 0x90; @@ -522,6 +533,7 @@ describe("Packet", function() { expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); } finally { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal; randomBytesStub.restore(); @@ -532,9 +544,11 @@ describe("Packet", function() { // From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-ocb-encryption-and-decryption let aead_protectVal = openpgp.config.aead_protect; + let aead_protect_versionVal = openpgp.config.aead_protect_version; let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte; - openpgp.config.aead_protect = 'draft04'; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; openpgp.config.aead_chunk_size_byte = 14; openpgp.config.s2k_iteration_count_byte = 0x90; @@ -598,6 +612,7 @@ describe("Packet", function() { expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); } finally { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal; randomBytesStub.restore(); @@ -715,7 +730,9 @@ describe("Packet", function() { it('Writing and encryption of a secret key packet. (draft04)', function() { let aead_protectVal = openpgp.config.aead_protect; - openpgp.config.aead_protect = 'draft04'; + let aead_protect_versionVal = openpgp.config.aead_protect_version; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; const key = new openpgp.packet.List(); key.push(new openpgp.packet.SecretKey()); @@ -742,6 +759,7 @@ describe("Packet", function() { expect(key[0].params.toString()).to.equal(key2[0].params.toString()); }).finally(function() { openpgp.config.aead_protect = aead_protectVal; + openpgp.config.aead_protect_version = aead_protect_versionVal; }); }); From e44fbbccabecbeed93dc618c1d98feb7d528100b Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 10 Apr 2018 17:55:12 +0200 Subject: [PATCH 20/51] Add more OCB tests --- test/crypto/ocb.js | 29 +++++++++++++++++++++++++++++ test/general/openpgp.js | 12 ++++++++++++ 2 files changed, 41 insertions(+) diff --git a/test/crypto/ocb.js b/test/crypto/ocb.js index a3358913..04de3065 100644 --- a/test/crypto/ocb.js +++ b/test/crypto/ocb.js @@ -149,4 +149,33 @@ describe('Symmetric AES-OCB', function() { expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); } }); + + it('Different key size test vectors', async function() { + const TAGLEN = 128; + const outputs = { + 128: '67E944D23256C5E0B6C61FA22FDF1EA2', + 192: 'F673F2C3E7174AAE7BAE986CA9F29E17', + 256: 'D90EB8E9C977C88B79DD793D7FFA161C' + }; + + for (const KEYLEN of [128, 192, 256]) { + const K = new Uint8Array(KEYLEN / 8); + K[K.length - 1] = TAGLEN; + + const C = []; + let N; + for (let i = 0; i < 128; i++) { + const S = new Uint8Array(i); + N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 1, 4)]); + C.push(await ocb.encrypt('aes' + KEYLEN, S, K, N, S)); + N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 2, 4)]); + C.push(await ocb.encrypt('aes' + KEYLEN, S, K, N, new Uint8Array())); + N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 3, 4)]); + C.push(await ocb.encrypt('aes' + KEYLEN, new Uint8Array(), K, N, S)); + } + N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(385, 4)]); + const output = await ocb.encrypt('aes' + KEYLEN, new Uint8Array(), K, N, openpgp.util.concatUint8Array(C)); + expect(openpgp.util.Uint8Array_to_hex(output)).to.equal(outputs[KEYLEN].toLowerCase()); + } + }); }); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 6ad80330..a6898a23 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -598,6 +598,7 @@ describe('OpenPGP.js public api tests', function() { let use_nativeVal; let aead_protectVal; let aead_protect_versionVal; + let aead_modeVal; beforeEach(function(done) { publicKey = openpgp.key.readArmored(pub_key); @@ -622,6 +623,7 @@ describe('OpenPGP.js public api tests', function() { use_nativeVal = openpgp.config.use_native; aead_protectVal = openpgp.config.aead_protect; aead_protect_versionVal = openpgp.config.aead_protect_version; + aead_modeVal = openpgp.config.aead_mode; done(); }); @@ -630,6 +632,7 @@ describe('OpenPGP.js public api tests', function() { openpgp.config.use_native = use_nativeVal; openpgp.config.aead_protect = aead_protectVal; openpgp.config.aead_protect_version = aead_protect_versionVal; + openpgp.config.aead_mode = aead_modeVal; }); it('Decrypting key with wrong passphrase rejected', async function () { @@ -688,6 +691,15 @@ describe('OpenPGP.js public api tests', function() { } }); + tryTests('OCB mode', tests, { + if: true, + beforeEach: function() { + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; + openpgp.config.aead_mode = openpgp.enums.aead.ocb; + } + }); + function tests() { it('Configuration', function() { openpgp.config.show_version = false; From e9a360019c3e8f4c0ae3d48ff0159c51a2eff60f Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 11 Apr 2018 15:13:31 +0200 Subject: [PATCH 21/51] Update table of supported native ECC curves --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f3f673a9..a518d99c 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,11 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch | p384 | ECDH | ECDSA | Yes | Yes* | Yes* | | p521 | ECDH | ECDSA | Yes | Yes* | Yes* | | secp256k1 | ECDH | ECDSA | Yes | Yes* | No | - | curve25519 | ECDH | N/A | Yes | No (TODO) | No | - | ed25519 | N/A | EdDSA | Yes | No (TODO) | No | - | brainpoolP256r1 | ECDH | ECDSA | Yes | No (TODO) | No | - | brainpoolP384r1 | ECDH | ECDSA | Yes | No (TODO) | No | - | brainpoolP512r1 | ECDH | ECDSA | Yes | No (TODO) | No | + | brainpoolP256r1 | ECDH | ECDSA | Yes | Yes* | No | + | brainpoolP384r1 | ECDH | ECDSA | Yes | Yes* | No | + | brainpoolP512r1 | ECDH | ECDSA | Yes | Yes* | No | + | curve25519 | ECDH | N/A | Yes | No | No | + | ed25519 | N/A | EdDSA | Yes | No | No | * Version 2.x of the library has been built from the ground up with Uint8Arrays. This allows for much better performance and memory usage than strings. From e24b46192dedbe60a29ac8cb886049294d37d303 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 11 Apr 2018 16:37:40 +0200 Subject: [PATCH 22/51] 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} * @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, From 2f849063f9db055f8a8030f843f4b00a37302396 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 12 Apr 2018 15:01:37 +0200 Subject: [PATCH 23/51] Allow reusing EAX/OCB instances with the same key This is useful for chunked encryption in draft04 --- src/crypto/eax.js | 168 ++++---- src/crypto/ocb.js | 444 +++++++++++---------- src/packet/secret_key.js | 4 +- src/packet/sym_encrypted_aead_protected.js | 10 +- src/packet/sym_encrypted_session_key.js | 4 +- test/crypto/eax.js | 20 +- test/crypto/ocb.js | 30 +- 7 files changed, 352 insertions(+), 328 deletions(-) diff --git a/src/crypto/eax.js b/src/crypto/eax.js index 28d9cce5..c8c81f8e 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -47,79 +47,113 @@ class OMAC extends CMAC { } } - -/** - * 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'); +class CTR { + constructor(key) { + if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + this.key = webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']); + this.ctr = this.webCtr; + } else if (util.getNodeCrypto()) { // Node crypto library + this.key = new Buffer(key); + this.ctr = this.nodeCtr; + } else { + // asm.js fallback + this.key = key; + } } - const omac = new OMAC(key); - const _nonce = omac.mac(zero, nonce); - const _adata = omac.mac(one, adata); - const ciphered = await CTR(plaintext, key, _nonce); - const _ciphered = omac.mac(two, ciphered); - const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac.mac(*).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'); + webCtr(pt, iv) { + return this.key + .then(keyObj => webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, keyObj, pt)) + .then(ct => new Uint8Array(ct)); } - 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 omac = new OMAC(key); - const _nonce = omac.mac(zero, nonce); - const _adata = omac.mac(one, adata); - const _ciphered = omac.mac(two, ciphered); - const _tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac.mac(*).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; + nodeCtr(pt, iv) { + pt = new Buffer(pt); + iv = new Buffer(iv); + const en = new nodeCrypto.createCipheriv('aes-' + (this.key.length * 8) + '-ctr', this.key, iv); + const ct = Buffer.concat([en.update(pt), en.final()]); + return Promise.resolve(new Uint8Array(ct)); + } + + ctr(pt, iv) { + return Promise.resolve(AES_CTR.encrypt(pt, this.key, iv)); + } } + +class EAX { + /** + * Class to en/decrypt using EAX mode. + * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' + * @param {Uint8Array} key The encryption key + */ + constructor(cipher, key) { + if (cipher.substr(0, 3) !== 'aes') { + throw new Error('EAX mode supports only AES cipher'); + } + + const omac = new OMAC(key); + this.omac = omac.mac.bind(omac); + const ctr = new CTR(key); + this.ctr = ctr.ctr.bind(ctr); + } + + /** + * Encrypt plaintext input. + * @param {Uint8Array} plaintext The cleartext input to be encrypted + * @param {Uint8Array} nonce The nonce (16 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output + */ + async encrypt(plaintext, nonce, adata) { + const _nonce = this.omac(zero, nonce); + const _adata = this.omac(one, adata); + const ciphered = await this.ctr(plaintext, _nonce); + const _ciphered = this.omac(two, ciphered); + const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac(*).length === tagLength. + return concat(ciphered, tag); + } + + /** + * Decrypt ciphertext input. + * @param {Uint8Array} ciphertext The ciphertext input to be decrypted + * @param {Uint8Array} nonce The nonce (16 bytes) + * @param {Uint8Array} adata Associated data to verify + * @returns {Promise} The plaintext output + */ + async decrypt(ciphertext, nonce, adata) { + 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 = this.omac(zero, nonce); + const _adata = this.omac(one, adata); + const _ciphered = this.omac(two, ciphered); + 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 this.ctr(ciphered, _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) { +EAX.getNonce = function(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 }; +EAX.blockLength = blockLength; +EAX.ivLength = ivLength; + +export default EAX; + ////////////////////////// // // @@ -135,27 +169,3 @@ function xor3(a, b, c) { function concat(...arrays) { return util.concatUint8Array(arrays); } - -function CTR(plaintext, key, iv) { - if (util.getWebCryptoAll() && 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 (util.getNodeCrypto()) { // 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/ocb.js b/src/crypto/ocb.js index 590192c3..5bfe4aab 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -79,237 +79,247 @@ function double(S) { const zeros_16 = zeros(16); const one = new Uint8Array([1]); -function constructKeyVariables(cipher, key, text, adata) { - const aes = new ciphers[cipher](key); - const encipher = aes.encrypt.bind(aes); - const decipher = aes.decrypt.bind(aes); - - const L_x = encipher(zeros_16); - const L_$ = double(L_x); - const L = []; - L[0] = double(L_$); - - const max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; - for (let i = 1; i <= max_ntz; i++) { - L[i] = double(L[i - 1]); +class OCB { + /** + * Class to en/decrypt using OCB mode. + * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' + * @param {Uint8Array} key The encryption key + */ + constructor(cipher, key) { + this.max_ntz = 0; + this.constructKeyVariables(cipher, key); } - L.x = L_x; - L.$ = L_$; + constructKeyVariables(cipher, key) { + const aes = new ciphers[cipher](key); + const encipher = aes.encrypt.bind(aes); + const decipher = aes.decrypt.bind(aes); - return { encipher, decipher, L }; + const L_x = encipher(zeros_16); + const L_$ = double(L_x); + const L = []; + L[0] = double(L_$); + + + L.x = L_x; + L.$ = L_$; + + this.kv = { encipher, decipher, L }; + } + + extendKeyVariables(text, adata) { + const { L } = this.kv; + const max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; + for (let i = this.max_ntz + 1; i <= max_ntz; i++) { + L[i] = double(L[i - 1]); + } + this.max_ntz = max_ntz; + } + + hash(adata) { + if (!adata.length) { + // Fast path + return zeros_16; + } + + const { encipher, L } = this.kv; + + // + // Consider A as a sequence of 128-bit blocks + // + const m = adata.length >> 4; + + const offset = zeros(16); + const sum = zeros(16); + for (let i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + set_xor(sum, encipher(xor(offset, adata))); + adata = adata.subarray(16); + } + + // + // Process any final partial block; compute final hash value + // + if (adata.length) { + set_xor(offset, L.x); + + const cipherInput = zeros(16); + cipherInput.set(adata, 0); + cipherInput[adata.length] = 0b10000000; + set_xor(cipherInput, offset); + + set_xor(sum, encipher(cipherInput)); + } + + return sum; + } + + + /** + * Encrypt plaintext input. + * @param {Uint8Array} plaintext The cleartext input to be encrypted + * @param {Uint8Array} nonce The nonce (15 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output + */ + async encrypt(plaintext, nonce, adata) { + // + // Consider P as a sequence of 128-bit blocks + // + const m = plaintext.length >> 4; + + // + // Key-dependent variables + // + this.extendKeyVariables(plaintext, adata); + const { encipher, L } = this.kv; + + // + // Nonce-dependent and per-encryption variables + // + // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). + const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); + const bottom = Nonce[15] & 0b111111; + Nonce[15] &= 0b11000000; + const Ktop = encipher(Nonce); + const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = zeros(16); + + const C = new Uint8Array(plaintext.length + tagLength); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + C.set(set_xor(encipher(xor(offset, plaintext)), offset), pos); + set_xor(checksum, plaintext); + + plaintext = plaintext.subarray(16); + pos += 16; + } + + // + // Process any final partial block and compute raw tag + // + if (plaintext.length) { + set_xor(offset, L.x); + const Pad = encipher(offset); + C.set(xor(plaintext, Pad), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) + const xorInput = zeros(16); + xorInput.set(plaintext, 0); + xorInput[plaintext.length] = 0b10000000; + set_xor(checksum, xorInput); + pos += plaintext.length; + } + const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), this.hash(adata)); + + // + // Assemble ciphertext + // + C.set(Tag, pos); + return C; + } + + + /** + * Decrypt ciphertext input. + * @param {Uint8Array} ciphertext The ciphertext input to be decrypted + * @param {Uint8Array} nonce The nonce (15 bytes) + * @param {Uint8Array} adata Associated data to verify + * @returns {Promise} The plaintext output + */ + async decrypt(ciphertext, nonce, adata) { + // + // Consider C as a sequence of 128-bit blocks + // + const T = ciphertext.subarray(ciphertext.length - tagLength); + ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); + const m = ciphertext.length >> 4; + + // + // Key-dependent variables + // + this.extendKeyVariables(ciphertext, adata); + const { encipher, decipher, L } = this.kv; + + // + // Nonce-dependent and per-encryption variables + // + // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). + const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); + const bottom = Nonce[15] & 0b111111; + Nonce[15] &= 0b11000000; + const Ktop = encipher(Nonce); + const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = zeros(16); + + const P = new Uint8Array(ciphertext.length); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + P.set(set_xor(decipher(xor(offset, ciphertext)), offset), pos); + set_xor(checksum, P.subarray(pos)); + + ciphertext = ciphertext.subarray(16); + pos += 16; + } + + // + // Process any final partial block and compute raw tag + // + if (ciphertext.length) { + set_xor(offset, L.x); + const Pad = encipher(offset); + P.set(xor(ciphertext, Pad), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) + const xorInput = zeros(16); + xorInput.set(P.subarray(pos), 0); + xorInput[ciphertext.length] = 0b10000000; + set_xor(checksum, xorInput); + pos += ciphertext.length; + } + const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), this.hash(adata)); + + // + // Check for validity and assemble plaintext + // + if (!util.equalsUint8Array(Tag, T)) { + throw new Error('Authentication tag mismatch in OCB ciphertext'); + } + return P; + } } -function hash(kv, key, adata) { - if (!adata.length) { - // Fast path - return zeros_16; - } - - const { encipher, L } = kv; - - // - // Consider A as a sequence of 128-bit blocks - // - const m = adata.length >> 4; - - const offset = zeros(16); - const sum = zeros(16); - for (let i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - set_xor(sum, encipher(xor(offset, adata))); - adata = adata.subarray(16); - } - - // - // Process any final partial block; compute final hash value - // - if (adata.length) { - set_xor(offset, L.x); - - const cipherInput = zeros(16); - cipherInput.set(adata, 0); - cipherInput[adata.length] = 0b10000000; - set_xor(cipherInput, offset); - - set_xor(sum, encipher(cipherInput)); - } - - return sum; -} - - -/** - * 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 (15 bytes) - * @param {Uint8Array} adata Associated data to sign - * @returns {Promise} The ciphertext output - */ -async function encrypt(cipher, plaintext, key, nonce, adata) { - // - // Consider P as a sequence of 128-bit blocks - // - const m = plaintext.length >> 4; - - // - // Key-dependent variables - // - const kv = constructKeyVariables(cipher, key, plaintext, adata); - const { encipher, L } = kv; - - // - // Nonce-dependent and per-encryption variables - // - // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); - const bottom = Nonce[15] & 0b111111; - Nonce[15] &= 0b11000000; - const Ktop = encipher(Nonce); - const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); - // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = zeros(16); - - const C = new Uint8Array(plaintext.length + tagLength); - - // - // Process any whole blocks - // - let i; - let pos = 0; - for (i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - C.set(set_xor(encipher(xor(offset, plaintext)), offset), pos); - set_xor(checksum, plaintext); - - plaintext = plaintext.subarray(16); - pos += 16; - } - - // - // Process any final partial block and compute raw tag - // - if (plaintext.length) { - set_xor(offset, L.x); - const Pad = encipher(offset); - C.set(xor(plaintext, Pad), pos); - - // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) - const xorInput = zeros(16); - xorInput.set(plaintext, 0); - xorInput[plaintext.length] = 0b10000000; - set_xor(checksum, xorInput); - pos += plaintext.length; - } - const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(kv, key, adata)); - - // - // Assemble ciphertext - // - C.set(Tag, pos); - return C; -} - - -/** - * 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 (15 bytes) - * @param {Uint8Array} adata Associated data to verify - * @returns {Promise} The plaintext output - */ -async function decrypt(cipher, ciphertext, key, nonce, adata) { - // - // Consider C as a sequence of 128-bit blocks - // - const T = ciphertext.subarray(ciphertext.length - tagLength); - ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); - const m = ciphertext.length >> 4; - - // - // Key-dependent variables - // - const kv = constructKeyVariables(cipher, key, ciphertext, adata); - const { encipher, decipher, L } = kv; - - // - // Nonce-dependent and per-encryption variables - // - // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); - const bottom = Nonce[15] & 0b111111; - Nonce[15] &= 0b11000000; - const Ktop = encipher(Nonce); - const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); - // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = zeros(16); - - const P = new Uint8Array(ciphertext.length); - - // - // Process any whole blocks - // - let i; - let pos = 0; - for (i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - P.set(set_xor(decipher(xor(offset, ciphertext)), offset), pos); - set_xor(checksum, P.subarray(pos)); - - ciphertext = ciphertext.subarray(16); - pos += 16; - } - - // - // Process any final partial block and compute raw tag - // - if (ciphertext.length) { - set_xor(offset, L.x); - const Pad = encipher(offset); - P.set(xor(ciphertext, Pad), pos); - - // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) - const xorInput = zeros(16); - xorInput.set(P.subarray(pos), 0); - xorInput[ciphertext.length] = 0b10000000; - set_xor(checksum, xorInput); - pos += ciphertext.length; - } - const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(kv, key, adata)); - - // - // Check for validity and assemble plaintext - // - if (!util.equalsUint8Array(Tag, T)) { - throw new Error('Authentication tag mismatch in OCB ciphertext'); - } - return P; -} /** * Get OCB nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2|RFC4880bis-04, section 5.16.2}. * @param {Uint8Array} iv The initialization vector (15 bytes) * @param {Uint8Array} chunkIndex The chunk index (8 bytes) */ -function getNonce(iv, chunkIndex) { +OCB.getNonce = function(iv, chunkIndex) { const nonce = iv.slice(); for (let i = 0; i < chunkIndex.length; i++) { nonce[7 + i] ^= chunkIndex[i]; } return nonce; -} - - -export default { - blockLength, - ivLength, - encrypt, - decrypt, - getNonce }; + +OCB.blockLength = blockLength; +OCB.ivLength = ivLength; + +export default OCB; diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index f764ff80..59105443 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -211,7 +211,7 @@ SecretKey.prototype.encrypt = async function (passphrase) { arr = [new Uint8Array([253, optionalFields.length])]; arr.push(optionalFields); const mode = crypto[aead]; - const encrypted = await mode.encrypt(symmetric, cleartext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); + const encrypted = await new mode(symmetric, key).encrypt(cleartext, iv.subarray(0, mode.ivLength), new Uint8Array()); arr.push(util.writeNumber(encrypted.length, 4)); arr.push(encrypted); } else { @@ -305,7 +305,7 @@ SecretKey.prototype.decrypt = async function (passphrase) { if (aead) { const mode = crypto[aead]; try { - cleartext = await mode.decrypt(symmetric, ciphertext, key, iv.subarray(0, mode.ivLength), new Uint8Array()); + cleartext = await new mode(symmetric, key).decrypt(ciphertext, iv.subarray(0, mode.ivLength), new Uint8Array()); } catch(err) { if (err.message.startsWith('Authentication tag mismatch')) { throw new Error('Incorrect key passphrase: ' + err.message); diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index e5268cc5..fcd4c80d 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -107,15 +107,16 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith 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 = []; + const modeInstance = new mode(cipher, key); for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { decryptedPromises.push( - mode.decrypt(cipher, data.subarray(0, chunkSize), key, mode.getNonce(this.iv, chunkIndexArray), adataArray) + modeInstance.decrypt(data.subarray(0, chunkSize), 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) + modeInstance.decrypt(authTag, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) ); this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises))); } else { @@ -148,9 +149,10 @@ 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 = new mode(sessionKeyAlgorithm, key); for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { encryptedPromises.push( - mode.encrypt(sessionKeyAlgorithm, data.subarray(0, chunkSize), key, mode.getNonce(this.iv, chunkIndexArray), adataArray) + modeInstance.encrypt(data.subarray(0, chunkSize), 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 @@ -159,7 +161,7 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) } encryptedPromises.push( - mode.encrypt(sessionKeyAlgorithm, data, key, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) + modeInstance.encrypt(data, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) ); this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises)); } else { diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index 275f6431..39687f40 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -142,7 +142,7 @@ SymEncryptedSessionKey.prototype.decrypt = async function(passphrase) { if (this.version === 5) { const mode = crypto[this.aeadAlgorithm]; const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); - this.sessionKey = await mode.decrypt(algo, this.encrypted, key, this.iv, adata); + this.sessionKey = await new mode(algo, key).decrypt(this.encrypted, this.iv, adata); } else if (this.encrypted !== null) { const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null); @@ -182,7 +182,7 @@ SymEncryptedSessionKey.prototype.encrypt = async function(passphrase) { const mode = crypto[this.aeadAlgorithm]; this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); - this.encrypted = await mode.encrypt(algo, this.sessionKey, key, this.iv, adata); + this.encrypted = await new mode(algo, key).encrypt(this.sessionKey, this.iv, adata); } else { const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); diff --git a/test/crypto/eax.js b/test/crypto/eax.js index 369dcfd4..c5aec8b9 100644 --- a/test/crypto/eax.js +++ b/test/crypto/eax.js @@ -9,8 +9,6 @@ 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 = [ @@ -96,28 +94,30 @@ function testAESEAX() { headerBytes = openpgp.util.hex_to_Uint8Array(vec.header), ctBytes = openpgp.util.hex_to_Uint8Array(vec.ct); + const eax = new openpgp.crypto.eax(cipher, keyBytes); + // encryption test - let ct = await eax.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + let ct = await eax.encrypt(msgBytes, 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); + let pt = await eax.decrypt(ctBytes, 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 = await eax.encrypt(msgBytes, nonceBytes, headerBytes); ct[2] ^= 8; - pt = eax.decrypt(cipher, ct, keyBytes, nonceBytes, headerBytes); + pt = eax.decrypt(ct, 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()); + ct = await eax.encrypt(msgBytes, nonceBytes, new Uint8Array()); + pt = await eax.decrypt(ct, 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])); + ct = await eax.encrypt(msgBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + pt = await eax.decrypt(ct, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase()); } }); diff --git a/test/crypto/ocb.js b/test/crypto/ocb.js index 04de3065..a1e269cb 100644 --- a/test/crypto/ocb.js +++ b/test/crypto/ocb.js @@ -9,8 +9,6 @@ chai.use(require('chai-as-promised')); const expect = chai.expect; -const ocb = openpgp.crypto.ocb; - describe('Symmetric AES-OCB', function() { it('Passes all test vectors', async function() { const K = '000102030405060708090A0B0C0D0E0F'; @@ -124,28 +122,30 @@ describe('Symmetric AES-OCB', function() { headerBytes = openpgp.util.hex_to_Uint8Array(vec.A), ctBytes = openpgp.util.hex_to_Uint8Array(vec.C); + const ocb = new openpgp.crypto.ocb(cipher, keyBytes); + // encryption test - let ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + let ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes); expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.C.toLowerCase()); // decryption test with verification - let pt = await ocb.decrypt(cipher, ctBytes, keyBytes, nonceBytes, headerBytes); + let pt = await ocb.decrypt(ctBytes, nonceBytes, headerBytes); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); // tampering detection test - ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, headerBytes); + ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes); ct[2] ^= 8; - pt = ocb.decrypt(cipher, ct, keyBytes, nonceBytes, headerBytes); + pt = ocb.decrypt(ct, nonceBytes, headerBytes); await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in OCB ciphertext') // testing without additional data - ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, new Uint8Array()); - pt = await ocb.decrypt(cipher, ct, keyBytes, nonceBytes, new Uint8Array()); + ct = await ocb.encrypt(msgBytes, nonceBytes, new Uint8Array()); + pt = await ocb.decrypt(ct, nonceBytes, new Uint8Array()); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); // testing with multiple additional data - ct = await ocb.encrypt(cipher, msgBytes, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); - pt = await ocb.decrypt(cipher, ct, keyBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + ct = await ocb.encrypt(msgBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); + pt = await ocb.decrypt(ct, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes])); expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase()); } }); @@ -162,19 +162,21 @@ describe('Symmetric AES-OCB', function() { const K = new Uint8Array(KEYLEN / 8); K[K.length - 1] = TAGLEN; + const ocb = new openpgp.crypto.ocb('aes' + KEYLEN, K); + const C = []; let N; for (let i = 0; i < 128; i++) { const S = new Uint8Array(i); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 1, 4)]); - C.push(await ocb.encrypt('aes' + KEYLEN, S, K, N, S)); + C.push(await ocb.encrypt(S, N, S)); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 2, 4)]); - C.push(await ocb.encrypt('aes' + KEYLEN, S, K, N, new Uint8Array())); + C.push(await ocb.encrypt(S, N, new Uint8Array())); N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 3, 4)]); - C.push(await ocb.encrypt('aes' + KEYLEN, new Uint8Array(), K, N, S)); + C.push(await ocb.encrypt(new Uint8Array(), N, S)); } N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(385, 4)]); - const output = await ocb.encrypt('aes' + KEYLEN, new Uint8Array(), K, N, openpgp.util.concatUint8Array(C)); + const output = await ocb.encrypt(new Uint8Array(), N, openpgp.util.concatUint8Array(C)); expect(openpgp.util.Uint8Array_to_hex(output)).to.equal(outputs[KEYLEN].toLowerCase()); } }); From 3b81088aafddce726ca3e2ab3781d31c13c71bea Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 12 Apr 2018 16:36:07 +0200 Subject: [PATCH 24/51] Decouple signature type from data packet type Instead of creating a text signature for text packets and a binary signature for binary packets, we determine the signature type based on whether a String or Uint8Array was originally passed. This is useful for the new MIME data packet type (implemented in the next commit) which you can pass in either format. This also partly reverts a22c9e4. Instead of canonicalizing the literal data packet, we canonicalize the data when signing. This fixes a hypothetical case where an uncanonicalized text packet has both a text and a binary signature. This also partly reverts c28f7ad. GPG does not strip trailing whitespace when creating text signatures of literal data packets. --- src/message.js | 18 ++++++------------ src/packet/literal.js | 29 +++++++++++++++++++++-------- src/packet/signature.js | 8 +++++++- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/message.js b/src/message.js index aca0de85..c2feb5f5 100644 --- a/src/message.js +++ b/src/message.js @@ -216,7 +216,7 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) { */ Message.prototype.getLiteralData = function() { const literal = this.packets.findPacket(enums.packet.literal); - return (literal && literal.data) || null; + return (literal && literal.getBytes()) || null; }; /** @@ -395,8 +395,8 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new let i; let existingSigPacketlist; - const literalFormat = enums.write(enums.literal, literalDataPacket.format); - const signatureType = literalFormat === enums.literal.binary ? + // If data packet was created from Uint8Array, use binary, otherwise use text + const signatureType = literalDataPacket.text === null ? enums.signature.binary : enums.signature.text; if (signature) { @@ -491,8 +491,8 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null, export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date()) { const packetlist = new packet.List(); - const literalFormat = enums.write(enums.literal, literalDataPacket.format); - const signatureType = literalFormat === enums.literal.binary ? + // If data packet was created from Uint8Array, use binary, otherwise use text + const signatureType = literalDataPacket.text === null ? enums.signature.binary : enums.signature.text; await Promise.all(privateKeys.map(async function(privateKey) { @@ -581,15 +581,9 @@ export async function createVerificationObjects(signatureList, literalDataList, } })); - // If this is a text signature, canonicalize line endings of the data - const literalDataPacket = literalDataList[0]; - if (signature.signatureType === enums.signature.text) { - literalDataPacket.setText(literalDataPacket.getText()); - } - const verifiedSig = { keyid: signature.issuerKeyId, - valid: keyPacket ? await signature.verify(keyPacket, literalDataPacket) : null + valid: keyPacket ? await signature.verify(keyPacket, literalDataList[0]) : null }; const packetlist = new packet.List(); diff --git a/src/packet/literal.js b/src/packet/literal.js index 0a6f8bee..8fdd99a1 100644 --- a/src/packet/literal.js +++ b/src/packet/literal.js @@ -37,7 +37,8 @@ function Literal(date=new Date()) { this.tag = enums.packet.literal; this.format = 'utf8'; // default format for literal data packets this.date = util.normalizeDate(date); - this.data = new Uint8Array(0); // literal data representation + this.text = null; // textual data representation + this.data = null; // literal data representation this.filename = 'msg.txt'; } @@ -45,13 +46,12 @@ function Literal(date=new Date()) { * Set the packet data to a javascript native string, end of line * will be normalized to \r\n and by default text is converted to UTF8 * @param {String} text Any native javascript string + * @param {utf8|binary|text} format (optional) The format of the string of bytes */ -Literal.prototype.setText = function(text) { - // normalize EOL to \r\n - text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n/g, "\r\n"); - this.format = 'utf8'; - // encode UTF8 - this.data = util.str_to_Uint8Array(util.encode_utf8(text)); +Literal.prototype.setText = function(text, format='utf8') { + this.format = format; + this.text = text; + this.data = null; }; /** @@ -60,10 +60,14 @@ Literal.prototype.setText = function(text) { * @returns {String} literal data as text */ Literal.prototype.getText = function() { + if (this.text !== null) { + return this.text; + } // decode UTF8 const text = util.decode_utf8(util.Uint8Array_to_str(this.data)); // normalize EOL to \n - return text.replace(/\r\n/g, '\n'); + this.text = text.replace(/\r\n/g, '\n'); + return this.text; }; /** @@ -74,6 +78,7 @@ Literal.prototype.getText = function() { Literal.prototype.setBytes = function(bytes, format) { this.format = format; this.data = bytes; + this.text = null; }; @@ -82,6 +87,14 @@ Literal.prototype.setBytes = function(bytes, format) { * @returns {Uint8Array} A sequence of bytes */ Literal.prototype.getBytes = function() { + if (this.data !== null) { + return this.data; + } + + // normalize EOL to \r\n + const text = this.text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'); + // encode UTF8 + this.data = util.str_to_Uint8Array(util.encode_utf8(text)); return this.data; }; diff --git a/src/packet/signature.js b/src/packet/signature.js index e3210e01..936a1520 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -551,9 +551,15 @@ Signature.prototype.toSign = function (type, data) { switch (type) { case t.binary: - case t.text: return data.getBytes(); + case t.text: { + let text = data.getText(); + // normalize EOL to \r\n + text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'); + // encode UTF8 + return util.str_to_Uint8Array(util.encode_utf8(text)); + } case t.standalone: return new Uint8Array(0); From 6f2abdc2cffe647f7571f1b34f019ef0f9276c9a Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 12 Apr 2018 15:45:31 +0200 Subject: [PATCH 25/51] Implement MIME message type (Literal Data Packet format 'm') --- src/enums.js | 4 +++- src/message.js | 13 ++++++------- src/openpgp.js | 21 ++++++++++++--------- src/packet/literal.js | 4 ++-- test/general/openpgp.js | 31 +++++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/enums.js b/src/enums.js index 12df0646..ae08ff49 100644 --- a/src/enums.js +++ b/src/enums.js @@ -212,7 +212,9 @@ export default { /** Text data 't' */ text: 't'.charCodeAt(), /** Utf8 data 'u' */ - utf8: 'u'.charCodeAt() + utf8: 'u'.charCodeAt(), + /** MIME message body part 'm' */ + mime: 'm'.charCodeAt() }, diff --git a/src/message.js b/src/message.js index c2feb5f5..ec7944ef 100644 --- a/src/message.js +++ b/src/message.js @@ -652,13 +652,14 @@ export function read(input) { * @param {String} text * @param {String} filename (optional) * @param {Date} date (optional) + * @param {utf8|binary|text|mime} type (optional) data packet type * @returns {module:message.Message} new message object * @static */ -export function fromText(text, filename, date=new Date()) { +export function fromText(text, filename, date=new Date(), type='utf8') { const literalDataPacket = new packet.Literal(date); // text will be converted to UTF8 - literalDataPacket.setText(text); + literalDataPacket.setText(text, type); if (filename !== undefined) { literalDataPacket.setFilename(filename); } @@ -672,19 +673,17 @@ export function fromText(text, filename, date=new Date()) { * @param {Uint8Array} bytes * @param {String} filename (optional) * @param {Date} date (optional) + * @param {utf8|binary|text|mime} type (optional) data packet type * @returns {module:message.Message} new message object * @static */ -export function fromBinary(bytes, filename, date=new Date()) { +export function fromBinary(bytes, filename, date=new Date(), type='binary') { if (!util.isUint8Array(bytes)) { throw new Error('Data must be in the form of a Uint8Array'); } const literalDataPacket = new packet.Literal(date); - if (filename) { - literalDataPacket.setFilename(filename); - } - literalDataPacket.setBytes(bytes, enums.read(enums.literal, enums.literal.binary)); + literalDataPacket.setBytes(bytes, type); if (filename !== undefined) { literalDataPacket.setFilename(filename); } diff --git a/src/openpgp.js b/src/openpgp.js index fbbb4148..b9049fab 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -213,6 +213,7 @@ export function encryptKey({ privateKey, passphrase }) { * Encrypts message text/data with public keys, passwords or both at once. At least either public keys or passwords * must be specified. If private keys are specified, those will be used to sign the message. * @param {String|Uint8Array} data text/data to be encrypted as JavaScript binary string or Uint8Array + * @param {utf8|binary|text|mime} dataType (optional) data packet type * @param {Key|Array} publicKeys (optional) array of keys or single key, used to encrypt the message * @param {Key|Array} privateKeys (optional) private keys for signing. If omitted message will not be signed * @param {String|Array} passwords (optional) array of passwords or a single password to encrypt the message @@ -231,15 +232,15 @@ export function encryptKey({ privateKey, passphrase }) { * @async * @static */ -export function encrypt({ data, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date()}) { +export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date()}) { checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported - return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, detached, signature, returnSessionKey, wildcard, date }); + return asyncProxy.delegate('encrypt', { data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, detached, signature, returnSessionKey, wildcard, date }); } const result = {}; return Promise.resolve().then(async function() { - let message = createMessage(data, filename, date); + let message = createMessage(data, filename, date, dataType); if (!privateKeys) { privateKeys = []; } @@ -314,6 +315,7 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe /** * Signs a cleartext message. * @param {String | Uint8Array} data cleartext input to be signed + * @param {utf8|binary|text|mime} dataType (optional) data packet type * @param {Key|Array} privateKeys array of keys or single key with decrypted secret key data to sign cleartext * @param {Boolean} armor (optional) if the return value should be ascii armored or the message object * @param {Boolean} detached (optional) if the return value should contain a detached signature @@ -324,19 +326,19 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe * @async * @static */ -export function sign({ data, privateKeys, armor=true, detached=false, date=new Date() }) { +export function sign({ data, dataType, privateKeys, armor=true, detached=false, date=new Date() }) { checkData(data); privateKeys = toArray(privateKeys); if (asyncProxy) { // use web worker if available return asyncProxy.delegate('sign', { - data, privateKeys, armor, detached, date + data, dataType, privateKeys, armor, detached, date }); } const result = {}; return Promise.resolve().then(async function() { - let message = util.isString(data) ? new CleartextMessage(data) : messageLib.fromBinary(data); + let message = util.isString(data) ? new CleartextMessage(data) : messageLib.fromBinary(data, dataType); if (detached) { const signature = await message.signDetached(privateKeys, undefined, date); @@ -527,14 +529,15 @@ function toArray(param) { * @param {String|Uint8Array} data the payload for the message * @param {String} filename the literal data packet's filename * @param {Date} date the creation date of the package + * @param {utf8|binary|text|mime} type (optional) data packet type * @returns {Message} a message object */ -function createMessage(data, filename, date=new Date()) { +function createMessage(data, filename, date=new Date(), type) { let msg; if (util.isUint8Array(data)) { - msg = messageLib.fromBinary(data, filename, date); + msg = messageLib.fromBinary(data, filename, date, type); } else if (util.isString(data)) { - msg = messageLib.fromText(data, filename, date); + msg = messageLib.fromText(data, filename, date, type); } else { throw new Error('Data must be of type String or Uint8Array'); } diff --git a/src/packet/literal.js b/src/packet/literal.js index 8fdd99a1..6a0cad1b 100644 --- a/src/packet/literal.js +++ b/src/packet/literal.js @@ -46,7 +46,7 @@ function Literal(date=new Date()) { * Set the packet data to a javascript native string, end of line * will be normalized to \r\n and by default text is converted to UTF8 * @param {String} text Any native javascript string - * @param {utf8|binary|text} format (optional) The format of the string of bytes + * @param {utf8|binary|text|mime} format (optional) The format of the string of bytes */ Literal.prototype.setText = function(text, format='utf8') { this.format = format; @@ -73,7 +73,7 @@ Literal.prototype.getText = function() { /** * Set the packet data to value represented by the provided string of bytes. * @param {Uint8Array} bytes The string of bytes - * @param {utf8|binary|text} format The format of the string of bytes + * @param {utf8|binary|text|mime} format The format of the string of bytes */ Literal.prototype.setBytes = function(bytes, format) { this.format = format; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index d08bffa9..fff0f5de 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -1776,6 +1776,37 @@ describe('OpenPGP.js public api tests', function() { }).then(function (packets) { const literals = packets.packets.filterByTag(openpgp.enums.packet.literal); expect(literals.length).to.equal(1); + expect(literals[0].format).to.equal('binary'); + expect(+literals[0].date).to.equal(+future); + expect(packets.getLiteralData()).to.deep.equal(data); + return packets.verify(encryptOpt.publicKeys, future); + }).then(function (signatures) { + expect(+signatures[0].signature.packets[0].created).to.equal(+future); + expect(signatures[0].valid).to.be.true; + expect(encryptOpt.privateKeys[0].getSigningKeyPacket(signatures[0].keyid, future)) + .to.be.not.null; + expect(signatures[0].signature.packets.length).to.equal(1); + }); + }); + + it('should sign, encrypt and decrypt, verify mime data with a date in the future', function () { + const future = new Date(2040, 5, 5, 5, 5, 5, 0); + const data = new Uint8Array([3, 14, 15, 92, 65, 35, 59]); + const encryptOpt = { + data, + dataType: 'mime', + publicKeys: publicKey_2038_2045.keys, + privateKeys: privateKey_2038_2045.keys, + date: future, + armor: false + }; + + return openpgp.encrypt(encryptOpt).then(function (encrypted) { + return encrypted.message.decrypt(encryptOpt.privateKeys); + }).then(function (packets) { + const literals = packets.packets.filterByTag(openpgp.enums.packet.literal); + expect(literals.length).to.equal(1); + expect(literals[0].format).to.equal('mime'); expect(+literals[0].date).to.equal(+future); expect(packets.getLiteralData()).to.deep.equal(data); return packets.verify(encryptOpt.publicKeys, future); From 51d7860622c0485ac288c26e49eca6f304c39c35 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 13 Apr 2018 12:16:36 +0200 Subject: [PATCH 26/51] Native CMAC --- src/crypto/cmac.js | 85 +++++- src/crypto/eax.js | 174 +++++------ src/crypto/ocb.js | 325 +++++++++++---------- src/packet/secret_key.js | 6 +- src/packet/sym_encrypted_aead_protected.js | 4 +- src/packet/sym_encrypted_session_key.js | 6 +- test/crypto/eax.js | 2 +- test/crypto/ocb.js | 4 +- 8 files changed, 339 insertions(+), 267 deletions(-) diff --git a/src/crypto/cmac.js b/src/crypto/cmac.js index 34061258..c4c8107a 100644 --- a/src/crypto/cmac.js +++ b/src/crypto/cmac.js @@ -1,21 +1,80 @@ /** + * @fileoverview This module implements AES-CMAC on top of + * native AES-CBC using either the WebCrypto API or Node.js' crypto API. * @requires asmcrypto.js + * @requires util + * @module crypto/cmac */ -import { AES_CMAC } from 'asmcrypto.js/src/aes/cmac/cmac'; +import { AES_CBC } from 'asmcrypto.js/src/aes/cbc/exports'; +import util from '../util'; -export default class CMAC extends AES_CMAC { - constructor(key) { - super(key); - this._k = this.k.slice(); - } +const webCrypto = util.getWebCryptoAll(); +const nodeCrypto = util.getNodeCrypto(); +const Buffer = util.getNodeBuffer(); - mac(data) { - if (this.result) { - this.bufferLength = 0; - this.k.set(this._k, 0); - this.cbc.AES_reset(undefined, new Uint8Array(16), false); - } - return this.process(data).finish().result; + +const blockLength = 16; + + +function set_xor_r(S, T) { + const offset = S.length - blockLength; + for (let i = 0; i < blockLength; i++) { + S[i + offset] ^= T[i]; } + return S; +} + +function mul2(data) { + const t = data[0] & 0x80; + for (let i = 0; i < 15; i++) { + data[i] = (data[i] << 1) ^ ((data[i + 1] & 0x80) ? 1 : 0); + } + data[15] = (data[15] << 1) ^ (t ? 0x87 : 0); + return data; +} + +const zeros_16 = new Uint8Array(16); + +export default async function CMAC(key) { + const cbc = await CBC(key); + const padding = mul2(await cbc(zeros_16)); + const padding2 = mul2(padding.slice()); + + return async function(data) { + return (await cbc(pad(data, padding, padding2))).subarray(-blockLength); + }; +} + +function pad(data, padding, padding2) { + if (data.length % blockLength === 0) { + return set_xor_r(data, padding); + } + const padded = new Uint8Array(data.length + (blockLength - data.length % blockLength)); + padded.set(data); + padded[data.length] = 0b10000000; + return set_xor_r(padded, padding2); +} + +async function CBC(key) { + if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']); + return async function(pt) { + const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeros_16, length: blockLength * 8 }, key, pt); + return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength); + }; + } + if (util.getNodeCrypto()) { // Node crypto library + key = new Buffer(key); + return async function(pt) { + pt = new Buffer(pt); + const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeros_16); + const ct = en.update(pt); + return new Uint8Array(ct); + }; + } + // asm.js fallback + return async function(pt) { + return AES_CBC.encrypt(pt, key, false, zeros_16); + }; } diff --git a/src/crypto/eax.js b/src/crypto/eax.js index c8c81f8e..86f7396d 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -41,98 +41,104 @@ 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]); -class OMAC extends CMAC { - mac(t, message) { - return super.mac(concat(t, message)); - } +async function OMAC(key) { + const cmac = await CMAC(key); + return function(t, message) { + return cmac(concat(t, message)); + }; } -class CTR { - constructor(key) { - if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support - this.key = webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']); - this.ctr = this.webCtr; - } else if (util.getNodeCrypto()) { // Node crypto library - this.key = new Buffer(key); - this.ctr = this.nodeCtr; - } else { - // asm.js fallback - this.key = key; - } +async function CTR(key) { + if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + key = await webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']); + return async function(pt, iv) { + const ct = await webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, key, pt); + return new Uint8Array(ct); + }; } - - webCtr(pt, iv) { - return this.key - .then(keyObj => webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, keyObj, pt)) - .then(ct => new Uint8Array(ct)); - } - - nodeCtr(pt, iv) { - pt = new Buffer(pt); - iv = new Buffer(iv); - const en = new nodeCrypto.createCipheriv('aes-' + (this.key.length * 8) + '-ctr', this.key, iv); - const ct = Buffer.concat([en.update(pt), en.final()]); - return Promise.resolve(new Uint8Array(ct)); - } - - ctr(pt, iv) { - return Promise.resolve(AES_CTR.encrypt(pt, this.key, iv)); + if (util.getNodeCrypto()) { // Node crypto library + key = new Buffer(key); + return async function(pt, iv) { + pt = new Buffer(pt); + 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 new Uint8Array(ct); + }; } + // asm.js fallback + return async function(pt, iv) { + return AES_CTR.encrypt(pt, key, iv); + }; } -class EAX { - /** - * Class to en/decrypt using EAX mode. - * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' - * @param {Uint8Array} key The encryption key - */ - constructor(cipher, key) { - if (cipher.substr(0, 3) !== 'aes') { - throw new Error('EAX mode supports only AES cipher'); +/** + * Class to en/decrypt using EAX mode. + * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' + * @param {Uint8Array} key The encryption key + */ +async function EAX(cipher, key) { + if (cipher.substr(0, 3) !== 'aes') { + throw new Error('EAX mode supports only AES cipher'); + } + + const [ + omac, + ctr + ] = await Promise.all([ + OMAC(key), + CTR(key) + ]); + + return { + /** + * Encrypt plaintext input. + * @param {Uint8Array} plaintext The cleartext input to be encrypted + * @param {Uint8Array} nonce The nonce (16 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output + */ + encrypt: async function(plaintext, nonce, adata) { + const [ + _nonce, + _adata + ] = await Promise.all([ + omac(zero, nonce), + omac(one, adata) + ]); + const ciphered = await ctr(plaintext, _nonce); + const _ciphered = await omac(two, ciphered); + const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac(*).length === tagLength. + return concat(ciphered, tag); + }, + + /** + * Decrypt ciphertext input. + * @param {Uint8Array} ciphertext The ciphertext input to be decrypted + * @param {Uint8Array} nonce The nonce (16 bytes) + * @param {Uint8Array} adata Associated data to verify + * @returns {Promise} The plaintext output + */ + decrypt: async function(ciphertext, nonce, adata) { + 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, + _adata, + _ciphered + ] = await Promise.all([ + omac(zero, nonce), + omac(one, adata), + omac(two, ciphered) + ]); + 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, _nonce); + return plaintext; } - - const omac = new OMAC(key); - this.omac = omac.mac.bind(omac); - const ctr = new CTR(key); - this.ctr = ctr.ctr.bind(ctr); - } - - /** - * Encrypt plaintext input. - * @param {Uint8Array} plaintext The cleartext input to be encrypted - * @param {Uint8Array} nonce The nonce (16 bytes) - * @param {Uint8Array} adata Associated data to sign - * @returns {Promise} The ciphertext output - */ - async encrypt(plaintext, nonce, adata) { - const _nonce = this.omac(zero, nonce); - const _adata = this.omac(one, adata); - const ciphered = await this.ctr(plaintext, _nonce); - const _ciphered = this.omac(two, ciphered); - const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac(*).length === tagLength. - return concat(ciphered, tag); - } - - /** - * Decrypt ciphertext input. - * @param {Uint8Array} ciphertext The ciphertext input to be decrypted - * @param {Uint8Array} nonce The nonce (16 bytes) - * @param {Uint8Array} adata Associated data to verify - * @returns {Promise} The plaintext output - */ - async decrypt(ciphertext, nonce, adata) { - 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 = this.omac(zero, nonce); - const _adata = this.omac(one, adata); - const _ciphered = this.omac(two, ciphered); - 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 this.ctr(ciphered, _nonce); - return plaintext; - } + }; } diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index 5bfe4aab..034abcd1 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -79,18 +79,19 @@ function double(S) { const zeros_16 = zeros(16); const one = new Uint8Array([1]); -class OCB { - /** - * Class to en/decrypt using OCB mode. - * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' - * @param {Uint8Array} key The encryption key - */ - constructor(cipher, key) { - this.max_ntz = 0; - this.constructKeyVariables(cipher, key); - } +/** + * Class to en/decrypt using OCB mode. + * @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128' + * @param {Uint8Array} key The encryption key + */ +async function OCB(cipher, key) { - constructKeyVariables(cipher, key) { + let max_ntz = 0; + let kv; + + constructKeyVariables(cipher, key); + + function constructKeyVariables(cipher, key) { const aes = new ciphers[cipher](key); const encipher = aes.encrypt.bind(aes); const decipher = aes.decrypt.bind(aes); @@ -104,25 +105,25 @@ class OCB { L.x = L_x; L.$ = L_$; - this.kv = { encipher, decipher, L }; + kv = { encipher, decipher, L }; } - extendKeyVariables(text, adata) { - const { L } = this.kv; - const max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; - for (let i = this.max_ntz + 1; i <= max_ntz; i++) { + function extendKeyVariables(text, adata) { + const { L } = kv; + const new_max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; + for (let i = max_ntz + 1; i <= new_max_ntz; i++) { L[i] = double(L[i - 1]); } - this.max_ntz = max_ntz; + max_ntz = new_max_ntz; } - hash(adata) { + function hash(adata) { if (!adata.length) { // Fast path return zeros_16; } - const { encipher, L } = this.kv; + const { encipher, L } = kv; // // Consider A as a sequence of 128-bit blocks @@ -155,154 +156,156 @@ class OCB { } - /** - * Encrypt plaintext input. - * @param {Uint8Array} plaintext The cleartext input to be encrypted - * @param {Uint8Array} nonce The nonce (15 bytes) - * @param {Uint8Array} adata Associated data to sign - * @returns {Promise} The ciphertext output - */ - async encrypt(plaintext, nonce, adata) { - // - // Consider P as a sequence of 128-bit blocks - // - const m = plaintext.length >> 4; + return { + /** + * Encrypt plaintext input. + * @param {Uint8Array} plaintext The cleartext input to be encrypted + * @param {Uint8Array} nonce The nonce (15 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output + */ + encrypt: async function(plaintext, nonce, adata) { + // + // Consider P as a sequence of 128-bit blocks + // + const m = plaintext.length >> 4; - // - // Key-dependent variables - // - this.extendKeyVariables(plaintext, adata); - const { encipher, L } = this.kv; + // + // Key-dependent variables + // + extendKeyVariables(plaintext, adata); + const { encipher, L } = kv; - // - // Nonce-dependent and per-encryption variables - // - // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); - const bottom = Nonce[15] & 0b111111; - Nonce[15] &= 0b11000000; - const Ktop = encipher(Nonce); - const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); - // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = zeros(16); + // + // Nonce-dependent and per-encryption variables + // + // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). + const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); + const bottom = Nonce[15] & 0b111111; + Nonce[15] &= 0b11000000; + const Ktop = encipher(Nonce); + const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = zeros(16); - const C = new Uint8Array(plaintext.length + tagLength); + const C = new Uint8Array(plaintext.length + tagLength); - // - // Process any whole blocks - // - let i; - let pos = 0; - for (i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - C.set(set_xor(encipher(xor(offset, plaintext)), offset), pos); - set_xor(checksum, plaintext); + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + C.set(set_xor(encipher(xor(offset, plaintext)), offset), pos); + set_xor(checksum, plaintext); - plaintext = plaintext.subarray(16); - pos += 16; + plaintext = plaintext.subarray(16); + pos += 16; + } + + // + // Process any final partial block and compute raw tag + // + if (plaintext.length) { + set_xor(offset, L.x); + const Pad = encipher(offset); + C.set(xor(plaintext, Pad), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) + const xorInput = zeros(16); + xorInput.set(plaintext, 0); + xorInput[plaintext.length] = 0b10000000; + set_xor(checksum, xorInput); + pos += plaintext.length; + } + const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(adata)); + + // + // Assemble ciphertext + // + C.set(Tag, pos); + return C; + }, + + + /** + * Decrypt ciphertext input. + * @param {Uint8Array} ciphertext The ciphertext input to be decrypted + * @param {Uint8Array} nonce The nonce (15 bytes) + * @param {Uint8Array} adata Associated data to verify + * @returns {Promise} The plaintext output + */ + decrypt: async function(ciphertext, nonce, adata) { + // + // Consider C as a sequence of 128-bit blocks + // + const T = ciphertext.subarray(ciphertext.length - tagLength); + ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); + const m = ciphertext.length >> 4; + + // + // Key-dependent variables + // + extendKeyVariables(ciphertext, adata); + const { encipher, decipher, L } = kv; + + // + // Nonce-dependent and per-encryption variables + // + // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). + const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); + const bottom = Nonce[15] & 0b111111; + Nonce[15] &= 0b11000000; + const Ktop = encipher(Nonce); + const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = zeros(16); + + const P = new Uint8Array(ciphertext.length); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + set_xor(offset, L[ntz(i + 1)]); + P.set(set_xor(decipher(xor(offset, ciphertext)), offset), pos); + set_xor(checksum, P.subarray(pos)); + + ciphertext = ciphertext.subarray(16); + pos += 16; + } + + // + // Process any final partial block and compute raw tag + // + if (ciphertext.length) { + set_xor(offset, L.x); + const Pad = encipher(offset); + P.set(xor(ciphertext, Pad), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) + const xorInput = zeros(16); + xorInput.set(P.subarray(pos), 0); + xorInput[ciphertext.length] = 0b10000000; + set_xor(checksum, xorInput); + pos += ciphertext.length; + } + const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(adata)); + + // + // Check for validity and assemble plaintext + // + if (!util.equalsUint8Array(Tag, T)) { + throw new Error('Authentication tag mismatch in OCB ciphertext'); + } + return P; } - - // - // Process any final partial block and compute raw tag - // - if (plaintext.length) { - set_xor(offset, L.x); - const Pad = encipher(offset); - C.set(xor(plaintext, Pad), pos); - - // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) - const xorInput = zeros(16); - xorInput.set(plaintext, 0); - xorInput[plaintext.length] = 0b10000000; - set_xor(checksum, xorInput); - pos += plaintext.length; - } - const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), this.hash(adata)); - - // - // Assemble ciphertext - // - C.set(Tag, pos); - return C; - } - - - /** - * Decrypt ciphertext input. - * @param {Uint8Array} ciphertext The ciphertext input to be decrypted - * @param {Uint8Array} nonce The nonce (15 bytes) - * @param {Uint8Array} adata Associated data to verify - * @returns {Promise} The plaintext output - */ - async decrypt(ciphertext, nonce, adata) { - // - // Consider C as a sequence of 128-bit blocks - // - const T = ciphertext.subarray(ciphertext.length - tagLength); - ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); - const m = ciphertext.length >> 4; - - // - // Key-dependent variables - // - this.extendKeyVariables(ciphertext, adata); - const { encipher, decipher, L } = this.kv; - - // - // Nonce-dependent and per-encryption variables - // - // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); - const bottom = Nonce[15] & 0b111111; - Nonce[15] &= 0b11000000; - const Ktop = encipher(Nonce); - const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); - // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = zeros(16); - - const P = new Uint8Array(ciphertext.length); - - // - // Process any whole blocks - // - let i; - let pos = 0; - for (i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - P.set(set_xor(decipher(xor(offset, ciphertext)), offset), pos); - set_xor(checksum, P.subarray(pos)); - - ciphertext = ciphertext.subarray(16); - pos += 16; - } - - // - // Process any final partial block and compute raw tag - // - if (ciphertext.length) { - set_xor(offset, L.x); - const Pad = encipher(offset); - P.set(xor(ciphertext, Pad), pos); - - // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) - const xorInput = zeros(16); - xorInput.set(P.subarray(pos), 0); - xorInput[ciphertext.length] = 0b10000000; - set_xor(checksum, xorInput); - pos += ciphertext.length; - } - const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), this.hash(adata)); - - // - // Check for validity and assemble plaintext - // - if (!util.equalsUint8Array(Tag, T)) { - throw new Error('Authentication tag mismatch in OCB ciphertext'); - } - return P; - } + }; } diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 59105443..491d60ae 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -211,7 +211,8 @@ SecretKey.prototype.encrypt = async function (passphrase) { arr = [new Uint8Array([253, optionalFields.length])]; arr.push(optionalFields); const mode = crypto[aead]; - const encrypted = await new mode(symmetric, key).encrypt(cleartext, iv.subarray(0, mode.ivLength), new Uint8Array()); + const modeInstance = await mode(symmetric, key); + const encrypted = await modeInstance.encrypt(cleartext, iv.subarray(0, mode.ivLength), new Uint8Array()); arr.push(util.writeNumber(encrypted.length, 4)); arr.push(encrypted); } else { @@ -305,7 +306,8 @@ SecretKey.prototype.decrypt = async function (passphrase) { if (aead) { const mode = crypto[aead]; try { - cleartext = await new mode(symmetric, key).decrypt(ciphertext, iv.subarray(0, mode.ivLength), new Uint8Array()); + const modeInstance = await mode(symmetric, key); + cleartext = await modeInstance.decrypt(ciphertext, iv.subarray(0, mode.ivLength), new Uint8Array()); } catch(err) { if (err.message.startsWith('Authentication tag mismatch')) { throw new Error('Incorrect key passphrase: ' + err.message); diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index fcd4c80d..eb31238a 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -107,7 +107,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith 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 = []; - const modeInstance = new mode(cipher, key); + const modeInstance = await mode(cipher, key); for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { decryptedPromises.push( modeInstance.decrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray) @@ -149,7 +149,7 @@ 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 = new mode(sessionKeyAlgorithm, key); + 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) diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index 39687f40..c0a26049 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -142,7 +142,8 @@ SymEncryptedSessionKey.prototype.decrypt = async function(passphrase) { if (this.version === 5) { const mode = crypto[this.aeadAlgorithm]; const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); - this.sessionKey = await new mode(algo, key).decrypt(this.encrypted, this.iv, adata); + const modeInstance = await mode(algo, key); + this.sessionKey = await modeInstance.decrypt(this.encrypted, this.iv, adata); } else if (this.encrypted !== null) { const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null); @@ -182,7 +183,8 @@ SymEncryptedSessionKey.prototype.encrypt = async function(passphrase) { const mode = crypto[this.aeadAlgorithm]; this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]); - this.encrypted = await new mode(algo, key).encrypt(this.sessionKey, this.iv, adata); + const modeInstance = await mode(algo, key); + this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, adata); } else { const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]); const private_key = util.concatUint8Array([algo_enum, this.sessionKey]); diff --git a/test/crypto/eax.js b/test/crypto/eax.js index c5aec8b9..3158428a 100644 --- a/test/crypto/eax.js +++ b/test/crypto/eax.js @@ -94,7 +94,7 @@ function testAESEAX() { headerBytes = openpgp.util.hex_to_Uint8Array(vec.header), ctBytes = openpgp.util.hex_to_Uint8Array(vec.ct); - const eax = new openpgp.crypto.eax(cipher, keyBytes); + const eax = await openpgp.crypto.eax(cipher, keyBytes); // encryption test let ct = await eax.encrypt(msgBytes, nonceBytes, headerBytes); diff --git a/test/crypto/ocb.js b/test/crypto/ocb.js index a1e269cb..290e8b88 100644 --- a/test/crypto/ocb.js +++ b/test/crypto/ocb.js @@ -122,7 +122,7 @@ describe('Symmetric AES-OCB', function() { headerBytes = openpgp.util.hex_to_Uint8Array(vec.A), ctBytes = openpgp.util.hex_to_Uint8Array(vec.C); - const ocb = new openpgp.crypto.ocb(cipher, keyBytes); + const ocb = await openpgp.crypto.ocb(cipher, keyBytes); // encryption test let ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes); @@ -162,7 +162,7 @@ describe('Symmetric AES-OCB', function() { const K = new Uint8Array(KEYLEN / 8); K[K.length - 1] = TAGLEN; - const ocb = new openpgp.crypto.ocb('aes' + KEYLEN, K); + const ocb = await openpgp.crypto.ocb('aes' + KEYLEN, K); const C = []; let N; From 69762f95de94e2ecfd1a86f6bc1467b2ff11047d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 17 Apr 2018 16:16:48 +0200 Subject: [PATCH 27/51] Code style --- src/crypto/cmac.js | 78 ++++++++++++++++------ src/crypto/eax.js | 58 +++++++---------- src/crypto/ocb.js | 157 +++++++++++++++++++++------------------------ 3 files changed, 154 insertions(+), 139 deletions(-) diff --git a/src/crypto/cmac.js b/src/crypto/cmac.js index c4c8107a..dde7af74 100644 --- a/src/crypto/cmac.js +++ b/src/crypto/cmac.js @@ -14,17 +14,47 @@ const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); +/** + * This implementation of CMAC is based on the description of OMAC in + * http://web.cs.ucdavis.edu/~rogaway/papers/eax.pdf. As per that + * document: + * + * We have made a small modification to the OMAC algorithm as it was + * originally presented, changing one of its two constants. + * Specifically, the constant 4 at line 85 was the constant 1/2 (the + * multiplicative inverse of 2) in the original definition of OMAC [14]. + * The OMAC authors indicate that they will promulgate this modification + * [15], which slightly simplifies implementations. + */ + const blockLength = 16; -function set_xor_r(S, T) { - const offset = S.length - blockLength; +/** + * xor `padding` into the end of `data`. This function implements "the + * operation xor→ [which] xors the shorter string into the end of longer + * one". Since data is always as least as long as padding, we can + * simplify the implementation. + * @param {Uint8Array} data + * @param {Uint8Array} padding + */ +function rightXorMut(data, padding) { + const offset = data.length - blockLength; for (let i = 0; i < blockLength; i++) { - S[i + offset] ^= T[i]; + data[i + offset] ^= padding[i]; } - return S; + return data; } +/** + * 2L = L<<1 if the first bit of L is 0 and 2L = (L<<1) xor (0^120 || + * 10000111) otherwise, where L<<1 means the left shift of L by one + * position (the first bit vanishing and a zero entering into the last + * bit). The value of 4L is simply 2(2L). We warn that to avoid side- + * channel attacks one must implement the doubling operation in a + * constant-time manner. + * @param {Uint8Array} data + */ function mul2(data) { const t = data[0] & 0x80; for (let i = 0; i < 15; i++) { @@ -34,33 +64,39 @@ function mul2(data) { return data; } -const zeros_16 = new Uint8Array(16); - -export default async function CMAC(key) { - const cbc = await CBC(key); - const padding = mul2(await cbc(zeros_16)); - const padding2 = mul2(padding.slice()); - - return async function(data) { - return (await cbc(pad(data, padding, padding2))).subarray(-blockLength); - }; -} - function pad(data, padding, padding2) { + // if |M| in {n, 2n, 3n, ...} if (data.length % blockLength === 0) { - return set_xor_r(data, padding); + // then return M xor→ B, + return rightXorMut(data, padding); } + // else return (M || 10^(n−1−(|M| mod n))) xor→ P const padded = new Uint8Array(data.length + (blockLength - data.length % blockLength)); padded.set(data); padded[data.length] = 0b10000000; - return set_xor_r(padded, padding2); + return rightXorMut(padded, padding2); +} + +const zeroBlock = new Uint8Array(blockLength); + +export default async function CMAC(key) { + const cbc = await CBC(key); + + // L ← E_K(0^n); B ← 2L; P ← 4L + const padding = mul2(await cbc(zeroBlock)); + const padding2 = mul2(padding.slice()); + + return async function(data) { + // return CBC_K(pad(M; B, P)) + return (await cbc(pad(data, padding, padding2))).subarray(-blockLength); + }; } async function CBC(key) { if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']); return async function(pt) { - const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeros_16, length: blockLength * 8 }, key, pt); + const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeroBlock, length: blockLength * 8 }, key, pt); return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength); }; } @@ -68,13 +104,13 @@ async function CBC(key) { key = new Buffer(key); return async function(pt) { pt = new Buffer(pt); - const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeros_16); + const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeroBlock); const ct = en.update(pt); return new Uint8Array(ct); }; } // asm.js fallback return async function(pt) { - return AES_CBC.encrypt(pt, key, false, zeros_16); + return AES_CBC.encrypt(pt, key, false, zeroBlock); }; } diff --git a/src/crypto/eax.js b/src/crypto/eax.js index 86f7396d..ca353b7c 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -37,14 +37,14 @@ 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]); +const zero = new Uint8Array(blockLength); +const one = new Uint8Array(blockLength); one[blockLength - 1] = 1; +const two = new Uint8Array(blockLength); two[blockLength - 1] = 2; async function OMAC(key) { const cmac = await CMAC(key); return function(t, message) { - return cmac(concat(t, message)); + return cmac(util.concatUint8Array([t, message])); }; } @@ -101,16 +101,19 @@ async function EAX(cipher, key) { */ encrypt: async function(plaintext, nonce, adata) { const [ - _nonce, - _adata + omacNonce, + omacAdata ] = await Promise.all([ omac(zero, nonce), omac(one, adata) ]); - const ciphered = await ctr(plaintext, _nonce); - const _ciphered = await omac(two, ciphered); - const tag = xor3(_nonce, _ciphered, _adata); // Assumes that omac(*).length === tagLength. - return concat(ciphered, tag); + const ciphered = await ctr(plaintext, omacNonce); + const omacCiphered = await omac(two, ciphered); + const tag = omacCiphered; // Assumes that omac(*).length === tagLength. + for (let i = 0; i < tagLength; i++) { + tag[i] ^= omacAdata[i] ^ omacNonce[i]; + } + return util.concatUint8Array([ciphered, tag]); }, /** @@ -122,20 +125,23 @@ async function EAX(cipher, key) { */ decrypt: async function(ciphertext, nonce, adata) { 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 ciphered = ciphertext.subarray(0, -tagLength); + const ctTag = ciphertext.subarray(-tagLength); const [ - _nonce, - _adata, - _ciphered + omacNonce, + omacAdata, + omacCiphered ] = await Promise.all([ omac(zero, nonce), omac(one, adata), omac(two, ciphered) ]); - 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, _nonce); + const tag = omacCiphered; // Assumes that omac(*).length === tagLength. + for (let i = 0; i < tagLength; i++) { + tag[i] ^= omacAdata[i] ^ omacNonce[i]; + } + if (!util.equalsUint8Array(ctTag, tag)) throw new Error('Authentication tag mismatch in EAX ciphertext'); + const plaintext = await ctr(ciphered, omacNonce); return plaintext; } }; @@ -159,19 +165,3 @@ EAX.blockLength = blockLength; EAX.ivLength = ivLength; export default EAX; - - -////////////////////////// -// // -// Helper functions // -// // -////////////////////////// - - -function xor3(a, b, c) { - return a.map((n, i) => n ^ b[i] ^ c[i]); -} - -function concat(...arrays) { - return util.concatUint8Array(arrays); -} diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index 034abcd1..f22f3aa4 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -36,13 +36,6 @@ const ivLength = 15; const tagLength = 16; -const { shiftLeft, shiftRight } = util; - - -function zeros(bytes) { - return new Uint8Array(bytes); -} - function ntz(n) { let ntz = 0; for(let i = 1; (n & i) === 0; i <<= 1) { @@ -51,7 +44,7 @@ function ntz(n) { return ntz; } -function set_xor(S, T) { +function xorMut(S, T) { for (let i = 0; i < S.length; i++) { S[i] ^= T[i]; } @@ -59,16 +52,12 @@ function set_xor(S, T) { } function xor(S, T) { - return set_xor(S.slice(), T); -} - -function concat(...arrays) { - return util.concatUint8Array(arrays); + return xorMut(S.slice(), T); } function double(S) { const double = S.slice(); - shiftLeft(double, 1); + util.shiftLeft(double, 1); if (S[0] & 0b10000000) { double[15] ^= 0b10000111; } @@ -76,7 +65,7 @@ function double(S) { } -const zeros_16 = zeros(16); +const zeroBlock = new Uint8Array(blockLength); const one = new Uint8Array([1]); /** @@ -86,7 +75,7 @@ const one = new Uint8Array([1]); */ async function OCB(cipher, key) { - let max_ntz = 0; + let maxNtz = 0; let kv; constructKeyVariables(cipher, key); @@ -96,45 +85,45 @@ async function OCB(cipher, key) { const encipher = aes.encrypt.bind(aes); const decipher = aes.decrypt.bind(aes); - const L_x = encipher(zeros_16); - const L_$ = double(L_x); - const L = []; - L[0] = double(L_$); + const mask_x = encipher(zeroBlock); + const mask_$ = double(mask_x); + const mask = []; + mask[0] = double(mask_$); - L.x = L_x; - L.$ = L_$; + mask.x = mask_x; + mask.$ = mask_$; - kv = { encipher, decipher, L }; + kv = { encipher, decipher, mask }; } function extendKeyVariables(text, adata) { - const { L } = kv; - const new_max_ntz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; - for (let i = max_ntz + 1; i <= new_max_ntz; i++) { - L[i] = double(L[i - 1]); + const { mask } = kv; + const newMaxNtz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; + for (let i = maxNtz + 1; i <= newMaxNtz; i++) { + mask[i] = double(mask[i - 1]); } - max_ntz = new_max_ntz; + maxNtz = newMaxNtz; } function hash(adata) { if (!adata.length) { // Fast path - return zeros_16; + return zeroBlock; } - const { encipher, L } = kv; + const { encipher, mask } = kv; // // Consider A as a sequence of 128-bit blocks // const m = adata.length >> 4; - const offset = zeros(16); - const sum = zeros(16); + const offset = new Uint8Array(16); + const sum = new Uint8Array(16); for (let i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - set_xor(sum, encipher(xor(offset, adata))); + xorMut(offset, mask[ntz(i + 1)]); + xorMut(sum, encipher(xor(offset, adata))); adata = adata.subarray(16); } @@ -142,14 +131,14 @@ async function OCB(cipher, key) { // Process any final partial block; compute final hash value // if (adata.length) { - set_xor(offset, L.x); + xorMut(offset, mask.x); - const cipherInput = zeros(16); + const cipherInput = new Uint8Array(16); cipherInput.set(adata, 0); cipherInput[adata.length] = 0b10000000; - set_xor(cipherInput, offset); + xorMut(cipherInput, offset); - set_xor(sum, encipher(cipherInput)); + xorMut(sum, encipher(cipherInput)); } return sum; @@ -174,22 +163,22 @@ async function OCB(cipher, key) { // Key-dependent variables // extendKeyVariables(plaintext, adata); - const { encipher, L } = kv; + const { encipher, mask } = kv; // // Nonce-dependent and per-encryption variables // - // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); - const bottom = Nonce[15] & 0b111111; - Nonce[15] &= 0b11000000; - const Ktop = encipher(Nonce); - const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // We assume here that tagLength mod 16 == 0. + const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, 15 - nonce.length), one, nonce]); + const bottom = paddedNonce[15] & 0b111111; + paddedNonce[15] &= 0b11000000; + const kTop = encipher(paddedNonce); + const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]); // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = zeros(16); + const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = new Uint8Array(16); - const C = new Uint8Array(plaintext.length + tagLength); + const ct = new Uint8Array(plaintext.length + tagLength); // // Process any whole blocks @@ -197,9 +186,9 @@ async function OCB(cipher, key) { let i; let pos = 0; for (i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - C.set(set_xor(encipher(xor(offset, plaintext)), offset), pos); - set_xor(checksum, plaintext); + xorMut(offset, mask[ntz(i + 1)]); + ct.set(xorMut(encipher(xor(offset, plaintext)), offset), pos); + xorMut(checksum, plaintext); plaintext = plaintext.subarray(16); pos += 16; @@ -209,24 +198,24 @@ async function OCB(cipher, key) { // Process any final partial block and compute raw tag // if (plaintext.length) { - set_xor(offset, L.x); - const Pad = encipher(offset); - C.set(xor(plaintext, Pad), pos); + xorMut(offset, mask.x); + const padding = encipher(offset); + ct.set(xor(plaintext, padding), pos); - // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) - const xorInput = zeros(16); + // Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*))) + const xorInput = new Uint8Array(16); xorInput.set(plaintext, 0); xorInput[plaintext.length] = 0b10000000; - set_xor(checksum, xorInput); + xorMut(checksum, xorInput); pos += plaintext.length; } - const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(adata)); + const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata)); // // Assemble ciphertext // - C.set(Tag, pos); - return C; + ct.set(tag, pos); + return ct; }, @@ -241,7 +230,7 @@ async function OCB(cipher, key) { // // Consider C as a sequence of 128-bit blocks // - const T = ciphertext.subarray(ciphertext.length - tagLength); + const ctTag = ciphertext.subarray(ciphertext.length - tagLength); ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); const m = ciphertext.length >> 4; @@ -249,22 +238,22 @@ async function OCB(cipher, key) { // Key-dependent variables // extendKeyVariables(ciphertext, adata); - const { encipher, decipher, L } = kv; + const { encipher, decipher, mask } = kv; // // Nonce-dependent and per-encryption variables // - // We assume here that TAGLEN mod 128 == 0 (tagLength === 16). - const Nonce = concat(zeros_16.subarray(0, 15 - nonce.length), one, nonce); - const bottom = Nonce[15] & 0b111111; - Nonce[15] &= 0b11000000; - const Ktop = encipher(Nonce); - const Stretch = concat(Ktop, xor(Ktop.subarray(0, 8), Ktop.subarray(1, 9))); + // We assume here that tagLength mod 16 == 0. + const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, 15 - nonce.length), one, nonce]); + const bottom = paddedNonce[15] & 0b111111; + paddedNonce[15] &= 0b11000000; + const kTop = encipher(paddedNonce); + const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]); // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = shiftRight(Stretch.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = zeros(16); + const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + const checksum = new Uint8Array(16); - const P = new Uint8Array(ciphertext.length); + const pt = new Uint8Array(ciphertext.length); // // Process any whole blocks @@ -272,9 +261,9 @@ async function OCB(cipher, key) { let i; let pos = 0; for (i = 0; i < m; i++) { - set_xor(offset, L[ntz(i + 1)]); - P.set(set_xor(decipher(xor(offset, ciphertext)), offset), pos); - set_xor(checksum, P.subarray(pos)); + xorMut(offset, mask[ntz(i + 1)]); + pt.set(xorMut(decipher(xor(offset, ciphertext)), offset), pos); + xorMut(checksum, pt.subarray(pos)); ciphertext = ciphertext.subarray(16); pos += 16; @@ -284,26 +273,26 @@ async function OCB(cipher, key) { // Process any final partial block and compute raw tag // if (ciphertext.length) { - set_xor(offset, L.x); - const Pad = encipher(offset); - P.set(xor(ciphertext, Pad), pos); + xorMut(offset, mask.x); + const padding = encipher(offset); + pt.set(xor(ciphertext, padding), pos); - // Checksum_* = Checksum_m xor (P_* || 1 || zeros(127-bitlen(P_*))) - const xorInput = zeros(16); - xorInput.set(P.subarray(pos), 0); + // Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*))) + const xorInput = new Uint8Array(16); + xorInput.set(pt.subarray(pos), 0); xorInput[ciphertext.length] = 0b10000000; - set_xor(checksum, xorInput); + xorMut(checksum, xorInput); pos += ciphertext.length; } - const Tag = set_xor(encipher(set_xor(set_xor(checksum, offset), L.$)), hash(adata)); + const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata)); // // Check for validity and assemble plaintext // - if (!util.equalsUint8Array(Tag, T)) { + if (!util.equalsUint8Array(ctTag, tag)) { throw new Error('Authentication tag mismatch in OCB ciphertext'); } - return P; + return pt; } }; } From d5a7cb303731d5da56e2d5bdf79e40c034c3fa8a Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 18 Apr 2018 15:42:16 +0200 Subject: [PATCH 28/51] Constant-time double() in OCB --- src/crypto/cmac.js | 22 ++-------------------- src/crypto/ocb.js | 16 +++------------- src/util.js | 28 ++++++++++++++-------------- 3 files changed, 19 insertions(+), 47 deletions(-) diff --git a/src/crypto/cmac.js b/src/crypto/cmac.js index dde7af74..060b2efb 100644 --- a/src/crypto/cmac.js +++ b/src/crypto/cmac.js @@ -46,24 +46,6 @@ function rightXorMut(data, padding) { return data; } -/** - * 2L = L<<1 if the first bit of L is 0 and 2L = (L<<1) xor (0^120 || - * 10000111) otherwise, where L<<1 means the left shift of L by one - * position (the first bit vanishing and a zero entering into the last - * bit). The value of 4L is simply 2(2L). We warn that to avoid side- - * channel attacks one must implement the doubling operation in a - * constant-time manner. - * @param {Uint8Array} data - */ -function mul2(data) { - const t = data[0] & 0x80; - for (let i = 0; i < 15; i++) { - data[i] = (data[i] << 1) ^ ((data[i + 1] & 0x80) ? 1 : 0); - } - data[15] = (data[15] << 1) ^ (t ? 0x87 : 0); - return data; -} - function pad(data, padding, padding2) { // if |M| in {n, 2n, 3n, ...} if (data.length % blockLength === 0) { @@ -83,8 +65,8 @@ export default async function CMAC(key) { const cbc = await CBC(key); // L ← E_K(0^n); B ← 2L; P ← 4L - const padding = mul2(await cbc(zeroBlock)); - const padding2 = mul2(padding.slice()); + const padding = util.double(await cbc(zeroBlock)); + const padding2 = util.double(padding); return async function(data) { // return CBC_K(pad(M; B, P)) diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index f22f3aa4..e0bd88f4 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -55,16 +55,6 @@ function xor(S, T) { return xorMut(S.slice(), T); } -function double(S) { - const double = S.slice(); - util.shiftLeft(double, 1); - if (S[0] & 0b10000000) { - double[15] ^= 0b10000111; - } - return double; -} - - const zeroBlock = new Uint8Array(blockLength); const one = new Uint8Array([1]); @@ -86,9 +76,9 @@ async function OCB(cipher, key) { const decipher = aes.decrypt.bind(aes); const mask_x = encipher(zeroBlock); - const mask_$ = double(mask_x); + const mask_$ = util.double(mask_x); const mask = []; - mask[0] = double(mask_$); + mask[0] = util.double(mask_$); mask.x = mask_x; @@ -101,7 +91,7 @@ async function OCB(cipher, key) { const { mask } = kv; const newMaxNtz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; for (let i = maxNtz + 1; i <= newMaxNtz; i++) { - mask[i] = double(mask[i - 1]); + mask[i] = util.double(mask[i - 1]); } maxNtz = newMaxNtz; } diff --git a/src/util.js b/src/util.js index af8a93cd..719d2bef 100644 --- a/src/util.js +++ b/src/util.js @@ -444,22 +444,22 @@ export default { }, /** - * Shift a Uint8Array to the left by n bits - * @param {Uint8Array} array The array to shift - * @param {Integer} bits Amount of bits to shift (MUST be smaller - * than 8) - * @returns {String} Resulting array. + * If S[1] == 0, then double(S) == (S[2..128] || 0); + * otherwise, double(S) == (S[2..128] || 0) xor + * (zeros(120) || 10000111). + * + * Both OCB and EAX (through CMAC) require this function to be constant-time. + * + * @param {Uint8Array} data */ - shiftLeft: function (array, bits) { - if (bits) { - for (let i = 0; i < array.length; i++) { - array[i] <<= bits; - if (i + 1 < array.length) { - array[i] |= array[i + 1] >> (8 - bits); - } - } + double: function(data) { + const double = new Uint8Array(data.length); + const last = data.length - 1; + for (let i = 0; i < last; i++) { + double[i] = (data[i] << 1) ^ (data[i + 1] >> 7); } - return array; + double[last] = (data[last] << 1) ^ ((data[0] >> 7) * 0x87); + return double; }, /** From e061df113c0b8505e6d15e9a06c4a62e8d168082 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 19 Apr 2018 17:15:25 +0200 Subject: [PATCH 29/51] Implement GCM mode in the new draft Also, implement additional data for GCM --- src/crypto/gcm.js | 140 ++++++++++----------- src/packet/sym_encrypted_aead_protected.js | 7 +- test/crypto/crypto.js | 36 ++++-- test/general/openpgp.js | 30 +++++ 4 files changed, 129 insertions(+), 84 deletions(-) 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() { From d7efead33720c67e987865fcd0dacdd282de378f Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 11 Apr 2018 19:47:32 +0200 Subject: [PATCH 30/51] Update Web Worker selection logic for AEAD --- src/openpgp.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index b9049fab..1ad5fdf1 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -23,6 +23,7 @@ * @requires cleartext * @requires key * @requires config + * @requires enums * @requires util * @requires polyfills * @requires worker/async_proxy @@ -41,6 +42,7 @@ import * as messageLib from './message'; import { CleartextMessage } from './cleartext'; import { generate, reformat } from './key'; import config from './config/config'; +import enums from './enums'; import util from './util'; import AsyncProxy from './worker/async_proxy'; @@ -581,10 +583,15 @@ function onError(message, error) { } /** - * Check for AES-GCM support and configuration by the user. Only browsers that - * implement the current WebCrypto specification support native AES-GCM. + * Check for native AEAD support and configuration by the user. Only + * browsers that implement the current WebCrypto specification support + * native GCM. Native EAX is built on CTR and CBC, which all browsers + * support. OCB and CFB are not natively supported. * @returns {Boolean} If authenticated encryption should be used */ function nativeAEAD() { - return util.getWebCrypto() && config.aead_protect; + return config.aead_protect && ( + ((config.aead_protect_version !== 4 || config.aead_mode === enums.aead.gcm) && util.getWebCrypto()) || + (config.aead_protect_version === 4 && config.aead_mode === enums.aead.eax && util.getWebCryptoAll()) + ); } From 4e204d7331111e50b33eff5733fb9702685d13ca Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 12 Apr 2018 15:00:09 +0200 Subject: [PATCH 31/51] Update AEAD instructions in README --- README.md | 19 ++++++++++++++++--- src/config/config.js | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a518d99c..973b7884 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,22 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch * If the user's browser supports [native WebCrypto](https://caniuse.com/#feat=cryptography) via the `window.crypto.subtle` API, this will be used. Under Node.js the native [crypto module](https://nodejs.org/API/crypto.html#crypto_crypto) is used. This can be deactivated by setting `openpgp.config.use_native = false`. -* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ford-openpgp-format-00) for authenticated encryption [using native AES-GCM](https://github.com/openpgpjs/openpgpjs/pull/430). This makes symmetric encryption about 30x faster on supported platforms. Since the specification has not been finalized and other OpenPGP implementations haven't adopted it yet, the feature is currently behind a flag. You can activate it by setting `openpgp.config.aead_protect = true`. **Note: activating this setting can break compatibility with other OpenPGP implementations, so be careful if that's one of your requirements.** +* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04) for authenticated encryption using native AES-EAX, OCB, or GCM. This makes symmetric encryption up to 30x faster on supported platforms. Since the specification has not been finalized and other OpenPGP implementations haven't adopted it yet, the feature is currently behind a flag. **Note: activating this setting can break compatibility with other OpenPGP implementations, and also with future versions of OpenPGP.js. Don't use it with messages you want to store on disk or in a database.** You can enable it by setting: + + ``` + openpgp.config.aead_protect = true + openpgp.config.aead_protect_version = 4 + ``` + + You can change the AEAD mode by setting one of the following options: + + ``` + openpgp.config.aead_mode = openpgp.enums.aead.eax // Default, native + openpgp.config.aead_mode = openpgp.enums.aead.ocb // Non-native + openpgp.config.aead_mode = openpgp.enums.aead.gcm // **Non-standard**, fastest + ``` + + We previously also implemented an [earlier version](https://tools.ietf.org/html/draft-ford-openpgp-format-00) of the draft (using GCM), which you could enable by simply setting `openpgp.config.aead_protect = true`. If you need to stay compatible with that version, don't set `openpgp.config.aead_protect_version = 4`. * For environments that don't provide native crypto, the library falls back to [asm.js](https://caniuse.com/#feat=asmjs) implementations of AES, SHA-1, and SHA-256. We use [Rusha](https://github.com/srijs/rusha) and [asmCrypto Lite](https://github.com/openpgpjs/asmcrypto-lite) (a minimal subset of asmCrypto.js built specifically for OpenPGP.js). @@ -92,8 +107,6 @@ Here are some examples of how to use the v2.x+ API. For more elaborate examples var openpgp = require('openpgp'); // use as CommonJS, AMD, ES6 module or via window.openpgp openpgp.initWorker({ path:'openpgp.worker.js' }) // set the relative web worker path - -openpgp.config.aead_protect = true // activate fast AES-GCM mode (not yet OpenPGP standard) ``` #### Encrypt and decrypt *Uint8Array* data with a password diff --git a/src/config/config.js b/src/config/config.js index 5446b291..8950bc9e 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -63,6 +63,7 @@ export default { /** * Default Authenticated Encryption with Additional Data (AEAD) encryption mode * Only has an effect when aead_protect is set to true. + * **FUTURE OPENPGP.JS VERSIONS MAY BREAK COMPATIBILITY WHEN USING THIS OPTION** * @memberof module:config * @property {Integer} aead_mode Default AEAD mode {@link module:enums.aead} */ From ebeedd3443ceda323d0fbe7a62c2c7c41d2c0809 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 19 Apr 2018 13:34:28 +0200 Subject: [PATCH 32/51] Fix removing whitespace from the last line of cleartext signed messages Also, move normalizing line endings and removing whitespace to util functions --- src/cleartext.js | 6 ++++-- src/packet/literal.js | 4 ++-- src/packet/signature.js | 2 +- src/util.js | 21 +++++++++++++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/cleartext.js b/src/cleartext.js index 60352fa4..8c423b96 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -19,6 +19,7 @@ * @requires config * @requires encoding/armor * @requires enums + * @requires util * @requires packet * @requires signature * @module cleartext @@ -27,6 +28,7 @@ import config from './config'; import armor from './encoding/armor'; import enums from './enums'; +import util from './util'; import packet from './packet'; import { Signature } from './signature'; import { createVerificationObjects, createSignaturePackets } from './message'; @@ -43,7 +45,7 @@ export function CleartextMessage(text, signature) { return new CleartextMessage(text, signature); } // normalize EOL to canonical form - this.text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n/g, "\r\n"); + this.text = util.canonicalizeEOL(util.removeTrailingSpaces(text)); if (signature && !(signature instanceof Signature)) { throw new Error('Invalid signature input'); } @@ -122,7 +124,7 @@ CleartextMessage.prototype.verifyDetached = function(signature, keys, date=new D */ CleartextMessage.prototype.getText = function() { // normalize end of line to \n - return this.text.replace(/\r\n/g, "\n"); + return util.nativeEOL(this.text); }; /** diff --git a/src/packet/literal.js b/src/packet/literal.js index 6a0cad1b..87c5cd1e 100644 --- a/src/packet/literal.js +++ b/src/packet/literal.js @@ -66,7 +66,7 @@ Literal.prototype.getText = function() { // decode UTF8 const text = util.decode_utf8(util.Uint8Array_to_str(this.data)); // normalize EOL to \n - this.text = text.replace(/\r\n/g, '\n'); + this.text = util.nativeEOL(text); return this.text; }; @@ -92,7 +92,7 @@ Literal.prototype.getBytes = function() { } // normalize EOL to \r\n - const text = this.text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'); + const text = util.canonicalizeEOL(this.text); // encode UTF8 this.data = util.str_to_Uint8Array(util.encode_utf8(text)); return this.data; diff --git a/src/packet/signature.js b/src/packet/signature.js index 936a1520..943d0925 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -556,7 +556,7 @@ Signature.prototype.toSign = function (type, data) { case t.text: { let text = data.getText(); // normalize EOL to \r\n - text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n'); + text = util.canonicalizeEOL(text); // encode UTF8 return util.str_to_Uint8Array(util.encode_utf8(text)); } diff --git a/src/util.js b/src/util.js index 719d2bef..375dc887 100644 --- a/src/util.js +++ b/src/util.js @@ -574,5 +574,26 @@ export default { return false; } return /$/.test(data); + }, + + /** + * Normalize line endings to \r\n + */ + canonicalizeEOL: function(text) { + return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n"); + }, + + /** + * Convert line endings from canonicalized \r\n to native \n + */ + nativeEOL: function(text) { + return text.replace(/\r\n/g, "\n"); + }, + + /** + * Remove trailing spaces and tabs from each line + */ + removeTrailingSpaces: function(text) { + return text.replace(/[ \t]+$/mg, ""); } }; From 343c64eca031702e7905c53a274984a11318131b Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 13:53:07 +0200 Subject: [PATCH 33/51] Add tests for signing and verifying messages with trailing spaces --- test/general/openpgp.js | 24 +++++++++++ test/general/signature.js | 89 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/test/general/openpgp.js b/test/general/openpgp.js index b0e6f570..32a7cf72 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -858,6 +858,30 @@ describe('OpenPGP.js public api tests', function() { }); }); + it('roundtrip workflow: encrypt, decryptSessionKeys, decrypt with pgp key pair -- trailing spaces', function () { + const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t '; + let msgAsciiArmored; + return openpgp.encrypt({ + data: plaintext, + publicKeys: publicKey.keys + }).then(function (encrypted) { + msgAsciiArmored = encrypted.data; + return openpgp.decryptSessionKeys({ + message: openpgp.message.readArmored(msgAsciiArmored), + privateKeys: privateKey.keys[0] + }); + + }).then(function (decryptedSessionKeys) { + const message = openpgp.message.readArmored(msgAsciiArmored); + return openpgp.decrypt({ + sessionKeys: decryptedSessionKeys[0], + message + }); + }).then(function (decrypted) { + expect(decrypted.data).to.equal(plaintext); + }); + }); + it('roundtrip workflow: encrypt, decryptSessionKeys, decrypt with password', function () { let msgAsciiArmored; return openpgp.encrypt({ diff --git a/test/general/signature.js b/test/general/signature.js index e2765265..45d09779 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -580,6 +580,75 @@ describe("Signature", function() { }); }); + it('Verify cleartext signed message with trailing spaces from GPG', function() { + const msg_armor = + `-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA1 + +space: +space and tab: \t +no trailing space + +tab:\t +tab and space:\t +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1 + +iJwEAQECAAYFAlrZzCQACgkQ4IT3RGwgLJeWggP+Pb33ubbELIzg9/imM+zlR063 +g0FbG4B+RGZNFSbaDArUgh9fdVqBy8M9vvbbDMBalSiQxY09Lrasfb+tsomrygbN +NisuPRa5phPhn1bB4hZDb2ed/iK41CNyU7QHuv4AAvLC0mMamRnEg0FW2M2jZLGh +zmuVOdNuWQqxT9Sqa84= +=bqAR +-----END PGP SIGNATURE-----`; + + const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t '; + const csMsg = openpgp.cleartext.readArmored(msg_armor); + const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; + + const keyids = csMsg.getSigningKeyIds(); + + expect(pubKey.getKeyPackets(keyids[0])).to.not.be.empty; + + return openpgp.verify({ publicKeys:[pubKey], message:csMsg }).then(function(cleartextSig) { + expect(cleartextSig).to.exist; + expect(cleartextSig.data).to.equal(openpgp.util.removeTrailingSpaces(plaintext)); + expect(cleartextSig.signatures).to.have.length(1); + expect(cleartextSig.signatures[0].valid).to.be.true; + expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1); + }); + }); + + it('Verify signed message with trailing spaces from GPG', function() { + const msg_armor = + `-----BEGIN PGP MESSAGE----- +Version: GnuPG v1 + +owGbwMvMyMT4oOW7S46CznTG01El3MUFicmpxbolqcUlUTev14K5Vgq8XGCGQmJe +ikJJYpKVAicvV16+QklRYmZOZl66AliWl0sBqBAkzQmmwKohBnAqdMxhYWRkYmBj +ZQIZy8DFKQCztusM8z+Vt/svG80IS/etn90utv/T16jquk69zPvp6t9F16ryrwpb +kfVlS5Xl38KnVYxWvIor0nao6WUczA4vvZX9TXPWnnW3tt1vbZoiqWUjYjjjhuKG +4DtmMTuL3TW6/zNzVfWp/Q11+71O8RGnXMsBvWM6mSqX75uLiPo6HRaUDHnvrfCP +yYDnCgA= +=15ki +-----END PGP MESSAGE-----`; + + const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t '; + const sMsg = openpgp.message.readArmored(msg_armor); + const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; + + const keyids = sMsg.getSigningKeyIds(); + + expect(pubKey.getKeyPackets(keyids[0])).to.not.be.empty; + + return openpgp.verify({ publicKeys:[pubKey], message:sMsg }).then(function(cleartextSig) { + expect(cleartextSig).to.exist; + expect(openpgp.util.nativeEOL(openpgp.util.Uint8Array_to_str(cleartextSig.data))).to.equal(plaintext); + expect(cleartextSig.signatures).to.have.length(1); + expect(cleartextSig.signatures[0].valid).to.be.true; + expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1); + }); + }); + it('Sign text with openpgp.sign and verify with openpgp.verify leads to same string cleartext and valid signatures', async function() { const plaintext = 'short message\nnext line\n한국어/조선말'; const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; @@ -620,6 +689,26 @@ describe("Signature", function() { }); }); + it('Sign text with openpgp.sign and verify with openpgp.verify leads to same string cleartext and valid signatures -- trailing spaces', async function() { + const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t '; + const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; + const privKey = openpgp.key.readArmored(priv_key_arm2).keys[0]; + await privKey.primaryKey.decrypt('hello world'); + + return openpgp.sign({ privateKeys:[privKey], data:plaintext }).then(function(signed) { + + const csMsg = openpgp.cleartext.readArmored(signed.data); + return openpgp.verify({ publicKeys:[pubKey], message:csMsg }); + + }).then(function(cleartextSig) { + expect(cleartextSig).to.exist; + expect(cleartextSig.data).to.equal(openpgp.util.removeTrailingSpaces(plaintext)); + expect(cleartextSig.signatures).to.have.length(1); + expect(cleartextSig.signatures[0].valid).to.be.true; + expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1); + }); + }); + it('Sign text with openpgp.sign and verify with openpgp.verify leads to same bytes cleartext and valid signatures - armored', async function() { const plaintext = openpgp.util.str_to_Uint8Array('short message\nnext line\n한국어/조선말'); const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0]; From 485cb17e9526e75a7da5cd482885208b79ebf9c1 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 16:56:47 +0200 Subject: [PATCH 34/51] Deduplicate SymEncryptedAEADProtected encrypt / decrypt --- src/crypto/eax.js | 1 + src/crypto/gcm.js | 1 + src/crypto/ocb.js | 1 + src/packet/sym_encrypted_aead_protected.js | 82 ++++++++++------------ 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/crypto/eax.js b/src/crypto/eax.js index ca353b7c..c5bc9ea2 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -163,5 +163,6 @@ EAX.getNonce = function(iv, chunkIndex) { EAX.blockLength = blockLength; EAX.ivLength = ivLength; +EAX.tagLength = tagLength; export default EAX; diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index c7af046c..fe023deb 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -120,5 +120,6 @@ GCM.getNonce = function(iv, chunkIndex) { GCM.blockLength = blockLength; GCM.ivLength = ivLength; +GCM.tagLength = tagLength; export default GCM; diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index e0bd88f4..ad5acdb1 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -303,5 +303,6 @@ OCB.getNonce = function(iv, chunkIndex) { OCB.blockLength = blockLength; OCB.ivLength = ivLength; +OCB.tagLength = tagLength; export default OCB; diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 8068c89c..ede69d62 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -95,33 +95,12 @@ SymEncryptedAEADProtected.prototype.write = function () { SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) { const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; if (config.aead_protect_version === 4) { - 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 = []; - const modeInstance = await mode(cipher, key); - for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { - decryptedPromises.push( - modeInstance.decrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray) - ); - data = data.subarray(chunkSize); - adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) - } - decryptedPromises.push( - modeInstance.decrypt(authTag, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) - ); - this.packets.read(util.concatUint8Array(await Promise.all(decryptedPromises))); + const data = this.encrypted.subarray(0, -mode.tagLength); + const authTag = this.encrypted.subarray(-mode.tagLength); + this.packets.read(await this.crypt('decrypt', key, data, authTag)); } else { - const modeInstance = await mode(sessionKeyAlgorithm, key); - this.packets.read(await modeInstance.decrypt(this.encrypted, this.iv)); + this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); + this.packets.read(await this.crypt('decrypt', key, this.encrypted)); } return true; }; @@ -134,14 +113,30 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith * @async */ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { + this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); 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(); + this.chunkSizeByte = config.aead_chunk_size_byte; + const data = this.packets.write(); + this.encrypted = await this.crypt('encrypt', key, data, data.subarray(0, 0)); +}; + +/** + * En/decrypt the payload. + * @param {encrypt|decrypt} fn Whether to encrypt or decrypt + * @param {Uint8Array} key The session key used to en/decrypt the payload + * @param {Uint8Array} data The data to en/decrypt + * @param {Uint8Array} finalChunk For encryption: empty final chunk; for decryption: final authentication tag + * @returns {Promise} + * @async + */ +SymEncryptedAEADProtected.prototype.crypt = async function (fn, key, data, finalChunk) { + const cipher = enums.read(enums.symmetric, this.cipherAlgo); + const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; + const modeInstance = await mode(cipher, key); if (config.aead_protect_version === 4) { - this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); - this.chunkSizeByte = config.aead_chunk_size_byte; + const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0; const chunkSize = 2 ** (this.chunkSizeByte + 6); // ((uint64_t)1 << (c + 6)) const adataBuffer = new ArrayBuffer(21); const adataArray = new Uint8Array(adataBuffer, 0, 13); @@ -149,24 +144,25 @@ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorith 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 = []; + adataView.setInt32(13 + 4, data.length - tagLengthIfDecrypting); // Should be setInt64(13, ...) + const cryptedPromises = []; for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { - encryptedPromises.push( - modeInstance.encrypt(data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray) + cryptedPromises.push( + modeInstance[fn](data.subarray(0, chunkSize), 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. + // We take a chunk of data, en/decrypt it, and shift `data` to the + // next chunk. data = data.subarray(chunkSize); adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...) } - encryptedPromises.push( - modeInstance.encrypt(data, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) + // After the final chunk, we either encrypt a final, empty data + // chunk to get the final authentication tag or validate that final + // authentication tag. + cryptedPromises.push( + modeInstance[fn](finalChunk, mode.getNonce(this.iv, chunkIndexArray), adataTagArray) ); - this.encrypted = util.concatUint8Array(await Promise.all(encryptedPromises)); + return util.concatUint8Array(await Promise.all(cryptedPromises)); } else { - this.encrypted = await modeInstance.encrypt(data, this.iv); + return modeInstance[fn](data, this.iv); } - return true; -}; +} From 4568d080d5e2cada2771afce978d1dff80ce5a8c Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 17:28:15 +0200 Subject: [PATCH 35/51] Fix decryption with multiple chunks --- src/packet/sym_encrypted_aead_protected.js | 4 ++-- test/general/openpgp.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index ede69d62..43836977 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -137,14 +137,14 @@ SymEncryptedAEADProtected.prototype.crypt = async function (fn, key, data, final const modeInstance = await mode(cipher, key); if (config.aead_protect_version === 4) { const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0; - const chunkSize = 2 ** (this.chunkSizeByte + 6); // ((uint64_t)1 << (c + 6)) + const chunkSize = 2 ** (this.chunkSizeByte + 6) + tagLengthIfDecrypting; // ((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 - tagLengthIfDecrypting); // Should be setInt64(13, ...) + adataView.setInt32(13 + 4, data.length - tagLengthIfDecrypting * Math.ceil(data.length / chunkSize)); // Should be setInt64(13, ...) const cryptedPromises = []; for (let chunkIndex = 0; chunkIndex === 0 || data.length;) { cryptedPromises.push( diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 32a7cf72..d202a42c 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -599,6 +599,7 @@ describe('OpenPGP.js public api tests', function() { let aead_protectVal; let aead_protect_versionVal; let aead_modeVal; + let aead_chunk_size_byteVal; beforeEach(function(done) { publicKey = openpgp.key.readArmored(pub_key); @@ -625,6 +626,7 @@ describe('OpenPGP.js public api tests', function() { aead_protectVal = openpgp.config.aead_protect; aead_protect_versionVal = openpgp.config.aead_protect_version; aead_modeVal = openpgp.config.aead_mode; + aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte; done(); }); @@ -634,6 +636,7 @@ describe('OpenPGP.js public api tests', function() { openpgp.config.aead_protect = aead_protectVal; openpgp.config.aead_protect_version = aead_protect_versionVal; openpgp.config.aead_mode = aead_modeVal; + openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal; }); it('Decrypting key with wrong passphrase rejected', async function () { @@ -732,6 +735,21 @@ describe('OpenPGP.js public api tests', function() { } }); + tryTests('EAX mode (small chunk size)', tests, { + if: openpgp.util.getWebCryptoAll() || openpgp.util.getNodeCrypto(), + beforeEach: function() { + openpgp.config.use_native = true; + openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 4; + openpgp.config.aead_chunk_size_byte = 0; + + // 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('OCB mode', tests, { if: true, beforeEach: function() { From 0376f49e016e0351b20d10333d9cc229007356eb Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 18:09:54 +0200 Subject: [PATCH 36/51] Deduplicate getPreferredSymAlgo / getPreferredAEADAlgo --- src/key.js | 57 +++++++++++++++------------------------------ src/message.js | 9 ++++--- test/general/key.js | 32 ++++++++++++++----------- 3 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/key.js b/src/key.js index 65da1173..0d58a2a3 100644 --- a/src/key.js +++ b/src/key.js @@ -1436,31 +1436,34 @@ export async function getPreferredHashAlgo(key, date) { } /** - * Returns the preferred symmetric algorithm for a set of keys + * Returns the preferred symmetric/aead algorithm for a set of keys + * @param {symmetric|aead} type Type of preference to return * @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, date) { +export async function getPreferredAlgo(type, keys, date) { + const prefProperty = type === 'symmetric' ? 'preferredSymmetricAlgorithms' : 'preferredAeadAlgorithms'; + const defaultAlgo = type === 'symmetric' ? config.encryption_cipher : config.aead_mode; const prioMap = {}; await Promise.all(keys.map(async function(key) { const primaryUser = await key.getPrimaryUser(date); - if (!primaryUser || !primaryUser.selfCertification.preferredSymmetricAlgorithms) { - return config.encryption_cipher; + if (!primaryUser || !primaryUser.selfCertification[prefProperty]) { + return defaultAlgo; } - primaryUser.selfCertification.preferredSymmetricAlgorithms.forEach(function(algo, index) { + primaryUser.selfCertification[prefProperty].forEach(function(algo, index) { const entry = prioMap[algo] || (prioMap[algo] = { prio: 0, count: 0, algo: algo }); entry.prio += 64 >> index; entry.count++; }); })); - let prefAlgo = { prio: 0, algo: config.encryption_cipher }; + let prefAlgo = { prio: 0, algo: defaultAlgo }; for (const algo in prioMap) { try { - if (algo !== enums.symmetric.plaintext && - algo !== enums.symmetric.idea && // not implemented - enums.read(enums.symmetric, algo) && // known algorithm + if (algo !== enums[type].plaintext && + algo !== enums[type].idea && // not implemented + enums.read(enums[type], algo) && // known algorithm prioMap[algo].count === keys.length && // available for all keys prioMap[algo].prio > prefAlgo.prio) { prefAlgo = prioMap[algo]; @@ -1471,43 +1474,21 @@ export async function getPreferredSymAlgo(keys, date) { } /** - * Returns the preferred aead algorithm for a set of keys + * Returns whether aead is supported by all keys in the set * @param {Array} keys Set of keys * @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 + * @returns {Promise} * @async */ -export async function getPreferredAeadAlgo(keys, date) { - let supports_aead = true; - const prioMap = {}; +export async function isAeadSupported(keys, date) { + let supported = true; + // TODO replace when Promise.some or Promise.any are implemented await Promise.all(keys.map(async function(key) { const primaryUser = await key.getPrimaryUser(date); if (!primaryUser || !primaryUser.selfCertification.features || !(primaryUser.selfCertification.features[0] & enums.features.aead)) { - supports_aead = false; - return; + supported = false; } - if (!primaryUser || !primaryUser.selfCertification.preferredAeadAlgorithms) { - return config.aead_mode; - } - primaryUser.selfCertification.preferredAeadAlgorithms.forEach(function(algo, index) { - const entry = prioMap[algo] || (prioMap[algo] = { prio: 0, count: 0, algo: algo }); - entry.prio += 64 >> index; - entry.count++; - }); })); - if (!supports_aead) { - return null; - } - let prefAlgo = { prio: 0, algo: config.aead_mode }; - for (const algo in prioMap) { - try { - if (enums.read(enums.aead, algo) && // known algorithm - prioMap[algo].count === keys.length && // available for all keys - prioMap[algo].prio > prefAlgo.prio) { - prefAlgo = prioMap[algo]; - } - } catch (e) {} - } - return prefAlgo.algo; + return supported; } diff --git a/src/message.js b/src/message.js index ec7944ef..7ce2cb2f 100644 --- a/src/message.js +++ b/src/message.js @@ -36,7 +36,7 @@ import enums from './enums'; import util from './util'; import packet from './packet'; import { Signature } from './signature'; -import { getPreferredHashAlgo, getPreferredSymAlgo, getPreferredAeadAlgo } from './key'; +import { getPreferredHashAlgo, getPreferredAlgo, isAeadSupported } from './key'; /** @@ -263,10 +263,9 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard aeadAlgo = sessionKey.aeadAlgorithm; sessionKey = sessionKey.data; } else if (keys && keys.length) { - symAlgo = enums.read(enums.symmetric, await getPreferredSymAlgo(keys, date)); - aeadAlgo = await getPreferredAeadAlgo(keys, date); - if (aeadAlgo) { - aeadAlgo = enums.read(enums.aead, aeadAlgo); + symAlgo = enums.read(enums.symmetric, await getPreferredAlgo('symmetric', keys, date)); + if (config.aead_protect && config.aead_protect_version === 4 && await isAeadSupported(keys, date)) { + aeadAlgo = enums.read(enums.aead, await getPreferredAlgo('aead', keys, date)); } } else if (passwords && passwords.length) { symAlgo = enums.read(enums.symmetric, config.encryption_cipher); diff --git a/test/general/key.js b/test/general/key.js index ec21ac20..8354423e 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1170,42 +1170,44 @@ p92yZgB3r2+f6/GIe2+7 }); }); - it('getPreferredSymAlgo() - one key - AES256', async function() { + it("getPreferredAlgo('symmetric') - one key - AES256", async function() { const key1 = openpgp.key.readArmored(twoKeys).keys[0]; - const prefAlgo = await openpgp.key.getPreferredSymAlgo([key1]); + const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1]); expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes256); }); - it('getPreferredSymAlgo() - two key - AES128', async function() { + it("getPreferredAlgo('symmetric') - two key - AES128", async function() { const keys = openpgp.key.readArmored(twoKeys).keys; const key1 = keys[0]; const key2 = keys[1]; const primaryUser = await key2.getPrimaryUser(); primaryUser.selfCertification.preferredSymmetricAlgorithms = [6,7,3]; - const prefAlgo = await openpgp.key.getPreferredSymAlgo([key1, key2]); + const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1, key2]); expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes128); }); - it('getPreferredSymAlgo() - two key - one without pref', async function() { + it("getPreferredAlgo('symmetric') - two key - one without pref", async function() { const keys = openpgp.key.readArmored(twoKeys).keys; const key1 = keys[0]; const key2 = keys[1]; const primaryUser = await key2.getPrimaryUser(); primaryUser.selfCertification.preferredSymmetricAlgorithms = null; - const prefAlgo = await openpgp.key.getPreferredSymAlgo([key1, key2]); + const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1, key2]); expect(prefAlgo).to.equal(openpgp.config.encryption_cipher); }); - it('getPreferredAeadAlgo() - one key - OCB', async function() { + it("getPreferredAlgo('aead') - 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]); + const prefAlgo = await openpgp.key.getPreferredAlgo('aead', [key1]); expect(prefAlgo).to.equal(openpgp.enums.aead.ocb); + const supported = await openpgp.key.isAeadSupported([key1]); + expect(supported).to.be.true; }); - it('getPreferredAeadAlgo() - two key - one without pref', async function() { + it("getPreferredAlgo('aead') - two key - one without pref", async function() { const keys = openpgp.key.readArmored(twoKeys).keys; const key1 = keys[0]; const key2 = keys[1]; @@ -1214,19 +1216,23 @@ p92yZgB3r2+f6/GIe2+7 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]); + const prefAlgo = await openpgp.key.getPreferredAlgo('aead', [key1, key2]); expect(prefAlgo).to.equal(openpgp.config.aead_mode); + const supported = await openpgp.key.isAeadSupported([key1, key2]); + expect(supported).to.be.true; }); - it('getPreferredAeadAlgo() - two key - one with no support', async function() { + it("getPreferredAlgo('aead') - 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; + const prefAlgo = await openpgp.key.getPreferredAlgo('aead', [key1, key2]); + expect(prefAlgo).to.equal(openpgp.config.aead_mode); + const supported = await openpgp.key.isAeadSupported([key1, key2]); + expect(supported).to.be.false; }); it('Preferences of generated key', function() { From be62b0cf65b865c4198d5a6b0d16bd8f7be237a2 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 18:32:35 +0200 Subject: [PATCH 37/51] Add algorithm IDs for AEDH and AEDSA --- src/enums.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/enums.js b/src/enums.js index ae08ff49..5a3cc072 100644 --- a/src/enums.js +++ b/src/enums.js @@ -88,7 +88,7 @@ export default { gnu: 101 }, - /** {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC4880, section 9.1} + /** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-9.1|RFC4880bis-04, section 9.1} * @enum {Integer} * @readonly */ @@ -109,7 +109,11 @@ export default { ecdsa: 19, /** EdDSA (Sign only) * [{@link https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04|Draft RFC}] */ - eddsa: 22 + eddsa: 22, + /** Reserved for AEDH */ + aedh: 23, + /** Reserved for AEDSA */ + aedsa: 24 }, /** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2} From 310d8dd9b9fb9edb12fe94d8c218c8d3818a660d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 19:38:46 +0200 Subject: [PATCH 38/51] Fix V5 key fingerprint in ECDH parameters --- src/crypto/public_key/elliptic/ecdh.js | 4 +--- src/packet/public_key.js | 19 +++++++++++++------ .../public_key_encrypted_session_key.js | 4 ++-- test/crypto/elliptic.js | 8 ++++---- test/general/packet.js | 4 ++-- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 8cdc3509..ef256b36 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -46,7 +46,7 @@ function buildEcdhParam(public_algo, oid, cipher_algo, hash_algo, fingerprint) { new Uint8Array([public_algo]), kdf_params.write(), util.str_to_Uint8Array("Anonymous Sender "), - fingerprint + fingerprint.subarray(0, 20) ]); } @@ -73,7 +73,6 @@ function kdf(hash_algo, X, length, param) { * @async */ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) { - fingerprint = util.hex_to_Uint8Array(fingerprint); const curve = new Curve(oid); const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint); cipher_algo = enums.read(enums.symmetric, cipher_algo); @@ -102,7 +101,6 @@ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) { * @async */ async function decrypt(oid, cipher_algo, hash_algo, V, C, d, fingerprint) { - fingerprint = util.hex_to_Uint8Array(fingerprint); const curve = new Curve(oid); const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint); cipher_algo = enums.read(enums.symmetric, cipher_algo); diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 491d3ddd..939689c7 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -202,9 +202,9 @@ PublicKey.prototype.getKeyId = function () { /** * Calculates the fingerprint of the key - * @returns {String} A string containing the fingerprint in lowercase hex + * @returns {Uint8Array} A Uint8Array containing the fingerprint */ -PublicKey.prototype.getFingerprint = function () { +PublicKey.prototype.getFingerprintBytes = function () { if (this.fingerprint) { return this.fingerprint; } @@ -212,10 +212,10 @@ PublicKey.prototype.getFingerprint = function () { if (this.version === 5) { const bytes = this.writePublicKey(); toHash = util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]); - this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha256(toHash)); + this.fingerprint = crypto.hash.sha256(toHash); } else if (this.version === 4) { toHash = this.writeOld(); - this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha1(toHash)); + this.fingerprint = crypto.hash.sha1(toHash); } else if (this.version === 3) { const algo = enums.write(enums.publicKey, this.algorithm); const paramCount = crypto.getPubKeyParamTypes(algo).length; @@ -223,12 +223,19 @@ PublicKey.prototype.getFingerprint = function () { for (let i = 0; i < paramCount; i++) { toHash += this.params[i].toString(); } - this.fingerprint = util.Uint8Array_to_str(crypto.hash.md5(util.str_to_Uint8Array(toHash))); + this.fingerprint = crypto.hash.md5(util.str_to_Uint8Array(toHash)); } - this.fingerprint = util.str_to_hex(this.fingerprint); return this.fingerprint; }; +/** + * Calculates the fingerprint of the key + * @returns {String} A string containing the fingerprint in lowercase hex + */ +PublicKey.prototype.getFingerprint = function () { + return util.Uint8Array_to_hex(this.getFingerprintBytes()); +}; + /** * Returns algorithm information * @returns {Promise} An object of the form {algorithm: String, bits:int, curve:String} diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index f926d0ca..f70c166c 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -122,7 +122,7 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { } this.encrypted = await crypto.publicKeyEncrypt( - algo, key.params, toEncrypt, key.fingerprint); + algo, key.params, toEncrypt, key.getFingerprintBytes()); return true; }; @@ -138,7 +138,7 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) { const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); const result = await crypto.publicKeyDecrypt( - algo, key.params, this.encrypted, key.fingerprint); + algo, key.params, this.encrypted, key.getFingerprintBytes()); let checksum; let decoded; diff --git a/test/crypto/elliptic.js b/test/crypto/elliptic.js index e1156a63..ed41edc8 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/elliptic.js @@ -317,7 +317,7 @@ describe('Elliptic Curve Cryptography', function () { new Uint8Array(ephemeral), data, new Uint8Array(priv), - fingerprint + new Uint8Array(fingerprint) ); }); }; @@ -344,17 +344,17 @@ describe('Elliptic Curve Cryptography', function () { it('Invalid curve oid', function (done) { expect(decrypt_message( - '', 2, 7, [], [], [], '' + '', 2, 7, [], [], [], [] )).to.be.rejectedWith(Error, /Not valid curve/).notify(done); }); it('Invalid ephemeral key', function (done) { expect(decrypt_message( - 'secp256k1', 2, 7, [], [], [], '' + 'secp256k1', 2, 7, [], [], [], [] )).to.be.rejectedWith(Error, /Unknown point format/).notify(done); }); it('Invalid key data integrity', function (done) { expect(decrypt_message( - 'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_data, '' + 'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_data, [] )).to.be.rejectedWith(Error, /Key Data Integrity failed/).notify(done); }); }); diff --git a/test/general/packet.js b/test/general/packet.js index 32ba3b25..25e1e43e 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -269,13 +269,13 @@ describe("Packet", function() { enc.publicKeyAlgorithm = 'rsa_encrypt'; enc.sessionKeyAlgorithm = 'aes256'; enc.publicKeyId.bytes = '12345678'; - return enc.encrypt({ params: mpi }).then(() => { + return enc.encrypt({ params: mpi, getFingerprintBytes() {} }).then(() => { msg.push(enc); msg2.read(msg.write()); - return msg2[0].decrypt({ params: mpi }).then(() => { + return msg2[0].decrypt({ params: mpi, getFingerprintBytes() {} }).then(() => { expect(stringify(msg2[0].sessionKey)).to.equal(stringify(enc.sessionKey)); expect(msg2[0].sessionKeyAlgorithm).to.equal(enc.sessionKeyAlgorithm); From e8adeef2780d98511a128c889b7e8742c50134f9 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Apr 2018 20:26:24 +0200 Subject: [PATCH 39/51] Implement Issuer Fingerprint subpacket --- src/enums.js | 1 + src/packet/signature.js | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/enums.js b/src/enums.js index 5a3cc072..790685ec 100644 --- a/src/enums.js +++ b/src/enums.js @@ -373,6 +373,7 @@ export default { features: 30, signature_target: 31, embedded_signature: 32, + issuer_fingerprint: 33, preferred_aead_algorithms: 34 }, diff --git a/src/packet/signature.js b/src/packet/signature.js index 943d0925..355a3d29 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -84,6 +84,8 @@ function Signature(date=new Date()) { this.signatureTargetHashAlgorithm = null; this.signatureTargetHash = null; this.embeddedSignature = null; + this.issuerKeyVersion = null; + this.issuerFingerprint = null; this.preferredAeadAlgorithms = null; this.verified = null; @@ -223,6 +225,13 @@ Signature.prototype.sign = async function (key, data) { const arr = [new Uint8Array([4, signatureType, publicKeyAlgorithm, hashAlgorithm])]; + if (key.version === 5) { + // We could also generate this subpacket for version 4 keys, but for + // now we don't. + this.issuerKeyVersion = key.version; + this.issuerFingerprint = key.getFingerprintBytes(); + } + this.issuerKeyId = key.getKeyId(); // Add hashed subpackets @@ -293,7 +302,9 @@ Signature.prototype.write_all_sub_packets = function () { bytes = util.concatUint8Array([bytes, this.revocationKeyFingerprint]); arr.push(write_sub_packet(sub.revocation_key, bytes)); } - if (!this.issuerKeyId.isNull()) { + if (!this.issuerKeyId.isNull() && this.issuerKeyVersion !== 5) { + // If the version of [the] key is greater than 4, this subpacket + // MUST NOT be included in the signature. arr.push(write_sub_packet(sub.issuer, this.issuerKeyId.write())); } if (this.notation !== null) { @@ -356,6 +367,11 @@ Signature.prototype.write_all_sub_packets = function () { if (this.embeddedSignature !== null) { arr.push(write_sub_packet(sub.embedded_signature, this.embeddedSignature.write())); } + if (this.issuerFingerprint !== null) { + bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint]; + bytes = util.concatUint8Array(bytes); + arr.push(write_sub_packet(sub.issuer_fingerprint, bytes)); + } if (this.preferredAeadAlgorithms !== null) { bytes = util.str_to_Uint8Array(util.Uint8Array_to_str(this.preferredAeadAlgorithms)); arr.push(write_sub_packet(sub.preferred_aead_algorithms, bytes)); @@ -536,6 +552,16 @@ Signature.prototype.read_sub_packet = function (bytes) { this.embeddedSignature = new Signature(); this.embeddedSignature.read(bytes.subarray(mypos, bytes.length)); break; + case 33: + // Issuer Fingerprint + this.issuerKeyVersion = bytes[mypos++]; + this.issuerFingerprint = bytes.subarray(mypos, bytes.length); + if (this.issuerKeyVersion === 5) { + this.issuerKeyId.read(this.issuerFingerprint); + } else { + this.issuerKeyId.read(this.issuerFingerprint.subarray(-8)); + } + break; case 34: // Preferred AEAD Algorithms read_array.call(this, 'preferredAeadAlgorithms', bytes.subarray(mypos, bytes.length)); From bbf71d149b92ec787ae968dcc1f3860721b4707f Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Tue, 24 Apr 2018 13:22:53 +0200 Subject: [PATCH 40/51] Deduplicate OCB encrypt / decrypt --- src/crypto/ocb.js | 256 +++++++++------------ src/packet/sym_encrypted_aead_protected.js | 2 +- 2 files changed, 112 insertions(+), 146 deletions(-) diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index ad5acdb1..ef4c7830 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -66,30 +66,29 @@ const one = new Uint8Array([1]); async function OCB(cipher, key) { let maxNtz = 0; - let kv; + let encipher; + let decipher; + let mask; constructKeyVariables(cipher, key); function constructKeyVariables(cipher, key) { const aes = new ciphers[cipher](key); - const encipher = aes.encrypt.bind(aes); - const decipher = aes.decrypt.bind(aes); + encipher = aes.encrypt.bind(aes); + decipher = aes.decrypt.bind(aes); const mask_x = encipher(zeroBlock); const mask_$ = util.double(mask_x); - const mask = []; + mask = []; mask[0] = util.double(mask_$); mask.x = mask_x; mask.$ = mask_$; - - kv = { encipher, decipher, mask }; } function extendKeyVariables(text, adata) { - const { mask } = kv; - const newMaxNtz = util.nbits(Math.max(text.length, adata.length) >> 4) - 1; + const newMaxNtz = util.nbits(Math.max(text.length, adata.length) / blockLength | 0) - 1; for (let i = maxNtz + 1; i <= newMaxNtz; i++) { mask[i] = util.double(mask[i - 1]); } @@ -102,19 +101,17 @@ async function OCB(cipher, key) { return zeroBlock; } - const { encipher, mask } = kv; - // // Consider A as a sequence of 128-bit blocks // - const m = adata.length >> 4; + const m = adata.length / blockLength | 0; - const offset = new Uint8Array(16); - const sum = new Uint8Array(16); + const offset = new Uint8Array(blockLength); + const sum = new Uint8Array(blockLength); for (let i = 0; i < m; i++) { xorMut(offset, mask[ntz(i + 1)]); xorMut(sum, encipher(xor(offset, adata))); - adata = adata.subarray(16); + adata = adata.subarray(blockLength); } // @@ -123,7 +120,7 @@ async function OCB(cipher, key) { if (adata.length) { xorMut(offset, mask.x); - const cipherInput = new Uint8Array(16); + const cipherInput = new Uint8Array(blockLength); cipherInput.set(adata, 0); cipherInput[adata.length] = 0b10000000; xorMut(cipherInput, offset); @@ -134,6 +131,92 @@ async function OCB(cipher, key) { return sum; } + /** + * Encrypt/decrypt data. + * @param {encipher|decipher} fn Encryption/decryption block cipher function + * @param {Uint8Array} text The cleartext or ciphertext (without tag) input + * @param {Uint8Array} nonce The nonce (15 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext or plaintext output, with tag appended in both cases + */ + function crypt(fn, text, nonce, adata) { + // + // Consider P as a sequence of 128-bit blocks + // + const m = text.length / blockLength | 0; + + // + // Key-dependent variables + // + extendKeyVariables(text, adata); + + // + // Nonce-dependent and per-encryption variables + // + // Nonce = num2str(TAGLEN mod 128,7) || zeros(120-bitlen(N)) || 1 || N + // Note: We assume here that tagLength mod 16 == 0. + const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, ivLength - nonce.length), one, nonce]); + // bottom = str2num(Nonce[123..128]) + const bottom = paddedNonce[blockLength - 1] & 0b111111; + // Ktop = ENCIPHER(K, Nonce[1..122] || zeros(6)) + paddedNonce[blockLength - 1] &= 0b11000000; + const kTop = encipher(paddedNonce); + // Stretch = Ktop || (Ktop[1..64] xor Ktop[9..72]) + const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]); + // Offset_0 = Stretch[1+bottom..128+bottom] + const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); + // Checksum_0 = zeros(128) + const checksum = new Uint8Array(blockLength); + + const ct = new Uint8Array(text.length + tagLength); + + // + // Process any whole blocks + // + let i; + let pos = 0; + for (i = 0; i < m; i++) { + // Offset_i = Offset_{i-1} xor L_{ntz(i)} + xorMut(offset, mask[ntz(i + 1)]); + // C_i = Offset_i xor ENCIPHER(K, P_i xor Offset_i) + // P_i = Offset_i xor DECIPHER(K, C_i xor Offset_i) + ct.set(xorMut(fn(xor(offset, text)), offset), pos); + // Checksum_i = Checksum_{i-1} xor P_i + xorMut(checksum, fn === encipher ? text : ct.subarray(pos)); + + text = text.subarray(blockLength); + pos += blockLength; + } + + // + // Process any final partial block and compute raw tag + // + if (text.length) { + // Offset_* = Offset_m xor L_* + xorMut(offset, mask.x); + // Pad = ENCIPHER(K, Offset_*) + const padding = encipher(offset); + // C_* = P_* xor Pad[1..bitlen(P_*)] + ct.set(xor(text, padding), pos); + + // Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*))) + const xorInput = new Uint8Array(blockLength); + xorInput.set(fn === encipher ? text : ct.subarray(pos, -tagLength), 0); + xorInput[text.length] = 0b10000000; + xorMut(checksum, xorInput); + pos += text.length; + } + // Tag = ENCIPHER(K, Checksum_* xor Offset_* xor L_$) xor HASH(K,A) + const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata)); + + // + // Assemble ciphertext + // + // C = C_1 || C_2 || ... || C_m || C_* || Tag[1..TAGLEN] + ct.set(tag, pos); + return ct; + } + return { /** @@ -144,145 +227,28 @@ async function OCB(cipher, key) { * @returns {Promise} The ciphertext output */ encrypt: async function(plaintext, nonce, adata) { - // - // Consider P as a sequence of 128-bit blocks - // - const m = plaintext.length >> 4; - - // - // Key-dependent variables - // - extendKeyVariables(plaintext, adata); - const { encipher, mask } = kv; - - // - // Nonce-dependent and per-encryption variables - // - // We assume here that tagLength mod 16 == 0. - const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, 15 - nonce.length), one, nonce]); - const bottom = paddedNonce[15] & 0b111111; - paddedNonce[15] &= 0b11000000; - const kTop = encipher(paddedNonce); - const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]); - // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = new Uint8Array(16); - - const ct = new Uint8Array(plaintext.length + tagLength); - - // - // Process any whole blocks - // - let i; - let pos = 0; - for (i = 0; i < m; i++) { - xorMut(offset, mask[ntz(i + 1)]); - ct.set(xorMut(encipher(xor(offset, plaintext)), offset), pos); - xorMut(checksum, plaintext); - - plaintext = plaintext.subarray(16); - pos += 16; - } - - // - // Process any final partial block and compute raw tag - // - if (plaintext.length) { - xorMut(offset, mask.x); - const padding = encipher(offset); - ct.set(xor(plaintext, padding), pos); - - // Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*))) - const xorInput = new Uint8Array(16); - xorInput.set(plaintext, 0); - xorInput[plaintext.length] = 0b10000000; - xorMut(checksum, xorInput); - pos += plaintext.length; - } - const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata)); - - // - // Assemble ciphertext - // - ct.set(tag, pos); - return ct; + return crypt(encipher, plaintext, nonce, adata); }, - /** * Decrypt ciphertext input. - * @param {Uint8Array} ciphertext The ciphertext input to be decrypted - * @param {Uint8Array} nonce The nonce (15 bytes) - * @param {Uint8Array} adata Associated data to verify - * @returns {Promise} The plaintext output + * @param {Uint8Array} ciphertext The ciphertext input to be decrypted + * @param {Uint8Array} nonce The nonce (15 bytes) + * @param {Uint8Array} adata Associated data to sign + * @returns {Promise} The ciphertext output */ decrypt: async function(ciphertext, nonce, adata) { - // - // Consider C as a sequence of 128-bit blocks - // - const ctTag = ciphertext.subarray(ciphertext.length - tagLength); - ciphertext = ciphertext.subarray(0, ciphertext.length - tagLength); - const m = ciphertext.length >> 4; + if (ciphertext.length < tagLength) throw new Error('Invalid OCB ciphertext'); - // - // Key-dependent variables - // - extendKeyVariables(ciphertext, adata); - const { encipher, decipher, mask } = kv; + const tag = ciphertext.subarray(-tagLength); + ciphertext = ciphertext.subarray(0, -tagLength); - // - // Nonce-dependent and per-encryption variables - // - // We assume here that tagLength mod 16 == 0. - const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, 15 - nonce.length), one, nonce]); - const bottom = paddedNonce[15] & 0b111111; - paddedNonce[15] &= 0b11000000; - const kTop = encipher(paddedNonce); - const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]); - // Offset_0 = Stretch[1+bottom..128+bottom] - const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1); - const checksum = new Uint8Array(16); - - const pt = new Uint8Array(ciphertext.length); - - // - // Process any whole blocks - // - let i; - let pos = 0; - for (i = 0; i < m; i++) { - xorMut(offset, mask[ntz(i + 1)]); - pt.set(xorMut(decipher(xor(offset, ciphertext)), offset), pos); - xorMut(checksum, pt.subarray(pos)); - - ciphertext = ciphertext.subarray(16); - pos += 16; + const crypted = crypt(decipher, ciphertext, nonce, adata); + // if (Tag[1..TAGLEN] == T) + if (util.equalsUint8Array(tag, crypted.subarray(-tagLength))) { + return crypted.subarray(0, -tagLength); } - - // - // Process any final partial block and compute raw tag - // - if (ciphertext.length) { - xorMut(offset, mask.x); - const padding = encipher(offset); - pt.set(xor(ciphertext, padding), pos); - - // Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*))) - const xorInput = new Uint8Array(16); - xorInput.set(pt.subarray(pos), 0); - xorInput[ciphertext.length] = 0b10000000; - xorMut(checksum, xorInput); - pos += ciphertext.length; - } - const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata)); - - // - // Check for validity and assemble plaintext - // - if (!util.equalsUint8Array(ctTag, tag)) { - throw new Error('Authentication tag mismatch in OCB ciphertext'); - } - return pt; + throw new Error('Authentication tag mismatch in OCB ciphertext'); } }; } diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 43836977..1fe5e1ed 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -165,4 +165,4 @@ SymEncryptedAEADProtected.prototype.crypt = async function (fn, key, data, final } else { return modeInstance[fn](data, this.iv); } -} +}; From 04651e359ab780f599472b1c94eb1d7d15096a97 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 26 Apr 2018 16:52:49 +0200 Subject: [PATCH 41/51] Rename enums.aead.gcm to experimental_gcm So that (1) if the spec ever defines GCM differently than we do, we have a clean upgrade path and (2) it makes it clear that it's experimental. --- README.md | 2 +- src/crypto/index.js | 1 + src/enums.js | 2 +- src/openpgp.js | 2 +- src/packet/sym_encrypted_aead_protected.js | 4 ++-- test/general/openpgp.js | 4 ++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 973b7884..699b1f7a 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch ``` openpgp.config.aead_mode = openpgp.enums.aead.eax // Default, native openpgp.config.aead_mode = openpgp.enums.aead.ocb // Non-native - openpgp.config.aead_mode = openpgp.enums.aead.gcm // **Non-standard**, fastest + openpgp.config.aead_mode = openpgp.enums.aead.experimental_gcm // **Non-standard**, fastest ``` We previously also implemented an [earlier version](https://tools.ietf.org/html/draft-ford-openpgp-format-00) of the draft (using GCM), which you could enable by simply setting `openpgp.config.aead_protect = true`. If you need to stay compatible with that version, don't set `openpgp.config.aead_protect_version = 4`. diff --git a/src/crypto/index.js b/src/crypto/index.js index 0626df39..d70f36bb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -33,6 +33,7 @@ const mod = { cfb: cfb, /** @see module:crypto/gcm */ gcm: gcm, + experimental_gcm: gcm, /** @see module:crypto/eax */ eax: eax, /** @see module:crypto/ocb */ diff --git a/src/enums.js b/src/enums.js index 790685ec..b4ca9d5e 100644 --- a/src/enums.js +++ b/src/enums.js @@ -178,7 +178,7 @@ export default { aead: { eax: 1, ocb: 2, - gcm: 100 // Private algorithm + experimental_gcm: 100 // Private algorithm }, /** A list of packet types and numeric tags associated with them. diff --git a/src/openpgp.js b/src/openpgp.js index 1ad5fdf1..f18ba753 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -591,7 +591,7 @@ function onError(message, error) { */ function nativeAEAD() { return config.aead_protect && ( - ((config.aead_protect_version !== 4 || config.aead_mode === enums.aead.gcm) && util.getWebCrypto()) || + ((config.aead_protect_version !== 4 || config.aead_mode === enums.aead.experimental_gcm) && util.getWebCrypto()) || (config.aead_protect_version === 4 && config.aead_mode === enums.aead.eax && util.getWebCryptoAll()) ); } diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 1fe5e1ed..41190bbd 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -66,7 +66,7 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) { this.aeadAlgo = bytes[offset++]; this.chunkSizeByte = bytes[offset++]; } else { - this.aeadAlgo = enums.aead.gcm; + this.aeadAlgo = enums.aead.experimental_gcm; } const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; this.iv = bytes.subarray(offset, mode.ivLength + offset); @@ -114,7 +114,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith */ SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); - this.aeadAlgo = config.aead_protect_version === 4 ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.gcm; + this.aeadAlgo = config.aead_protect_version === 4 ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.experimental_gcm; const mode = crypto[enums.read(enums.aead, this.aeadAlgo)]; this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV this.chunkSizeByte = config.aead_chunk_size_byte; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index d202a42c..35dd0039 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -683,7 +683,7 @@ describe('OpenPGP.js public api tests', 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; + openpgp.config.aead_mode = openpgp.enums.aead.experimental_gcm; // Monkey-patch AEAD feature flag publicKey.keys[0].users[0].selfCertifications[0].features = [7]; @@ -698,7 +698,7 @@ describe('OpenPGP.js public api tests', 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; + openpgp.config.aead_mode = openpgp.enums.aead.experimental_gcm; // Monkey-patch AEAD feature flag publicKey.keys[0].users[0].selfCertifications[0].features = [7]; From 7ce3f5521fd939776912a4e0147083721a55956f Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 26 Apr 2018 16:50:23 +0200 Subject: [PATCH 42/51] Set default draft version to 4 --- src/config/config.js | 2 +- test/general/openpgp.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/config/config.js b/src/config/config.js index 8950bc9e..404e7298 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -59,7 +59,7 @@ export default { * @memberof module:config * @property {Integer} aead_protect_version */ - aead_protect_version: 0, + aead_protect_version: 4, /** * Default Authenticated Encryption with Additional Data (AEAD) encryption mode * Only has an effect when aead_protect is set to true. diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 35dd0039..c0974afe 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -674,6 +674,7 @@ describe('OpenPGP.js public api tests', function() { beforeEach: function() { openpgp.config.use_native = true; openpgp.config.aead_protect = true; + openpgp.config.aead_protect_version = 0; } }); @@ -682,7 +683,6 @@ describe('OpenPGP.js public api tests', function() { 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.experimental_gcm; // Monkey-patch AEAD feature flag @@ -697,7 +697,6 @@ describe('OpenPGP.js public api tests', function() { 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.experimental_gcm; // Monkey-patch AEAD feature flag @@ -712,7 +711,6 @@ describe('OpenPGP.js public api tests', function() { beforeEach: 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]; @@ -726,7 +724,6 @@ describe('OpenPGP.js public api tests', function() { beforeEach: 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]; @@ -740,7 +737,6 @@ describe('OpenPGP.js public api tests', function() { beforeEach: function() { openpgp.config.use_native = true; openpgp.config.aead_protect = true; - openpgp.config.aead_protect_version = 4; openpgp.config.aead_chunk_size_byte = 0; // Monkey-patch AEAD feature flag @@ -754,7 +750,6 @@ describe('OpenPGP.js public api tests', function() { if: true, beforeEach: 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 From 48cbb97d199b00b09757db45c0ce20341951de1e Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Sun, 29 Apr 2018 15:03:46 +0200 Subject: [PATCH 43/51] Bump Sauce Labs timeout --- Gruntfile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2714535f..af6a0ba0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -229,8 +229,8 @@ module.exports = function(grunt) { public: "public", maxRetries: 3, throttled: 2, - pollInterval: 4000, - 'max-duration': 360, + pollInterval: 10000, + sauceConfig: {maxDuration: 1800, commandTimeout: 600, idleTimeout: 1000}, statusCheckAttempts: 200 } } From b8191388cdc8ce407b59a01dabd015eeeac7429d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Sun, 29 Apr 2018 15:05:14 +0200 Subject: [PATCH 44/51] Bump "old Chrome" version from 38 to 41 --- travis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travis.sh b/travis.sh index cc1334f9..6c743961 100755 --- a/travis.sh +++ b/travis.sh @@ -17,7 +17,7 @@ elif [[ $OPENPGPJSTEST =~ ^end2end-.* ]]; then declare -a capabilities=( "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Firefox\", \"version\":\"34\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'" "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Firefox\", \"version\":\"54\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'" - "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Chrome\", \"version\":\"38\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\", \"extendedDebugging\":true}'" + "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Chrome\", \"version\":\"41\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\", \"extendedDebugging\":true}'" "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Chrome\", \"version\":\"59\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\", \"extendedDebugging\":true}'" "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Internet Explorer\", \"version\":\"11.103\", \"platform\":\"Windows 10\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'" "export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"MicrosoftEdge\", \"version\":\"15.15063\", \"platform\":\"Windows 10\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'" From 550b758d574e977f51bda865db7bb345e3849fb1 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Sun, 29 Apr 2018 19:07:52 +0200 Subject: [PATCH 45/51] Fall back to asm for CTR and CBC in old Safari --- src/crypto/cmac.js | 4 ++-- src/crypto/eax.js | 4 ++-- src/openpgp.js | 6 +++--- test/general/openpgp.js | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/crypto/cmac.js b/src/crypto/cmac.js index 060b2efb..de0b9146 100644 --- a/src/crypto/cmac.js +++ b/src/crypto/cmac.js @@ -9,7 +9,7 @@ import { AES_CBC } from 'asmcrypto.js/src/aes/cbc/exports'; import util from '../util'; -const webCrypto = util.getWebCryptoAll(); +const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); @@ -75,7 +75,7 @@ export default async function CMAC(key) { } async function CBC(key) { - if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']); return async function(pt) { const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeroBlock, length: blockLength * 8 }, key, pt); diff --git a/src/crypto/eax.js b/src/crypto/eax.js index c5bc9ea2..d2b578a1 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -28,7 +28,7 @@ import { AES_CTR } from 'asmcrypto.js/src/aes/ctr/exports'; import CMAC from './cmac'; import util from '../util'; -const webCrypto = util.getWebCryptoAll(); +const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); @@ -49,7 +49,7 @@ async function OMAC(key) { } async function CTR(key) { - if (util.getWebCryptoAll() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support key = await webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']); return async function(pt, iv) { const ct = await webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, key, pt); diff --git a/src/openpgp.js b/src/openpgp.js index f18ba753..ab3579d2 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -585,13 +585,13 @@ function onError(message, error) { /** * Check for native AEAD support and configuration by the user. Only * browsers that implement the current WebCrypto specification support - * native GCM. Native EAX is built on CTR and CBC, which all browsers - * support. OCB and CFB are not natively supported. + * native GCM. Native EAX is built on CTR and CBC, which current + * browsers support. OCB and CFB are not natively supported. * @returns {Boolean} If authenticated encryption should be used */ function nativeAEAD() { return config.aead_protect && ( ((config.aead_protect_version !== 4 || config.aead_mode === enums.aead.experimental_gcm) && util.getWebCrypto()) || - (config.aead_protect_version === 4 && config.aead_mode === enums.aead.eax && util.getWebCryptoAll()) + (config.aead_protect_version === 4 && config.aead_mode === enums.aead.eax && util.getWebCrypto()) ); } diff --git a/test/general/openpgp.js b/test/general/openpgp.js index c0974afe..4eada9bd 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -720,7 +720,7 @@ describe('OpenPGP.js public api tests', function() { }); tryTests('EAX mode (native)', tests, { - if: openpgp.util.getWebCryptoAll() || openpgp.util.getNodeCrypto(), + if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), beforeEach: function() { openpgp.config.use_native = true; openpgp.config.aead_protect = true; @@ -733,7 +733,7 @@ describe('OpenPGP.js public api tests', function() { }); tryTests('EAX mode (small chunk size)', tests, { - if: openpgp.util.getWebCryptoAll() || openpgp.util.getNodeCrypto(), + if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), beforeEach: function() { openpgp.config.use_native = true; openpgp.config.aead_protect = true; From cc1f7a47652a5b0b0581c94ae6a122c74fa2356e Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Sun, 29 Apr 2018 22:24:06 +0200 Subject: [PATCH 46/51] Lower chunk_size_byte to 12 (256KiB) - In anticipation of streaming decryption - Firefox 34 does not support chunk_size_byte > 24 256KiB is almost as fast as no chunks (although both of those can be up to ~1.5x slower than optimally using threads for very large message sizes). The optimal chunk size would be something like: max(data.length / navigator.hardwareConcurrency, 128KiB) But we don't do so currently because - We don't know the hardwareConcurrency of the decrypting machine - Smaller chunk sizes are better for streaming decryption --- src/config/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.js b/src/config/config.js index 404e7298..bb644fa4 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -75,7 +75,7 @@ export default { * @memberof module:config * @property {Integer} aead_chunk_size_byte */ - aead_chunk_size_byte: 46, + aead_chunk_size_byte: 12, /** * {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3|RFC4880 3.7.1.3}: * Iteration Count Byte for S2K (String to Key) From a7fce274241c3fb2ad6d624541e64f2776f31f6a Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 30 Apr 2018 13:46:13 +0200 Subject: [PATCH 47/51] Safari 8 compatibility --- src/crypto/eax.js | 2 +- src/crypto/ocb.js | 2 +- src/packet/secret_key.js | 2 +- test/crypto/eax.js | 2 +- test/crypto/ocb.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/crypto/eax.js b/src/crypto/eax.js index d2b578a1..acad0d67 100644 --- a/src/crypto/eax.js +++ b/src/crypto/eax.js @@ -140,7 +140,7 @@ async function EAX(cipher, key) { for (let i = 0; i < tagLength; i++) { tag[i] ^= omacAdata[i] ^ omacNonce[i]; } - if (!util.equalsUint8Array(ctTag, tag)) throw new Error('Authentication tag mismatch in EAX ciphertext'); + if (!util.equalsUint8Array(ctTag, tag)) throw new Error('Authentication tag mismatch'); const plaintext = await ctr(ciphered, omacNonce); return plaintext; } diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index ef4c7830..62a2b6d5 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -248,7 +248,7 @@ async function OCB(cipher, key) { if (util.equalsUint8Array(tag, crypted.subarray(-tagLength))) { return crypted.subarray(0, -tagLength); } - throw new Error('Authentication tag mismatch in OCB ciphertext'); + throw new Error('Authentication tag mismatch'); } }; } diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 491d60ae..dd9eb56c 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -309,7 +309,7 @@ SecretKey.prototype.decrypt = async function (passphrase) { const modeInstance = await mode(symmetric, key); cleartext = await modeInstance.decrypt(ciphertext, iv.subarray(0, mode.ivLength), new Uint8Array()); } catch(err) { - if (err.message.startsWith('Authentication tag mismatch')) { + if (err.message === 'Authentication tag mismatch') { throw new Error('Incorrect key passphrase: ' + err.message); } } diff --git a/test/crypto/eax.js b/test/crypto/eax.js index 3158428a..6b4191ed 100644 --- a/test/crypto/eax.js +++ b/test/crypto/eax.js @@ -108,7 +108,7 @@ function testAESEAX() { ct = await eax.encrypt(msgBytes, nonceBytes, headerBytes); ct[2] ^= 8; pt = eax.decrypt(ct, nonceBytes, headerBytes); - await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in EAX ciphertext') + await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch') // testing without additional data ct = await eax.encrypt(msgBytes, nonceBytes, new Uint8Array()); diff --git a/test/crypto/ocb.js b/test/crypto/ocb.js index 290e8b88..5983434c 100644 --- a/test/crypto/ocb.js +++ b/test/crypto/ocb.js @@ -136,7 +136,7 @@ describe('Symmetric AES-OCB', function() { ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes); ct[2] ^= 8; pt = ocb.decrypt(ct, nonceBytes, headerBytes); - await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch in OCB ciphertext') + await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch') // testing without additional data ct = await ocb.encrypt(msgBytes, nonceBytes, new Uint8Array()); From 8ec01ae07ae8dde1ab3e2bc834e981dffcf4a3ec Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 30 Apr 2018 14:33:09 +0200 Subject: [PATCH 48/51] Reduce duplicate tests --- test/general/openpgp.js | 57 +++++------------------------------------ 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 4eada9bd..3f6e8ed3 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -648,9 +648,8 @@ describe('OpenPGP.js public api tests', function() { }); tryTests('CFB mode (asm.js)', tests, { - if: true, + if: !(typeof window !== 'undefined' && window.Worker), beforeEach: function() { - openpgp.config.use_native = true; openpgp.config.aead_protect = false; } }); @@ -661,7 +660,6 @@ describe('OpenPGP.js public api tests', function() { openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); }, beforeEach: function() { - openpgp.config.use_native = true; openpgp.config.aead_protect = false; }, after: function() { @@ -669,61 +667,19 @@ describe('OpenPGP.js public api tests', function() { } }); - tryTests('GCM mode (native)', tests, { - if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), + tryTests('GCM mode', tests, { + if: true, beforeEach: function() { - openpgp.config.use_native = true; openpgp.config.aead_protect = true; openpgp.config.aead_protect_version = 0; } }); - 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_mode = openpgp.enums.aead.experimental_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_mode = openpgp.enums.aead.experimental_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, { + tryTests('GCM mode (draft04)', tests, { if: true, beforeEach: function() { - openpgp.config.use_native = false; - openpgp.config.aead_protect = true; - - // 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 (native)', tests, { - if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), - beforeEach: function() { - openpgp.config.use_native = true; openpgp.config.aead_protect = true; + openpgp.config.aead_mode = openpgp.enums.aead.experimental_gcm; // Monkey-patch AEAD feature flag publicKey.keys[0].users[0].selfCertifications[0].features = [7]; @@ -733,9 +689,8 @@ describe('OpenPGP.js public api tests', function() { }); tryTests('EAX mode (small chunk size)', tests, { - if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), + if: true, beforeEach: function() { - openpgp.config.use_native = true; openpgp.config.aead_protect = true; openpgp.config.aead_chunk_size_byte = 0; From 49c9fb193d07f3376ecea262e798e0eef60b9b7f Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 30 Apr 2018 14:40:59 +0200 Subject: [PATCH 49/51] Only call webCrypto.generateKey once in tests --- test/general/key.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/general/key.js b/test/general/key.js index 8354423e..3003b8f1 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1,11 +1,34 @@ 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')); const { expect } = chai; describe('Key', function() { + let webCrypto = openpgp.util.getWebCryptoAll(); + + if (webCrypto) { + let generateKey = webCrypto.generateKey; + let keyGenStub; + let keyGenValue; + + beforeEach(function() { + keyGenStub = stub(webCrypto, 'generateKey'); + keyGenStub.callsFake(function() { + if (!keyGenValue) { + keyGenValue = generateKey.apply(webCrypto, arguments); + } + return keyGenValue; + }); + }); + + afterEach(function() { + keyGenStub.restore(); + }); + } + describe('V4', tests); describe('V5', function() { From 2627755b496b6f70711cb6ea7c301582ac67ae6e Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 30 Apr 2018 16:02:16 +0200 Subject: [PATCH 50/51] iOS Safari doesn't allow setting Error.message --- src/openpgp.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/openpgp.js b/src/openpgp.js index ab3579d2..9f0a690e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -577,7 +577,9 @@ function onError(message, error) { util.print_debug_error(error); // update error message - error.message = message + ': ' + error.message; + try { + error.message = message + ': ' + error.message; + } catch(e) {} throw error; } From a16d1a6a1df850a750b0e3c4a032d77769515a79 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Mon, 30 Apr 2018 18:40:52 +0200 Subject: [PATCH 51/51] iOS does not support GCM-en/decrypting empty messages --- src/crypto/gcm.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index fe023deb..97b7aed3 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -48,16 +48,26 @@ async function GCM(cipher, key) { } if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support - key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']); + const _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); + if (!pt.length) { + // iOS does not support GCM-en/decrypting empty messages + // Also, synchronous en/decryption might be faster in this case. + return AES_GCM.encrypt(pt, key, iv, adata); + } + 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); + if (ct.length === tagLength) { + // iOS does not support GCM-en/decrypting empty messages + // Also, synchronous en/decryption might be faster in this case. + return AES_GCM.decrypt(ct, key, iv, adata); + } + const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata }, _key, ct); return new Uint8Array(pt); } };