From ded8926b27aeba8cbf10c5c9521f110bfac3cdbc Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 02:41:10 +0800 Subject: [PATCH 01/30] Support AES-GCM with AEAD Protected Data Packets Closes openpgpjs/openpgpjs#421 --- src/config/config.js | 1 + src/crypto/gcm.js | 105 +++++++++++++++++++++ src/crypto/index.js | 3 + src/crypto/random.js | 1 + src/enums.js | 3 +- src/message.js | 35 ++++--- src/openpgp.js | 22 ++--- src/packet/all_packets.js | 2 + src/packet/sym_encrypted_aead_protected.js | 66 +++++++++++++ 9 files changed, 213 insertions(+), 25 deletions(-) create mode 100644 src/crypto/gcm.js create mode 100644 src/packet/sym_encrypted_aead_protected.js diff --git a/src/config/config.js b/src/config/config.js index 78c97647..4cd0a284 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -37,6 +37,7 @@ export default { prefer_hash_algorithm: enums.hash.sha256, encryption_cipher: enums.symmetric.aes256, compression: enums.compression.zip, + aead_protect: true, // use Authenticated Encryption with Additional Data (AEAD) protection for symmetric encryption integrity_protect: true, // use integrity protection for symmetric encryption ignore_mdc_error: false, // fail on decrypt if message is not integrity protected rsa_blinding: true, diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js new file mode 100644 index 00000000..29a927ea --- /dev/null +++ b/src/crypto/gcm.js @@ -0,0 +1,105 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2016 Tankred Hase +// +// 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 wraps native AES-GCM en/decryption for both + * the WebCrypto api as well as node.js' crypto api. + */ + +'use strict'; + +import util from '../util.js'; +import asmCrypto from 'asmcrypto-lite'; +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); +const Buffer = util.getNodeBuffer(); + +export const ivLength = 12; + +/** + * Encrypt plaintext input. + * @param {String} cipher The symmetric cipher algorithm to use + * @param {Uint8Array} plaintext The cleartext input to be encrypted + * @param {Uint8Array} key The encryption key + * @param {Uint8Array} iv The initialization vector (12 bytes) + * @return {Promise} The ciphertext output + */ +export function encrypt(cipher, plaintext, key, iv) { + if (cipher.substr(0,3) !== 'aes') { + return Promise.reject(new Error('Invalid cipher for GCM mode')); + } + + if (webCrypto) { // native WebCrypto api + const keyOptions = { + name: 'AES-GCM' + }, + encryptOptions = { + name: 'AES-GCM', + iv: iv + }; + return webCrypto.importKey('raw', key, keyOptions, false, ['encrypt']).then(keyObj => { + return webCrypto.encrypt(encryptOptions, keyObj, plaintext); + }).then(ciphertext => { + return new Uint8Array(ciphertext); + }); + + } else if(nodeCrypto) { // native node crypto library + let cipherObj = new nodeCrypto.createCipheriv('aes-' + cipher.substr(3,3) + '-gcm', new Buffer(key), new Buffer(iv)); + let encrypted = Buffer.concat([cipherObj.update(new Buffer(plaintext)), cipherObj.final()]); + return Promise.resolve(new Uint8Array(encrypted)); + + } else { // asm.js fallback + return Promise.resolve(asmCrypto.AES_GCM.encrypt(plaintext, key, iv)); + } +} + +/** + * Decrypt ciphertext input + * @param {String} cipher The symmetric cipher algorithm to use + * @param {Uint8Array} ciphertext The ciphertext input to be decrypted + * @param {Uint8Array} key The encryption key + * @param {Uint8Array} iv The initialization vector (12 bytes) + * @return {Promise} The plaintext output + */ +export function decrypt(cipher, ciphertext, key, iv) { + if (cipher.substr(0,3) !== 'aes') { + return Promise.reject(new Error('Invalid cipher for GCM mode')); + } + + if (webCrypto) { // native WebCrypto api + const keyOptions = { + name: 'AES-GCM' + }, + decryptOptions = { + name: 'AES-GCM', + iv: iv + }; + return webCrypto.importKey('raw', key, keyOptions, false, ['decrypt']).then(keyObj => { + return webCrypto.decrypt(decryptOptions, keyObj, ciphertext); + }).then(plaintext => { + return new Uint8Array(plaintext); + }); + + } else if(nodeCrypto) { // native node crypto library + let decipherObj = new nodeCrypto.createDecipheriv('aes-' + cipher.substr(3,3) + '-gcm', new Buffer(key), new Buffer(iv)); + let decrypted = Buffer.concat([decipherObj.update(new Buffer(ciphertext)), decipherObj.final()]); + return Promise.resolve(new Uint8Array(decrypted)); + + } else { // asm.js fallback + return Promise.resolve(asmCrypto.AES_GCM.decrypt(ciphertext, key, iv)); + } +} \ No newline at end of file diff --git a/src/crypto/index.js b/src/crypto/index.js index af3dfeb1..8888a4e5 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -8,6 +8,7 @@ import cipher from './cipher'; import hash from './hash'; import cfb from './cfb'; +import * as gcm from './gcm'; import publicKey from './public_key'; import signature from './signature'; import random from './random'; @@ -21,6 +22,8 @@ const mod = { hash: hash, /** @see module:crypto/cfb */ cfb: cfb, + /** @see module:crypto/aes-gcm */ + gcm: gcm, /** @see module:crypto/public_key */ publicKey: publicKey, /** @see module:crypto/signature */ diff --git a/src/crypto/random.js b/src/crypto/random.js index 6023382b..4e2331a4 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -91,6 +91,7 @@ export default { } else { throw new Error('No secure random number generator available.'); } + return buf; }, /** diff --git a/src/enums.js b/src/enums.js index 2b08d348..baf7b158 100644 --- a/src/enums.js +++ b/src/enums.js @@ -94,7 +94,8 @@ export default { publicSubkey: 14, userAttribute: 17, symEncryptedIntegrityProtected: 18, - modificationDetectionCode: 19 + modificationDetectionCode: 19, + symEncryptedAEADProtected: 20 // see IETF draft: https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.2.1 }, /** Data types in the literal packet diff --git a/src/message.js b/src/message.js index 10115ad8..cc427f46 100644 --- a/src/message.js +++ b/src/message.js @@ -96,15 +96,23 @@ Message.prototype.decrypt = function(privateKey, sessionKey, password) { if (!keyObj || !util.isUint8Array(keyObj.data) || !util.isString(keyObj.algorithm)) { throw new Error('Invalid session key for decryption.'); } - var symEncryptedPacketlist = this.packets.filterByTag(enums.packet.symmetricallyEncrypted, enums.packet.symEncryptedIntegrityProtected); + + var symEncryptedPacketlist = this.packets.filterByTag( + enums.packet.symmetricallyEncrypted, + enums.packet.symEncryptedIntegrityProtected, + enums.packet.symEncryptedAEADProtected + ); + if (symEncryptedPacketlist.length !== 0) { var symEncryptedPacket = symEncryptedPacketlist[0]; - symEncryptedPacket.decrypt(keyObj.algorithm, keyObj.data); - var resultMsg = new Message(symEncryptedPacket.packets); - // remove packets after decryption - symEncryptedPacket.packets = new packet.List(); - return resultMsg; + return symEncryptedPacket.decrypt(keyObj.algorithm, keyObj.data).then(() => { + var resultMsg = new Message(symEncryptedPacket.packets); + // remove packets after decryption + symEncryptedPacket.packets = new packet.List(); + return resultMsg; + }); } + return Promise.resolve(); }; /** @@ -219,18 +227,21 @@ Message.prototype.encrypt = function(keys, passwords) { var packetlist = msg.packets; var symEncryptedPacket; - if (config.integrity_protect) { + if (config.aead_protect) { + symEncryptedPacket = new packet.SymEncryptedAEADProtected(); + } else if (config.integrity_protect) { symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); } else { symEncryptedPacket = new packet.SymmetricallyEncrypted(); } symEncryptedPacket.packets = this.packets; - symEncryptedPacket.encrypt(enums.read(enums.symmetric, symAlgo), sessionKey); - packetlist.push(symEncryptedPacket); - // remove packets after encryption - symEncryptedPacket.packets = new packet.List(); - return msg; + return symEncryptedPacket.encrypt(enums.read(enums.symmetric, symAlgo), sessionKey).then(() => { + packetlist.push(symEncryptedPacket); + // remove packets after encryption + symEncryptedPacket.packets = new packet.List(); + return msg; + }); }; /** diff --git a/src/openpgp.js b/src/openpgp.js index 0cf3f403..6cf458dd 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -169,17 +169,16 @@ export function decryptKey({ privateKey, passphrase }) { export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true }) { checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); - if (asyncProxy) { // use web worker if available + if (!util.getWebCrypto() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); } - return execute(() => { + let message = createMessage(data, filename); + if (privateKeys) { // sign the message only if private keys are specified + message = message.sign(privateKeys); + } - let message = createMessage(data, filename); - if (privateKeys) { // sign the message only if private keys are specified - message = message.sign(privateKeys); - } - message = message.encrypt(publicKeys, passwords); + return message.encrypt(publicKeys, passwords).then(message => { if(armor) { return { @@ -190,7 +189,7 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar message: message }; - }, 'Error encrypting message'); + }).catch(onError.bind(null, 'Error encrypting message')); } /** @@ -209,20 +208,19 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8' }) { checkMessage(message); publicKeys = toArray(publicKeys); - if (asyncProxy) { // use web worker if available + if (!util.getWebCrypto() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format }); } - return execute(() => { + return message.decrypt(privateKey, sessionKey, password).then(message => { - message = message.decrypt(privateKey, sessionKey, password); const result = parseMessage(message, format); if (publicKeys && result.data) { // verify only if publicKeys are specified result.signatures = message.verify(publicKeys); } return result; - }, 'Error decrypting message'); + }).catch(onError.bind(null, 'Error decrypting message')); } diff --git a/src/packet/all_packets.js b/src/packet/all_packets.js index aa63a27e..077026c8 100644 --- a/src/packet/all_packets.js +++ b/src/packet/all_packets.js @@ -12,6 +12,8 @@ import * as packets from './all_packets.js'; // re-import module to parse packet export { default as Compressed } from './compressed.js'; /** @see module:packet/sym_encrypted_integrity_protected */ export { default as SymEncryptedIntegrityProtected } from './sym_encrypted_integrity_protected.js'; +/** @see module:packet/sym_encrypted_aead_protected */ +export { default as SymEncryptedAEADProtected } from './sym_encrypted_aead_protected.js'; /** @see module:packet/public_key_encrypted_session_key */ export { default as PublicKeyEncryptedSessionKey } from './public_key_encrypted_session_key.js'; /** @see module:packet/sym_encrypted_session_key */ diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js new file mode 100644 index 00000000..eb42ffe7 --- /dev/null +++ b/src/packet/sym_encrypted_aead_protected.js @@ -0,0 +1,66 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2016 Tankred Hase +// +// 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 + +/** + * Implementation of the Symmetrically Encrypted AEAD Protected Data Packet
+ *
+ * {@link https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1}: AEAD Protected Data Packet + */ + +'use strict'; + +import util from '../util.js'; +import crypto from '../crypto'; +import enums from '../enums.js'; + +const IV_LEN = crypto.gcm.ivLength; + +/** + * @constructor + */ +export default function SymEncryptedAEADProtected() { + this.tag = enums.packet.symEncryptedAEADProtected; + this.iv = null; + this.encrypted = null; + /** Decrypted packets contained within. + * @type {module:packet/packetlist} */ + this.packets = null; +} + +SymEncryptedAEADProtected.prototype.read = function (bytes) { + this.iv = bytes.subarray(0, IV_LEN); + this.encrypted = bytes.subarray(IV_LEN, bytes.length); +}; + +SymEncryptedAEADProtected.prototype.write = function () { + return util.concatUint8Array([this.iv, this.encrypted]); +}; + +SymEncryptedAEADProtected.prototype.decrypt = function (sessionKeyAlgorithm, key) { + return crypto.gcm.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv).then(decrypted => { + this.packets.read(decrypted); + }); +}; + +SymEncryptedAEADProtected.prototype.encrypt = function (sessionKeyAlgorithm, key) { + var data = this.packets.write(); + this.iv = crypto.random.getRandomValues(new Uint8Array(IV_LEN)); + + return crypto.gcm.encrypt(sessionKeyAlgorithm, data, key, this.iv).then(encrypted => { + this.encrypted = encrypted; + }); +}; From 149f5d519189fb1b69ae9cc3bdcdd8e7f22a4944 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 14:09:07 +0800 Subject: [PATCH 02/30] Use promise api in sym_encrypted_* packets --- src/packet/sym_encrypted_integrity_protected.js | 4 ++++ src/packet/symmetrically_encrypted.js | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/packet/sym_encrypted_integrity_protected.js b/src/packet/sym_encrypted_integrity_protected.js index f2eaa985..415bf789 100644 --- a/src/packet/sym_encrypted_integrity_protected.js +++ b/src/packet/sym_encrypted_integrity_protected.js @@ -108,6 +108,8 @@ SymEncryptedIntegrityProtected.prototype.encrypt = function (sessionKeyAlgorithm this.encrypted = crypto.cfb.encrypt(prefixrandom, sessionKeyAlgorithm, tohash, key, false) .subarray(0, prefix.length + tohash.length); } + + return Promise.resolve(); }; /** @@ -153,4 +155,6 @@ SymEncryptedIntegrityProtected.prototype.decrypt = function (sessionKeyAlgorithm } else { this.packets.read(decrypted.subarray(0, decrypted.length - 22)); } + + return Promise.resolve(); }; diff --git a/src/packet/symmetrically_encrypted.js b/src/packet/symmetrically_encrypted.js index 2b919be3..04a0b8a1 100644 --- a/src/packet/symmetrically_encrypted.js +++ b/src/packet/symmetrically_encrypted.js @@ -73,11 +73,14 @@ SymmetricallyEncrypted.prototype.decrypt = function (sessionKeyAlgorithm, key) { throw new Error('Decryption failed due to missing MDC in combination with modern cipher.'); } this.packets.read(decrypted); + + return Promise.resolve(); }; SymmetricallyEncrypted.prototype.encrypt = function (algo, key) { var data = this.packets.write(); - this.encrypted = crypto.cfb.encrypt( - crypto.getPrefixRandom(algo), algo, data, key, true); + this.encrypted = crypto.cfb.encrypt(crypto.getPrefixRandom(algo), algo, data, key, true); + + return Promise.resolve(); }; From 2e4d8547a04fb2e024bd3e9035fa59904e2f868b Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 14:12:58 +0800 Subject: [PATCH 03/30] Fix typo in src/crypto/index.js --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 8888a4e5..e337b4cb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -22,7 +22,7 @@ const mod = { hash: hash, /** @see module:crypto/cfb */ cfb: cfb, - /** @see module:crypto/aes-gcm */ + /** @see module:crypto/gcm */ gcm: gcm, /** @see module:crypto/public_key */ publicKey: publicKey, From 365a9d21078edfe1598d5e9e4d31c77f8d295d95 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 14:17:46 +0800 Subject: [PATCH 04/30] Fix link to IEFT draft packet tag in enums.js --- src/enums.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums.js b/src/enums.js index baf7b158..837c6649 100644 --- a/src/enums.js +++ b/src/enums.js @@ -95,7 +95,7 @@ export default { userAttribute: 17, symEncryptedIntegrityProtected: 18, modificationDetectionCode: 19, - symEncryptedAEADProtected: 20 // see IETF draft: https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.2.1 + symEncryptedAEADProtected: 20 // see IETF draft: https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1 }, /** Data types in the literal packet From 49faca83c5b921abf58224f656e7ca0526c30018 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 14:27:24 +0800 Subject: [PATCH 05/30] Upgrade to asmcrypto-lite@1.1.0 in npm-shrinkwrap.json --- npm-shrinkwrap.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index b665fff2..46a5c16a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -3,9 +3,9 @@ "version": "2.1.0", "dependencies": { "asmcrypto-lite": { - "version": "1.0.1", + "version": "1.1.0", "from": "asmcrypto-lite@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/asmcrypto-lite/-/asmcrypto-lite-1.0.1.tgz" + "resolved": "https://registry.npmjs.org/asmcrypto-lite/-/asmcrypto-lite-1.1.0.tgz" }, "babel-preset-es2015": { "version": "6.5.0", From 8aa15b66a9b4cac2d81189fff47d116d27fc4f43 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 16:25:24 +0800 Subject: [PATCH 06/30] Cleanup and unit test gcm.js --- src/crypto/gcm.js | 54 +++++++++++++---------------------- test/crypto/crypto.js | 65 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 75 insertions(+), 44 deletions(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index 29a927ea..feccb104 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -23,12 +23,14 @@ 'use strict'; import util from '../util.js'; +import config from '../config'; import asmCrypto from 'asmcrypto-lite'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); export const ivLength = 12; +const ALGO = 'AES-GCM'; /** * Encrypt plaintext input. @@ -40,26 +42,18 @@ export const ivLength = 12; */ export function encrypt(cipher, plaintext, key, iv) { if (cipher.substr(0,3) !== 'aes') { - return Promise.reject(new Error('Invalid cipher for GCM mode')); + return Promise.reject(new Error('GCM mode supports only AES cipher')); } - if (webCrypto) { // native WebCrypto api - const keyOptions = { - name: 'AES-GCM' - }, - encryptOptions = { - name: 'AES-GCM', - iv: iv - }; - return webCrypto.importKey('raw', key, keyOptions, false, ['encrypt']).then(keyObj => { - return webCrypto.encrypt(encryptOptions, keyObj, plaintext); - }).then(ciphertext => { - return new Uint8Array(ciphertext); - }); + const keySize = cipher.substr(3,3); + if (webCrypto && config.useNative && keySize !== '192') { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + return webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt']) + .then(keyObj => webCrypto.encrypt({ name: ALGO, iv }, keyObj, plaintext)) + .then(ciphertext => new Uint8Array(ciphertext)); - } else if(nodeCrypto) { // native node crypto library - let cipherObj = new nodeCrypto.createCipheriv('aes-' + cipher.substr(3,3) + '-gcm', new Buffer(key), new Buffer(iv)); - let encrypted = Buffer.concat([cipherObj.update(new Buffer(plaintext)), cipherObj.final()]); + } else if (nodeCrypto && config.useNative) { // Node crypto library + const en = new nodeCrypto.createCipheriv('aes-' + keySize + '-gcm', new Buffer(key), new Buffer(iv)); + const encrypted = Buffer.concat([en.update(new Buffer(plaintext)), en.final()]); return Promise.resolve(new Uint8Array(encrypted)); } else { // asm.js fallback @@ -77,26 +71,18 @@ export function encrypt(cipher, plaintext, key, iv) { */ export function decrypt(cipher, ciphertext, key, iv) { if (cipher.substr(0,3) !== 'aes') { - return Promise.reject(new Error('Invalid cipher for GCM mode')); + return Promise.reject(new Error('GCM mode supports only AES cipher')); } - if (webCrypto) { // native WebCrypto api - const keyOptions = { - name: 'AES-GCM' - }, - decryptOptions = { - name: 'AES-GCM', - iv: iv - }; - return webCrypto.importKey('raw', key, keyOptions, false, ['decrypt']).then(keyObj => { - return webCrypto.decrypt(decryptOptions, keyObj, ciphertext); - }).then(plaintext => { - return new Uint8Array(plaintext); - }); + const keySize = cipher.substr(3,3); + if (webCrypto && config.useNative && keySize !== '192') { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + return webCrypto.importKey('raw', key, { name: ALGO }, false, ['decrypt']) + .then(keyObj => webCrypto.decrypt({ name: ALGO, iv }, keyObj, ciphertext)) + .then(plaintext => new Uint8Array(plaintext)); - } else if(nodeCrypto) { // native node crypto library - let decipherObj = new nodeCrypto.createDecipheriv('aes-' + cipher.substr(3,3) + '-gcm', new Buffer(key), new Buffer(iv)); - let decrypted = Buffer.concat([decipherObj.update(new Buffer(ciphertext)), decipherObj.final()]); + } else if (nodeCrypto && config.useNative) { // Node crypto library + let de = new nodeCrypto.createDecipheriv('aes-' + keySize + '-gcm', new Buffer(key), new Buffer(iv)); + let decrypted = Buffer.concat([de.update(new Buffer(ciphertext)), de.final()]); return Promise.resolve(new Uint8Array(decrypted)); } else { // asm.js fallback diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 08afcfeb..1cfd6642 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -80,7 +80,7 @@ describe('API functional testing', function() { 0x51,0xe0,0x22,0xf0,0xff,0xa7,0x42,0xd4,0xde,0x0b,0x47,0x8f,0x2b, 0xf5,0x4d,0x04,0x32,0x91,0x89,0x4b,0x0e,0x05,0x8d,0x70,0xf9,0xbb, 0xe7,0xd6,0x76,0xea,0x0e,0x1a,0x90,0x30,0xf5,0x98,0x01,0xc5,0x73])]; - + var DSApubMPIstrs = [ new Uint8Array([0x08,0x00,0xa8,0x85,0x5c,0x28,0x05,0x94,0x03,0xbe,0x07,0x6c,0x13,0x3e,0x65, 0xfb,0xb5,0xe1,0x99,0x7c,0xfa,0x84,0xe3,0xac,0x47,0xa5,0xc4,0x46,0xd8,0x5f, @@ -143,7 +143,7 @@ describe('API functional testing', function() { new Uint8Array([0x01,0x00,0x9b,0x58,0xa8,0xf4,0x04,0xb1,0xd5,0x14,0x09,0xe1,0xe1,0xa1,0x8a, 0x0b,0xa3,0xc3,0xa3,0x66,0xaa,0x27,0x99,0x50,0x1c,0x4d,0xba,0x24,0xee,0xdf, 0xdf,0xb8,0x8e,0x8e])]; - + var ElgamalpubMPIstrs = [ new Uint8Array([0x08,0x00,0xea,0xcc,0xbe,0xe2,0xe4,0x5a,0x51,0x18,0x93,0xa1,0x12,0x2f,0x00, 0x99,0x42,0xd8,0x5c,0x1c,0x2f,0xb6,0x3c,0xd9,0x94,0x61,0xb4,0x55,0x8d,0x4e, @@ -200,13 +200,13 @@ describe('API functional testing', function() { RSAsecMPIs[i] = new openpgp.MPI(); RSAsecMPIs[i].read(RSAsecMPIstrs[i]); } - + var DSAsecMPIs = []; for (i = 0; i < 1; i++) { DSAsecMPIs[i] = new openpgp.MPI(); DSAsecMPIs[i].read(DSAsecMPIstrs[i]); } - + var DSApubMPIs = []; for (i = 0; i < 4; i++) { DSApubMPIs[i] = new openpgp.MPI(); @@ -217,7 +217,7 @@ describe('API functional testing', function() { ElgamalsecMPIs[i] = new openpgp.MPI(); ElgamalsecMPIs[i].read(ElgamalsecMPIstrs[i]); } - + var ElgamalpubMPIs = []; for (i = 0; i < 3; i++) { ElgamalpubMPIs[i] = new openpgp.MPI(); @@ -287,6 +287,25 @@ describe('API functional testing', function() { }); } + function testAESGCM(plaintext) { + symmAlgos.forEach(function(algo) { + if(algo.substr(0,3) === 'aes') { + it(algo, function(done) { + var key = openpgp.crypto.generateSessionKey(algo); + var iv = openpgp.crypto.random.getRandomValues(new Uint8Array(openpgp.crypto.gcm.ivLength)); + + openpgp.crypto.gcm.encrypt(algo, util.str2Uint8Array(plaintext), key, iv).then(function(ciphertext) { + return openpgp.crypto.gcm.decrypt(algo, ciphertext, key, iv); + }).then(function(decrypted) { + var decryptedStr = util.Uint8Array2str(decrypted); + expect(decryptedStr).to.equal(plaintext); + done(); + }); + }); + } + }); + } + it("Symmetric with OpenPGP CFB resync", function () { testCFB("hello", true); testCFB("1234567", true); @@ -301,11 +320,37 @@ describe('API functional testing', function() { testCFB("12345678901234567890123456789012345678901234567890", false); }); - it("asmCrypto AES without OpenPGP CFB resync", function () { - testCFB("hello"); - testCFB("1234567"); - testCFB("foobarfoobar1234567890"); - testCFB("12345678901234567890123456789012345678901234567890"); + it.skip("asmCrypto AES without OpenPGP CFB resync", function () { + testAESCFB("hello"); + testAESCFB("1234567"); + testAESCFB("foobarfoobar1234567890"); + testAESCFB("12345678901234567890123456789012345678901234567890"); + }); + + describe('Symmetric AES-GCM (native)', function() { + var useNativeVal; + beforeEach(function() { + useNativeVal = openpgp.config.useNative; + openpgp.config.useNative = true; + }); + afterEach(function() { + openpgp.config.useNative = useNativeVal; + }); + + testAESGCM("12345678901234567890123456789012345678901234567890"); + }); + + describe('Symmetric AES-GCM (asm.js fallback)', function() { + var useNativeVal; + beforeEach(function() { + useNativeVal = openpgp.config.useNative; + openpgp.config.useNative = false; + }); + afterEach(function() { + openpgp.config.useNative = useNativeVal; + }); + + testAESGCM("12345678901234567890123456789012345678901234567890"); }); it('Asymmetric using RSA with eme_pkcs1 padding', function (done) { From 8f8218e9def6c2c0bb6c68e54b55cf1abdef20d1 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 17:35:10 +0800 Subject: [PATCH 07/30] Cleanup and test AEAD protected packet --- src/packet/sym_encrypted_aead_protected.js | 47 ++++++++++++++++------ test/general/packet.js | 23 +++++++++++ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index eb42ffe7..36dcfb05 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -16,8 +16,7 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA /** - * Implementation of the Symmetrically Encrypted AEAD Protected Data Packet
- *
+ * Implementation of the Symmetrically Encrypted Authenticated Encryption with Additional Data (AEAD) Protected Data Packet * {@link https://tools.ietf.org/html/draft-ford-openpgp-format-00#section-2.1}: AEAD Protected Data Packet */ @@ -27,7 +26,8 @@ import util from '../util.js'; import crypto from '../crypto'; import enums from '../enums.js'; -const IV_LEN = crypto.gcm.ivLength; +const VERSION = 1; // A one-octet version number of the data packet. +const IV_LEN = crypto.gcm.ivLength; // currently only AES-GCM is supported /** * @constructor @@ -36,31 +36,52 @@ export default function SymEncryptedAEADProtected() { this.tag = enums.packet.symEncryptedAEADProtected; this.iv = null; this.encrypted = null; - /** Decrypted packets contained within. - * @type {module:packet/packetlist} */ this.packets = null; } +/** + * Parse an encrypted payload of bytes in the order: version, IV, ciphertext (see specification) + */ SymEncryptedAEADProtected.prototype.read = function (bytes) { - this.iv = bytes.subarray(0, IV_LEN); - this.encrypted = bytes.subarray(IV_LEN, bytes.length); + let offset = 0; + if (bytes[offset] !== VERSION) { // The only currently defined value is 1. + throw new Error('Invalid packet version.'); + } + offset++; + this.iv = bytes.subarray(offset, IV_LEN + offset); + offset += IV_LEN; + this.encrypted = bytes.subarray(offset, bytes.length); }; +/** + * Write the encrypted payload of bytes in the order: version, IV, ciphertext (see specification) + * @return {Uint8Array} The encrypted payload + */ SymEncryptedAEADProtected.prototype.write = function () { - return util.concatUint8Array([this.iv, this.encrypted]); + return util.concatUint8Array([new Uint8Array([VERSION]), this.iv, this.encrypted]); }; +/** + * Decrypt the encrypted payload. + * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' + * @param {Uint8Array} key The session key used to encrypt the payload + * @return {Promise} Nothing is returned + */ SymEncryptedAEADProtected.prototype.decrypt = function (sessionKeyAlgorithm, key) { return crypto.gcm.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv).then(decrypted => { this.packets.read(decrypted); }); }; +/** + * Encrypt the packet list payload. + * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' + * @param {Uint8Array} key The session key used to encrypt the payload + * @return {Promise} Nothing is returned + */ SymEncryptedAEADProtected.prototype.encrypt = function (sessionKeyAlgorithm, key) { - var data = this.packets.write(); - this.iv = crypto.random.getRandomValues(new Uint8Array(IV_LEN)); - - return crypto.gcm.encrypt(sessionKeyAlgorithm, data, key, this.iv).then(encrypted => { + this.iv = crypto.random.getRandomValues(new Uint8Array(IV_LEN)); // generate new random IV + return crypto.gcm.encrypt(sessionKeyAlgorithm, this.packets.write(), key, this.iv).then(encrypted => { this.encrypted = encrypted; }); -}; +}; \ No newline at end of file diff --git a/test/general/packet.js b/test/general/packet.js index 0bc59f14..759630a2 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -121,6 +121,29 @@ describe("Packet", function() { done(); }); + it('Sym. encrypted AEAD protected packet', function(done) { + var 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]), + algo = 'aes256'; + + var literal = new openpgp.packet.Literal(), + enc = new openpgp.packet.SymEncryptedAEADProtected(), + msg = new openpgp.packet.List(); + + msg.push(enc); + literal.setText('Hello world!'); + enc.packets.push(literal); + + var msg2 = new openpgp.packet.List(); + + 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); + done(); + }); + }); + it('Sym encrypted session key with a compressed packet', function(done) { var msg = '-----BEGIN PGP MESSAGE-----\n' + From 7fabe02e035b35a3b33abfb8406dc3afe0f5e41b Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 13:20:42 +0800 Subject: [PATCH 08/30] Fix GCM under node.js --- src/crypto/gcm.js | 29 ++++++++++++++++------------- test/crypto/crypto.js | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index feccb104..53e44e97 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -29,12 +29,13 @@ const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); -export const ivLength = 12; +export const ivLength = 12; // size of the IV in bytes +const TAG_LEN = 16; // size of the tag in bytes const ALGO = 'AES-GCM'; /** * Encrypt plaintext input. - * @param {String} cipher The symmetric cipher algorithm to use + * @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) @@ -45,16 +46,16 @@ export function encrypt(cipher, plaintext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - const keySize = cipher.substr(3,3); - if (webCrypto && config.useNative && keySize !== '192') { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + const keySize = key.length * 8; + if (webCrypto && config.useNative && keySize !== 192) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support return webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt']) .then(keyObj => webCrypto.encrypt({ name: ALGO, iv }, keyObj, plaintext)) .then(ciphertext => new Uint8Array(ciphertext)); } else if (nodeCrypto && config.useNative) { // Node crypto library - const en = new nodeCrypto.createCipheriv('aes-' + keySize + '-gcm', new Buffer(key), new Buffer(iv)); - const encrypted = Buffer.concat([en.update(new Buffer(plaintext)), en.final()]); - return Promise.resolve(new Uint8Array(encrypted)); + const en = new nodeCrypto.createCipheriv('aes-' + keySize + '-gcm', new Buffer(key.buffer), new Buffer(iv.buffer)); + const encrypted = Buffer.concat([en.update(new Buffer(plaintext.buffer)), en.final()]); + return Promise.resolve(new Uint8Array(Buffer.concat([encrypted, en.getAuthTag()]))); } else { // asm.js fallback return Promise.resolve(asmCrypto.AES_GCM.encrypt(plaintext, key, iv)); @@ -63,7 +64,7 @@ export function encrypt(cipher, plaintext, key, iv) { /** * Decrypt ciphertext input - * @param {String} cipher The symmetric cipher algorithm to use + * @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} iv The initialization vector (12 bytes) @@ -74,16 +75,18 @@ export function decrypt(cipher, ciphertext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - const keySize = cipher.substr(3,3); - if (webCrypto && config.useNative && keySize !== '192') { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + const keySize = key.length * 8; + if (webCrypto && config.useNative && keySize !== 192) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support return webCrypto.importKey('raw', key, { name: ALGO }, false, ['decrypt']) .then(keyObj => webCrypto.decrypt({ name: ALGO, iv }, keyObj, ciphertext)) .then(plaintext => new Uint8Array(plaintext)); } else if (nodeCrypto && config.useNative) { // Node crypto library - let de = new nodeCrypto.createDecipheriv('aes-' + keySize + '-gcm', new Buffer(key), new Buffer(iv)); - let decrypted = Buffer.concat([de.update(new Buffer(ciphertext)), de.final()]); - return Promise.resolve(new Uint8Array(decrypted)); + const ctBuf = new Buffer(ciphertext.buffer); + const de = new nodeCrypto.createDecipheriv('aes-' + keySize + '-gcm', new Buffer(key.buffer), new Buffer(iv.buffer)); + de.setAuthTag(ctBuf.slice(ctBuf.length - TAG_LEN, ctBuf.length)); + const encrypted = ctBuf.slice(0, ctBuf.length - TAG_LEN); + return Promise.resolve(new Uint8Array(Buffer.concat([de.update(encrypted), de.final()]))); } else { // asm.js fallback return Promise.resolve(asmCrypto.AES_GCM.decrypt(ciphertext, key, iv)); diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 1cfd6642..67b0f0c1 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -1,6 +1,6 @@ 'use strict'; -var openpgp = typeof window != 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp'); +var openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp'); var chai = require('chai'), expect = chai.expect; From a225027a6bbc0317d1c7e37facda19aba14eb0b9 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 13:42:46 +0800 Subject: [PATCH 09/30] Wrap message.sign into a promise in openpgp.encrypt --- src/openpgp.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index 6cf458dd..d5c00587 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -173,12 +173,14 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); } - let message = createMessage(data, filename); - if (privateKeys) { // sign the message only if private keys are specified - message = message.sign(privateKeys); - } + return new Promise(resolve => { + let message = createMessage(data, filename); + if (privateKeys) { // sign the message only if private keys are specified + message = message.sign(privateKeys); + } + resolve(message); - return message.encrypt(publicKeys, passwords).then(message => { + }).then(message => message.encrypt(publicKeys, passwords)).then(message => { if(armor) { return { From e5e76d2eb7074652b571ccce959312df53419f7d Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 13:54:02 +0800 Subject: [PATCH 10/30] Cleanup gcm.js --- src/crypto/gcm.js | 69 +++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index 53e44e97..bdcb604c 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -46,24 +46,17 @@ export function encrypt(cipher, plaintext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - const keySize = key.length * 8; - if (webCrypto && config.useNative && keySize !== 192) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support - return webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt']) - .then(keyObj => webCrypto.encrypt({ name: ALGO, iv }, keyObj, plaintext)) - .then(ciphertext => new Uint8Array(ciphertext)); - + if (webCrypto && config.useNative && 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 && config.useNative) { // Node crypto library - const en = new nodeCrypto.createCipheriv('aes-' + keySize + '-gcm', new Buffer(key.buffer), new Buffer(iv.buffer)); - const encrypted = Buffer.concat([en.update(new Buffer(plaintext.buffer)), en.final()]); - return Promise.resolve(new Uint8Array(Buffer.concat([encrypted, en.getAuthTag()]))); - + return nodeEncrypt(plaintext, key, iv) ; } else { // asm.js fallback return Promise.resolve(asmCrypto.AES_GCM.encrypt(plaintext, key, iv)); } } /** - * Decrypt ciphertext input + * 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 @@ -75,20 +68,50 @@ export function decrypt(cipher, ciphertext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - const keySize = key.length * 8; - if (webCrypto && config.useNative && keySize !== 192) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support - return webCrypto.importKey('raw', key, { name: ALGO }, false, ['decrypt']) - .then(keyObj => webCrypto.decrypt({ name: ALGO, iv }, keyObj, ciphertext)) - .then(plaintext => new Uint8Array(plaintext)); - + if (webCrypto && config.useNative && 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 && config.useNative) { // Node crypto library - const ctBuf = new Buffer(ciphertext.buffer); - const de = new nodeCrypto.createDecipheriv('aes-' + keySize + '-gcm', new Buffer(key.buffer), new Buffer(iv.buffer)); - de.setAuthTag(ctBuf.slice(ctBuf.length - TAG_LEN, ctBuf.length)); - const encrypted = ctBuf.slice(0, ctBuf.length - TAG_LEN); - return Promise.resolve(new Uint8Array(Buffer.concat([de.update(encrypted), de.final()]))); - + return nodeDecrypt(ciphertext, key, iv); } else { // asm.js fallback return Promise.resolve(asmCrypto.AES_GCM.decrypt(ciphertext, key, iv)); } +} + + +////////////////////////// +// // +// 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.buffer); + key = new Buffer(key.buffer); + iv = new Buffer(iv.buffer); + 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.buffer); + key = new Buffer(key.buffer); + iv = new Buffer(iv.buffer); + 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)); } \ No newline at end of file From 963648c6ea8323b4656ab52ee392b83dd3a342b3 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 15:39:51 +0800 Subject: [PATCH 11/30] Fix unit tests for browsers --- src/message.js | 9 +++------ test/general/key.js | 12 ++++++++---- test/general/openpgp.js | 5 ++++- test/general/signature.js | 13 +++++++------ 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/message.js b/src/message.js index cc427f46..f5f217d3 100644 --- a/src/message.js +++ b/src/message.js @@ -107,8 +107,7 @@ Message.prototype.decrypt = function(privateKey, sessionKey, password) { var symEncryptedPacket = symEncryptedPacketlist[0]; return symEncryptedPacket.decrypt(keyObj.algorithm, keyObj.data).then(() => { var resultMsg = new Message(symEncryptedPacket.packets); - // remove packets after decryption - symEncryptedPacket.packets = new packet.List(); + symEncryptedPacket.packets = new packet.List(); // remove packets after decryption return resultMsg; }); } @@ -224,7 +223,6 @@ Message.prototype.encrypt = function(keys, passwords) { var sessionKey = crypto.generateSessionKey(enums.read(enums.symmetric, symAlgo)); var msg = encryptSessionKey(sessionKey, enums.read(enums.symmetric, symAlgo), keys, passwords); - var packetlist = msg.packets; var symEncryptedPacket; if (config.aead_protect) { @@ -237,9 +235,8 @@ Message.prototype.encrypt = function(keys, passwords) { symEncryptedPacket.packets = this.packets; return symEncryptedPacket.encrypt(enums.read(enums.symmetric, symAlgo), sessionKey).then(() => { - packetlist.push(symEncryptedPacket); - // remove packets after encryption - symEncryptedPacket.packets = new packet.List(); + msg.packets.push(symEncryptedPacket); + symEncryptedPacket.packets = new packet.List(); // remove packets after encryption return msg; }); }; diff --git a/test/general/key.js b/test/general/key.js index 0283a44e..3584ca78 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -659,10 +659,14 @@ var pgp_desktop_priv = it('Generated key is not unlocked by default', function(done) { var opt = {numBits: 512, userIds: 'test ', passphrase: '123'}; if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys - openpgp.generateKey(opt).then(function(key) { - var msg = openpgp.message.fromText('hello').encrypt([key.key]); - msg = msg.decrypt.bind(msg, key.key); - expect(msg).to.throw('Private key is not decrypted.'); + var key; + openpgp.generateKey(opt).then(function(newKey) { + key = newKey; + return openpgp.message.fromText('hello').encrypt([key.key]); + }).then(function(msg) { + return msg.decrypt(key.key); + }).catch(function(err) { + expect(err.message).to.equal('Private key is not decrypted.'); done(); }); }); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index f7ecd6ca..3e6b6aea 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -422,7 +422,7 @@ describe('OpenPGP.js public api tests', function() { }); describe('encrypt, decrypt, sign, verify - integration tests', function() { - var privateKey, publicKey, zeroCopyVal; + var privateKey, publicKey, zeroCopyVal, useNativeVal; beforeEach(function() { publicKey = openpgp.key.readArmored(pub_key); @@ -432,10 +432,12 @@ describe('OpenPGP.js public api tests', function() { expect(privateKey.keys).to.have.length(1); expect(privateKey.err).to.not.exist; zeroCopyVal = openpgp.config.zeroCopy; + useNativeVal = openpgp.config.useNative; }); afterEach(function() { openpgp.config.zeroCopy = zeroCopyVal; + openpgp.config.useNative = useNativeVal; }); it('Decrypting key with wrong passphrase returns false', function () { @@ -876,6 +878,7 @@ describe('OpenPGP.js public api tests', function() { it('should encrypt and decrypt with binary data and transferable objects', function(done) { openpgp.config.zeroCopy = true; // activate transferable objects + openpgp.config.useNative = false; // use asm.js fallback with web worker, not native crypto var encOpt = { data: new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01]), passwords: password1, diff --git a/test/general/signature.js b/test/general/signature.js index e29c562a..397b4307 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -304,12 +304,13 @@ describe("Signature", function() { var msg = openpgp.message.readArmored(msg_arm1); priv_key_gnupg_ext.subKeys[0].subKey.decrypt("abcd"); - msg = msg.decrypt(priv_key_gnupg_ext); - var verified = msg.verify([pub_key]); - expect(verified).to.exist; - expect(verified).to.have.length(1); - expect(verified[0].valid).to.be.true; - done(); + msg.decrypt(priv_key_gnupg_ext).then(function(msg) { + var verified = msg.verify([pub_key]); + expect(verified).to.exist; + expect(verified).to.have.length(1); + expect(verified[0].valid).to.be.true; + done(); + }); }); it('Verify V4 signature. Hash: SHA1. PK: RSA. Signature Type: 0x00 (binary document)', function(done) { From 9b2823aa674cea4ec438edacbf224b50f446d3b5 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 16:43:40 +0800 Subject: [PATCH 12/30] Fix unit tests under node --- src/crypto/gcm.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index bdcb604c..00cef355 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -98,18 +98,18 @@ function webDecrypt(ct, key, iv) { } function nodeEncrypt(pt, key, iv) { - pt = new Buffer(pt.buffer); - key = new Buffer(key.buffer); - iv = new Buffer(iv.buffer); + 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.buffer); - key = new Buffer(key.buffer); - iv = new Buffer(iv.buffer); + 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()]); From 760bdb8f00226eed42ea3178facf07bae4d19597 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 17:27:09 +0800 Subject: [PATCH 13/30] Use only standard window.crypto.subtle in gcm.js There is currently no support for AES-GCM in IE11 and Safari/iOSqq --- src/crypto/gcm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index 00cef355..d21aefa9 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -25,7 +25,7 @@ import util from '../util.js'; import config from '../config'; import asmCrypto from 'asmcrypto-lite'; -const webCrypto = util.getWebCrypto(); +const webCrypto = typeof window !== 'undefined' && window.crypto && window.crypto.subtle; // no GCM support in IE11, Safari 9 const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); From 26bf7b62c772ec62373aede69640327fc6d6bdfd Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 19:15:04 +0800 Subject: [PATCH 14/30] Prevent native crypto for worker tests. --- test/general/openpgp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 3e6b6aea..3be82c67 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -451,6 +451,7 @@ describe('OpenPGP.js public api tests', function() { describe('without Worker', tests); tryWorker('with Worker', tests, function() { + openpgp.config.useNative = false; openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); }, function() { openpgp.destroyWorker(); // cleanup worker in case of failure @@ -878,7 +879,6 @@ describe('OpenPGP.js public api tests', function() { it('should encrypt and decrypt with binary data and transferable objects', function(done) { openpgp.config.zeroCopy = true; // activate transferable objects - openpgp.config.useNative = false; // use asm.js fallback with web worker, not native crypto var encOpt = { data: new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01]), passwords: password1, From 453a9cee07a89c86330d516f93f78a4677bd2429 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 21:17:36 +0800 Subject: [PATCH 15/30] Use web worker for encrypt/decrypt if no native gcm --- src/crypto/gcm.js | 2 +- src/crypto/public_key/rsa.js | 2 +- src/openpgp.js | 4 ++-- src/util.js | 20 ++++++++++++++++++-- test/general/key.js | 8 ++++---- test/general/openpgp.js | 15 +++++++++------ test/general/packet.js | 6 +++--- test/general/signature.js | 2 +- 8 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index d21aefa9..663dfbfd 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -25,7 +25,7 @@ import util from '../util.js'; import config from '../config'; import asmCrypto from 'asmcrypto-lite'; -const webCrypto = typeof window !== 'undefined' && window.crypto && window.crypto.subtle; // no GCM support in IE11, Safari 9 +const webCrypto = util.getWebCrypto(); // no GCM support in IE11, Safari 9 const nodeCrypto = util.getNodeCrypto(); const Buffer = util.getNodeBuffer(); diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index 0089de6e..89510f49 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -136,7 +136,7 @@ export default function RSA() { // Generate a new random private key B bits long, using public expt E function generate(B, E) { - var webCrypto = util.getWebCrypto(); + var webCrypto = util.getWebCryptoAll(); // // Native RSA keygen using Web Crypto diff --git a/src/openpgp.js b/src/openpgp.js index d5c00587..c881b959 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -99,7 +99,7 @@ export function destroyWorker() { export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=false } = {}) { const options = formatUserIds({ userIds, passphrase, numBits, unlocked }); - if (!util.getWebCrypto() && asyncProxy) { // use web worker if web crypto apis are not supported + if (!util.getWebCryptoAll() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('generateKey', options); } @@ -113,7 +113,7 @@ export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=fal // js fallback already tried if (config.debug) { console.error(err); } - if (!util.getWebCrypto()) { + if (!util.getWebCryptoAll()) { throw new Error('Error generating keypair using js fallback'); } // fall back to js keygen in a worker diff --git a/src/util.js b/src/util.js index c6f4fa73..4ac04c8e 100644 --- a/src/util.js +++ b/src/util.js @@ -450,8 +450,9 @@ export default { }, /** - * Get native Web Cryptography api. The default configuration is to use - * the api when available. But it can also be deactivated with config.useNative + * Get native Web Cryptography api, only the current versioon of the spec. + * The default configuration is to use the api when available. But it can + * be deactivated with config.useNative * @return {Object} The SubtleCrypto api or 'undefined' */ getWebCrypto: function() { @@ -459,6 +460,21 @@ export default { return; } + return typeof window !== 'undefined' && window.crypto && window.crypto.subtle; + }, + + /** + * Get native Web Cryptography api for all browsers, including legacy + * implementations of the spec e.g IE11 and Safari 8/9. The default + * configuration is to use the api when available. But it can be deactivated + * with config.useNative + * @return {Object} The SubtleCrypto api or 'undefined' + */ + getWebCryptoAll: function() { + if (!config.useNative) { + return; + } + if (typeof window !== 'undefined') { if (window.crypto) { return window.crypto.subtle || window.crypto.webkitSubtle; diff --git a/test/general/key.js b/test/general/key.js index 3584ca78..f90de200 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -634,7 +634,7 @@ var pgp_desktop_priv = expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.integrity_protect ? [1] : null); // modification detection }; var opt = {numBits: 512, userIds: 'test ', passphrase: 'hello'}; - if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys openpgp.generateKey(opt).then(function(key) { testPref(key.key); testPref(openpgp.key.readArmored(key.publicKeyArmored).keys[0]); @@ -658,7 +658,7 @@ var pgp_desktop_priv = it('Generated key is not unlocked by default', function(done) { var opt = {numBits: 512, userIds: 'test ', passphrase: '123'}; - if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys var key; openpgp.generateKey(opt).then(function(newKey) { key = newKey; @@ -674,7 +674,7 @@ var pgp_desktop_priv = it('Generate key - single userid', function(done) { var userId = 'test '; var opt = {numBits: 512, userIds: userId, passphrase: '123'}; - if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys openpgp.generateKey(opt).then(function(key) { key = key.key; expect(key.users.length).to.equal(1); @@ -687,7 +687,7 @@ var pgp_desktop_priv = var userId1 = 'test '; var userId2 = 'test '; var opt = {numBits: 512, userIds: [userId1, userId2], passphrase: '123'}; - if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys openpgp.generateKey(opt).then(function(key) { key = key.key; expect(key.users.length).to.equal(2); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 3be82c67..d42cbaa9 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -184,7 +184,7 @@ describe('OpenPGP.js public api tests', function() { }); describe('generateKey - unit tests', function() { - var keyGenStub, keyObjStub, getWebCryptoStub; + var keyGenStub, keyObjStub, getWebCryptoAllStub; beforeEach(function() { keyObjStub = { @@ -201,13 +201,13 @@ describe('OpenPGP.js public api tests', function() { }; keyGenStub = sinon.stub(openpgp.key, 'generate'); keyGenStub.returns(resolves(keyObjStub)); - getWebCryptoStub = sinon.stub(openpgp.util, 'getWebCrypto'); + getWebCryptoAllStub = sinon.stub(openpgp.util, 'getWebCryptoAll'); }); afterEach(function() { keyGenStub.restore(); openpgp.destroyWorker(); - getWebCryptoStub.restore(); + getWebCryptoAllStub.restore(); }); it('should fail for invalid user name', function() { @@ -333,7 +333,7 @@ describe('OpenPGP.js public api tests', function() { worker: workerStub }); var proxyGenStub = sinon.stub(openpgp.getWorker(), 'delegate'); - getWebCryptoStub.returns(); + getWebCryptoAllStub.returns(); openpgp.generateKey(); expect(proxyGenStub.calledOnce).to.be.true; @@ -348,7 +348,7 @@ describe('OpenPGP.js public api tests', function() { worker: workerStub }); var proxyGenStub = sinon.stub(openpgp.getWorker(), 'delegate').returns(resolves('proxy_key')); - getWebCryptoStub.returns({}); + getWebCryptoAllStub.returns({}); keyGenStub.returns(rejects(new Error('Native webcrypto keygen failed on purpose :)'))); openpgp.generateKey().then(function(newKey) { @@ -410,7 +410,7 @@ describe('OpenPGP.js public api tests', function() { userIds: [{ name: 'Test User', email: 'text@example.com' }], numBits: 512 }; - if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys openpgp.generateKey(opt).then(function(newKey) { expect(newKey.key.getUserIds()[0]).to.equal('Test User '); @@ -461,6 +461,9 @@ describe('OpenPGP.js public api tests', function() { it('Configuration', function(done){ openpgp.config.show_version = false; openpgp.config.commentstring = 'different'; + if (openpgp.getWorker()) { // init again to trigger config event + openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); + } openpgp.encrypt({ publicKeys:publicKey.keys, data:plaintext }).then(function(encrypted) { expect(encrypted.data).to.exist; expect(encrypted.data).not.to.match(/^Version:/); diff --git a/test/general/packet.js b/test/general/packet.js index 759630a2..475989ee 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -173,7 +173,7 @@ describe("Packet", function() { it('Public key encrypted symmetric key packet', function(done) { var rsa = new openpgp.crypto.publicKey.rsa(); - var keySize = openpgp.util.getWebCrypto() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys + var keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys rsa.generate(keySize, "10001").then(function(mpiGen) { @@ -437,7 +437,7 @@ describe("Packet", function() { key.push(new openpgp.packet.SecretKey()); var rsa = new openpgp.crypto.publicKey.rsa(); - var keySize = openpgp.util.getWebCrypto() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys + var keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys rsa.generate(keySize, "10001").then(function(mipGen) { var mpi = [mipGen.n, mipGen.ee, mipGen.d, mipGen.p, mipGen.q, mipGen.u]; @@ -466,7 +466,7 @@ describe("Packet", function() { var key = new openpgp.packet.SecretKey(); var rsa = new openpgp.crypto.publicKey.rsa(); - var keySize = openpgp.util.getWebCrypto() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys + var keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys rsa.generate(keySize, "10001").then(function(mpiGen) { var mpi = [mpiGen.n, mpiGen.ee, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u]; diff --git a/test/general/signature.js b/test/general/signature.js index 397b4307..7370380e 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -643,7 +643,7 @@ describe("Signature", function() { it('Sign message with key without password', function(done) { var opt = {numBits: 512, userIds: { name:'test', email:'a@b.com' }, passphrase: null}; - if (openpgp.util.getWebCrypto()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys + if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys openpgp.generateKey(opt).then(function(gen) { var key = gen.key; From 1e3d6468d6baa649126455abed09ff6891bbeef9 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 21:19:02 +0800 Subject: [PATCH 16/30] Wrap code into promises to globally catch errors --- src/key.js | 29 +++++++++----------- src/message.js | 73 +++++++++++++++++++++++++++----------------------- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/key.js b/src/key.js index 31130995..ae303f38 100644 --- a/src/key.js +++ b/src/key.js @@ -933,24 +933,21 @@ export function readArmored(armoredText) { */ export function generate(options) { var packetlist, secretKeyPacket, userIdPacket, dataToSign, signaturePacket, secretSubkeyPacket, subkeySignaturePacket; + return new Promise(resolve => resolve()).then(() => { + options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; + if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated + throw new Error('Only RSA Encrypt or Sign supported'); + } - options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; - // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated - if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { - throw new Error('Only RSA Encrypt or Sign supported'); - } - // Key without passphrase is unlocked by definition - if (!options.passphrase) { - options.unlocked = true; - } - if (String.prototype.isPrototypeOf(options.userIds) || typeof options.userIds === 'string') { - options.userIds = [options.userIds]; - } + if (!options.passphrase) { // Key without passphrase is unlocked by definition + options.unlocked = true; + } + if (String.prototype.isPrototypeOf(options.userIds) || typeof options.userIds === 'string') { + options.userIds = [options.userIds]; + } - // generate - var genSecretKey = generateSecretKey(); - var genSecretSubkey = generateSecretSubkey(); - return Promise.all([genSecretKey, genSecretSubkey]).then(wrapKeyObject); + return Promise.all([generateSecretKey(), generateSecretSubkey()]).then(wrapKeyObject); + }); function generateSecretKey() { secretKeyPacket = new packet.SecretKey(); diff --git a/src/message.js b/src/message.js index f5f217d3..8ac585fe 100644 --- a/src/message.js +++ b/src/message.js @@ -92,26 +92,29 @@ Message.prototype.getSigningKeyIds = function() { * @return {Message} new message with decrypted content */ Message.prototype.decrypt = function(privateKey, sessionKey, password) { - var keyObj = sessionKey || this.decryptSessionKey(privateKey, password); - if (!keyObj || !util.isUint8Array(keyObj.data) || !util.isString(keyObj.algorithm)) { - throw new Error('Invalid session key for decryption.'); - } + return new Promise(resolve => resolve()).then(() => { + const keyObj = sessionKey || this.decryptSessionKey(privateKey, password); + if (!keyObj || !util.isUint8Array(keyObj.data) || !util.isString(keyObj.algorithm)) { + throw new Error('Invalid session key for decryption.'); + } - var symEncryptedPacketlist = this.packets.filterByTag( - enums.packet.symmetricallyEncrypted, - enums.packet.symEncryptedIntegrityProtected, - enums.packet.symEncryptedAEADProtected - ); + const symEncryptedPacketlist = this.packets.filterByTag( + enums.packet.symmetricallyEncrypted, + enums.packet.symEncryptedIntegrityProtected, + enums.packet.symEncryptedAEADProtected + ); - if (symEncryptedPacketlist.length !== 0) { - var symEncryptedPacket = symEncryptedPacketlist[0]; + if (symEncryptedPacketlist.length === 0) { + return; + } + + const symEncryptedPacket = symEncryptedPacketlist[0]; return symEncryptedPacket.decrypt(keyObj.algorithm, keyObj.data).then(() => { - var resultMsg = new Message(symEncryptedPacket.packets); + const resultMsg = new Message(symEncryptedPacket.packets); symEncryptedPacket.packets = new packet.List(); // remove packets after decryption return resultMsg; }); - } - return Promise.resolve(); + }); }; /** @@ -212,29 +215,31 @@ Message.prototype.getText = function() { * @return {Message} new message with encrypted content */ Message.prototype.encrypt = function(keys, passwords) { - var symAlgo; - if (keys) { - symAlgo = keyModule.getPreferredSymAlgo(keys); - } else if (passwords) { - symAlgo = config.encryption_cipher; - } else { - throw new Error('No keys or passwords'); - } + let symAlgo, msg, symEncryptedPacket; + return new Promise(resolve => resolve()).then(() => { + if (keys) { + symAlgo = keyModule.getPreferredSymAlgo(keys); + } else if (passwords) { + symAlgo = config.encryption_cipher; + } else { + throw new Error('No keys or passwords'); + } - var sessionKey = crypto.generateSessionKey(enums.read(enums.symmetric, symAlgo)); - var msg = encryptSessionKey(sessionKey, enums.read(enums.symmetric, symAlgo), keys, passwords); + let sessionKey = crypto.generateSessionKey(enums.read(enums.symmetric, symAlgo)); + msg = encryptSessionKey(sessionKey, enums.read(enums.symmetric, symAlgo), keys, passwords); - var symEncryptedPacket; - if (config.aead_protect) { - symEncryptedPacket = new packet.SymEncryptedAEADProtected(); - } else if (config.integrity_protect) { - symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); - } else { - symEncryptedPacket = new packet.SymmetricallyEncrypted(); - } - symEncryptedPacket.packets = this.packets; + if (config.aead_protect) { + symEncryptedPacket = new packet.SymEncryptedAEADProtected(); + } else if (config.integrity_protect) { + symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); + } else { + symEncryptedPacket = new packet.SymmetricallyEncrypted(); + } + symEncryptedPacket.packets = this.packets; - return symEncryptedPacket.encrypt(enums.read(enums.symmetric, symAlgo), sessionKey).then(() => { + return symEncryptedPacket.encrypt(enums.read(enums.symmetric, symAlgo), sessionKey); + + }).then(() => { msg.packets.push(symEncryptedPacket); symEncryptedPacket.packets = new packet.List(); // remove packets after encryption return msg; From ddedb5cb42f57db6d5f47ea3b8a5dae429baa04a Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 23 Mar 2016 21:36:48 +0800 Subject: [PATCH 17/30] Cleanup public api --- src/openpgp.js | 20 +++++--------------- test/general/openpgp.js | 19 ------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index c881b959..470bc3e7 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -109,18 +109,7 @@ export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=fal privateKeyArmored: newKey.armor(), publicKeyArmored: newKey.toPublic().armor() - })).catch(err => { - - // js fallback already tried - if (config.debug) { console.error(err); } - if (!util.getWebCryptoAll()) { - throw new Error('Error generating keypair using js fallback'); - } - // fall back to js keygen in a worker - if (config.debug) { console.log('Error generating keypair using native WebCrypto... falling back back to js'); } - return asyncProxy.delegate('generateKey', options); - - }).catch(onError.bind(null, 'Error generating keypair')); + })).catch(onError.bind(null, 'Error generating keypair')); } /** @@ -173,14 +162,15 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); } - return new Promise(resolve => { + return new Promise(resolve => resolve()).then(() => { + let message = createMessage(data, filename); if (privateKeys) { // sign the message only if private keys are specified message = message.sign(privateKeys); } - resolve(message); + return message.encrypt(publicKeys, passwords); - }).then(message => message.encrypt(publicKeys, passwords)).then(message => { + }).then(message => { if(armor) { return { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index d42cbaa9..5a03b3e7 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -339,25 +339,6 @@ describe('OpenPGP.js public api tests', function() { expect(proxyGenStub.calledOnce).to.be.true; expect(keyGenStub.calledOnce).to.be.false; }); - - it('should delegate to async proxy after web crypto failure', function(done) { - var workerStub = { - postMessage: function() {} - }; - openpgp.initWorker({ - worker: workerStub - }); - var proxyGenStub = sinon.stub(openpgp.getWorker(), 'delegate').returns(resolves('proxy_key')); - getWebCryptoAllStub.returns({}); - keyGenStub.returns(rejects(new Error('Native webcrypto keygen failed on purpose :)'))); - - openpgp.generateKey().then(function(newKey) { - expect(keyGenStub.calledOnce).to.be.true; - expect(proxyGenStub.calledOnce).to.be.true; - expect(newKey).to.equal('proxy_key'); - done(); - }); - }); }); describe('generateKey - integration tests', function() { From 979f2123051833dd40fa7d4c9089400f6874110b Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 09:29:41 +0800 Subject: [PATCH 18/30] Cleanup Promise code --- src/key.js | 2 +- src/message.js | 4 ++-- src/openpgp.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/key.js b/src/key.js index ae303f38..abd08d2f 100644 --- a/src/key.js +++ b/src/key.js @@ -933,7 +933,7 @@ export function readArmored(armoredText) { */ export function generate(options) { var packetlist, secretKeyPacket, userIdPacket, dataToSign, signaturePacket, secretSubkeyPacket, subkeySignaturePacket; - return new Promise(resolve => resolve()).then(() => { + return Promise.resolve().then(() => { options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated throw new Error('Only RSA Encrypt or Sign supported'); diff --git a/src/message.js b/src/message.js index 8ac585fe..0cf35287 100644 --- a/src/message.js +++ b/src/message.js @@ -92,7 +92,7 @@ Message.prototype.getSigningKeyIds = function() { * @return {Message} new message with decrypted content */ Message.prototype.decrypt = function(privateKey, sessionKey, password) { - return new Promise(resolve => resolve()).then(() => { + return Promise.resolve().then(() => { const keyObj = sessionKey || this.decryptSessionKey(privateKey, password); if (!keyObj || !util.isUint8Array(keyObj.data) || !util.isString(keyObj.algorithm)) { throw new Error('Invalid session key for decryption.'); @@ -216,7 +216,7 @@ Message.prototype.getText = function() { */ Message.prototype.encrypt = function(keys, passwords) { let symAlgo, msg, symEncryptedPacket; - return new Promise(resolve => resolve()).then(() => { + return Promise.resolve().then(() => { if (keys) { symAlgo = keyModule.getPreferredSymAlgo(keys); } else if (passwords) { diff --git a/src/openpgp.js b/src/openpgp.js index 470bc3e7..d8a312e3 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -162,7 +162,7 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); } - return new Promise(resolve => resolve()).then(() => { + return Promise.resolve().then(() => { let message = createMessage(data, filename); if (privateKeys) { // sign the message only if private keys are specified From 8b46a117ab543374f161c16d911e48fb987c9e8a Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 10:38:00 +0800 Subject: [PATCH 19/30] Prefer aes128 over aes192 (no WebCrypto support) --- src/key.js | 2 +- test/general/key.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/key.js b/src/key.js index abd08d2f..dd261f8d 100644 --- a/src/key.js +++ b/src/key.js @@ -987,8 +987,8 @@ export function generate(options) { signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data]; signaturePacket.preferredSymmetricAlgorithms = []; signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes256); - signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes128); + signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.cast5); signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.tripledes); signaturePacket.preferredHashAlgorithms = []; diff --git a/test/general/key.js b/test/general/key.js index f90de200..4344cfb8 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -599,13 +599,13 @@ var pgp_desktop_priv = expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes256); }); - it('getPreferredSymAlgo() - two key - AES192', function() { + it('getPreferredSymAlgo() - two key - AES128', function() { var keys = openpgp.key.readArmored(twoKeys).keys; var key1 = keys[0]; var key2 = keys[1]; - key2.getPrimaryUser().selfCertificate.preferredSymmetricAlgorithms = [6,8,3]; + key2.getPrimaryUser().selfCertificate.preferredSymmetricAlgorithms = [6,7,3]; var prefAlgo = openpgp.key.getPreferredSymAlgo([key1, key2]); - expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes192); + expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes128); }); it('getPreferredSymAlgo() - two key - one without pref', function() { @@ -626,7 +626,7 @@ var pgp_desktop_priv = expect(key.subKeys[0].bindingSignature.keyFlags[0] & keyFlags.encrypt_communication).to.equal(keyFlags.encrypt_communication); expect(key.subKeys[0].bindingSignature.keyFlags[0] & keyFlags.encrypt_storage).to.equal(keyFlags.encrypt_storage); var sym = openpgp.enums.symmetric; - expect(key.users[0].selfCertifications[0].preferredSymmetricAlgorithms).to.eql([sym.aes256, sym.aes192, sym.aes128, sym.cast5, sym.tripledes]); + expect(key.users[0].selfCertifications[0].preferredSymmetricAlgorithms).to.eql([sym.aes256, sym.aes128, sym.aes192, sym.cast5, sym.tripledes]); var hash = openpgp.enums.hash; expect(key.users[0].selfCertifications[0].preferredHashAlgorithms).to.eql([hash.sha256, hash.sha1, hash.sha512]); var compr = openpgp.enums.compression; From c8569e0cd5d6d96dab8b59faf972d144c57c8e03 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 10:52:34 +0800 Subject: [PATCH 20/30] Add version attribute to the AEAD packet --- src/packet/sym_encrypted_aead_protected.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 36dcfb05..41f44aec 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -34,6 +34,7 @@ const IV_LEN = crypto.gcm.ivLength; // currently only AES-GCM is supported */ export default function SymEncryptedAEADProtected() { this.tag = enums.packet.symEncryptedAEADProtected; + this.version = VERSION; this.iv = null; this.encrypted = null; this.packets = null; @@ -58,7 +59,7 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) { * @return {Uint8Array} The encrypted payload */ SymEncryptedAEADProtected.prototype.write = function () { - return util.concatUint8Array([new Uint8Array([VERSION]), this.iv, this.encrypted]); + return util.concatUint8Array([new Uint8Array([this.version]), this.iv, this.encrypted]); }; /** From f4fc274f14362493da1fe8f259e15ee83bbb2e1c Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 13:24:18 +0800 Subject: [PATCH 21/30] Fix: use worker for CFB w/ webcrypto support --- src/openpgp.js | 13 +++++++++++-- src/util.js | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index d8a312e3..b0b35f4e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -158,7 +158,7 @@ export function decryptKey({ privateKey, passphrase }) { export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true }) { checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); - if (!util.getWebCrypto() && asyncProxy) { // use web worker if web crypto apis are not supported + if (!useAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); } @@ -200,7 +200,7 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8' }) { checkMessage(message); publicKeys = toArray(publicKeys); - if (!util.getWebCrypto() && asyncProxy) { // use web worker if web crypto apis are not supported + if (!useAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format }); } @@ -473,3 +473,12 @@ function onError(message, error) { // rethrow new high level error for api users throw new Error(message + ': ' + error.message); } + +/** + * Check for AES-GCM support and configuration by the user. Note that only browsers that + * implement the current WebCrypto specification support AES-GCM. + * @return {Boolean} If authenticated encryption should be used + */ +function useAEAD() { + return util.getWebCrypto() && config.aead_protect; +} \ No newline at end of file diff --git a/src/util.js b/src/util.js index 4ac04c8e..e937274d 100644 --- a/src/util.js +++ b/src/util.js @@ -450,7 +450,7 @@ export default { }, /** - * Get native Web Cryptography api, only the current versioon of the spec. + * Get native Web Cryptography api, only the current version of the spec. * The default configuration is to use the api when available. But it can * be deactivated with config.useNative * @return {Object} The SubtleCrypto api or 'undefined' From da3fbf89653f09c49e2bd9b946fd653675ad33cd Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 13:25:35 +0800 Subject: [PATCH 22/30] Test CFB, GCM, worker, asm.js, native cases --- test/general/openpgp.js | 47 +++++++++++++++++++++++++++++++------- test/unittests.js | 12 ++++++---- test/worker/async_proxy.js | 16 ++++++++----- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 5a03b3e7..5588aa7f 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -1,4 +1,4 @@ -/* globals tryWorker: true */ +/* globals tryTests: true */ 'use strict'; @@ -403,7 +403,7 @@ describe('OpenPGP.js public api tests', function() { }); describe('encrypt, decrypt, sign, verify - integration tests', function() { - var privateKey, publicKey, zeroCopyVal, useNativeVal; + var privateKey, publicKey, zeroCopyVal, useNativeVal, aead_protectVal; beforeEach(function() { publicKey = openpgp.key.readArmored(pub_key); @@ -414,11 +414,13 @@ describe('OpenPGP.js public api tests', function() { expect(privateKey.err).to.not.exist; zeroCopyVal = openpgp.config.zeroCopy; useNativeVal = openpgp.config.useNative; + aead_protectVal = openpgp.config.aead_protect; }); afterEach(function() { openpgp.config.zeroCopy = zeroCopyVal; openpgp.config.useNative = useNativeVal; + openpgp.config.aead_protect = aead_protectVal; }); it('Decrypting key with wrong passphrase returns false', function () { @@ -429,13 +431,42 @@ describe('OpenPGP.js public api tests', function() { expect(privateKey.keys[0].decrypt(passphrase)).to.be.true; }); - describe('without Worker', tests); + tryTests('CFB mode (asm.js)', tests, { + if: true, + beforeEach: function() { + openpgp.config.useNative = false; + openpgp.config.aead_protect = false; + } + }); - tryWorker('with Worker', tests, function() { - openpgp.config.useNative = false; - openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); - }, function() { - openpgp.destroyWorker(); // cleanup worker in case of failure + tryTests('CFB mode (asm.js, worker)', tests, { + if: typeof window !== 'undefined' && window.Worker, + before: function() { + openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); + }, + beforeEach: function() { + openpgp.config.useNative = false; + openpgp.config.aead_protect = false; + }, + after: function() { + openpgp.destroyWorker(); + } + }); + + tryTests('GCM mode (asm.js)', tests, { + if: true, + beforeEach: function() { + openpgp.config.useNative = false; + openpgp.config.aead_protect = true; + } + }); + + tryTests('GCM mode (native)', tests, { + if: openpgp.util.getWebCrypto(), + beforeEach: function() { + openpgp.config.useNative = true; + openpgp.config.aead_protect = true; + } }); function tests() { diff --git a/test/unittests.js b/test/unittests.js index 9ed7383a..6af30273 100644 --- a/test/unittests.js +++ b/test/unittests.js @@ -6,17 +6,19 @@ return new Promise(function(res, rej) { rej(val); }); }; -(typeof window !== 'undefined' ? window : global).tryWorker = function(name, tests, beforeFn, afterFn) { - if (typeof window !== 'undefined' && window.Worker) { +(typeof window !== 'undefined' ? window : global).tryTests = function(name, tests, options) { + if (options.if) { describe(name, function() { - before(beforeFn); + if (options.before) { before(options.before); } + if (options.beforeEach) { beforeEach(options.beforeEach); } tests(); - after(afterFn); + if (options.afterEach) { afterEach(options.afterEach); } + if (options.after) { after(options.after); } }); } else { - describe.skip(name + ' (No Web Worker support --> skipping tests)', tests); + describe.skip(name + ' (no support --> skipping tests)', tests); } }; diff --git a/test/worker/async_proxy.js b/test/worker/async_proxy.js index 66170871..c90bd71b 100644 --- a/test/worker/async_proxy.js +++ b/test/worker/async_proxy.js @@ -1,4 +1,4 @@ -/* globals tryWorker: true */ +/* globals tryTests: true */ 'use strict'; @@ -35,11 +35,15 @@ var pub_key = var plaintext = 'short message\nnext line\n한국어/조선말'; var pubKey; -tryWorker('Async Proxy', tests, function() { - openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); - pubKey = openpgp.key.readArmored(pub_key).keys[0]; -}, function() { - openpgp.destroyWorker(); +tryTests('Async Proxy', tests, { + if: typeof window !== 'undefined' && window.Worker, + before: function() { + openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); + pubKey = openpgp.key.readArmored(pub_key).keys[0]; + }, + after: function() { + openpgp.destroyWorker(); + } }); function tests() { From 2dce233d10a0745adfa78642173cdd718c0687b9 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 13:34:12 +0800 Subject: [PATCH 23/30] Fix: activate native tests under node.js --- test/general/openpgp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 5588aa7f..913c184f 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -462,7 +462,7 @@ describe('OpenPGP.js public api tests', function() { }); tryTests('GCM mode (native)', tests, { - if: openpgp.util.getWebCrypto(), + if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), beforeEach: function() { openpgp.config.useNative = true; openpgp.config.aead_protect = true; From 969e39dcf207761912f1539060d02d16315201d3 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 13:49:26 +0800 Subject: [PATCH 24/30] Rename useAEAD --> nativeAEAD --- src/openpgp.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/openpgp.js b/src/openpgp.js index b0b35f4e..0a5cacf3 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -158,7 +158,7 @@ export function decryptKey({ privateKey, passphrase }) { export function encrypt({ data, publicKeys, privateKeys, passwords, filename, armor=true }) { checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); - if (!useAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported + if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, filename, armor }); } @@ -200,7 +200,7 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, filename, ar export function decrypt({ message, privateKey, publicKeys, sessionKey, password, format='utf8' }) { checkMessage(message); publicKeys = toArray(publicKeys); - if (!useAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported + if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported return asyncProxy.delegate('decrypt', { message, privateKey, publicKeys, sessionKey, password, format }); } @@ -475,10 +475,10 @@ function onError(message, error) { } /** - * Check for AES-GCM support and configuration by the user. Note that only browsers that - * implement the current WebCrypto specification support AES-GCM. + * Check for AES-GCM support and configuration by the user. Only browsers that + * implement the current WebCrypto specification support native AES-GCM. * @return {Boolean} If authenticated encryption should be used */ -function useAEAD() { +function nativeAEAD() { return util.getWebCrypto() && config.aead_protect; } \ No newline at end of file From d95282977f33912e2f51a25316db697d2e2db519 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 15:09:53 +0800 Subject: [PATCH 25/30] Add GCM config usage in README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57059843..a9186c6d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch * If the user's browser supports [native WebCrypto](http://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.useNative = false`. -* For environments that don't provide native crypto, the library falls back to [asm.js](http://caniuse.com/#feat=asmjs) implementations of AES-CFB, 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). +* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ford-openpgp-format-00) for authenticated encryption using native AES-GCM. This makes symmetric encryption about 30x faster on supported platforms, but is currently incompatible with OpenPGP implementations like GnuPG that do not support GCM yet. You can deactivate this feature by setting `openpgp.config.aead_protect = false` for standard CFB mode. + +* For environments that don't provide native crypto, the library falls back to [asm.js](http://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). ### Getting started From 72c8bf5ea79d3b7b4ab040a2ee3e0751426dcfe0 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 20:47:42 +0800 Subject: [PATCH 26/30] Deactivate GCM by default --- README.md | 3 ++- src/config/config.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9186c6d..8aee81e5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch * If the user's browser supports [native WebCrypto](http://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.useNative = false`. -* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ford-openpgp-format-00) for authenticated encryption using native AES-GCM. This makes symmetric encryption about 30x faster on supported platforms, but is currently incompatible with OpenPGP implementations like GnuPG that do not support GCM yet. You can deactivate this feature by setting `openpgp.config.aead_protect = false` for standard CFB mode. +* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ford-openpgp-format-00) for authenticated encryption using native AES-GCM provided by WebCrypto/Node. This makes symmetric encryption about 30x faster on supported platforms (with hardware acceleration). Since the specification has not been finalized and other OpenPGP implementations like GnuPG have not adopted it yet, the feature is currently hidden behind a flag. You can activate it by setting `openpgp.config.aead_protect = true`. * For environments that don't provide native crypto, the library falls back to [asm.js](http://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). @@ -49,6 +49,7 @@ Here are some examples of how to use the v2.x api. For more elaborate examples a 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 native AES-GCM ([see Performance](https://github.com/openpgpjs/openpgpjs#performance)) ``` #### Encrypt and decrypt *String* data with a password diff --git a/src/config/config.js b/src/config/config.js index 4cd0a284..317c9ae1 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -37,7 +37,7 @@ export default { prefer_hash_algorithm: enums.hash.sha256, encryption_cipher: enums.symmetric.aes256, compression: enums.compression.zip, - aead_protect: true, // use Authenticated Encryption with Additional Data (AEAD) protection for symmetric encryption + aead_protect: false, // use Authenticated Encryption with Additional Data (AEAD) protection for symmetric encryption integrity_protect: true, // use integrity protection for symmetric encryption ignore_mdc_error: false, // fail on decrypt if message is not integrity protected rsa_blinding: true, From 843fba0d40477aa494c4cd29e31ddcb11b4e66f6 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 20:58:52 +0800 Subject: [PATCH 27/30] =?UTF-8?q?Use=20underscore=20instead=20of=20camelca?= =?UTF-8?q?se=20in=20config=20zeroCopy=20=E2=80=94>=20zero=5Fcopy=20useNat?= =?UTF-8?q?ive=20=E2=80=94>=20use=5Fnative=20Remove=20unnecessary=20tests?= =?UTF-8?q?=20from=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/config/config.js | 4 +- src/crypto/gcm.js | 8 ++-- .../sym_encrypted_integrity_protected.js | 4 +- src/util.js | 14 +++---- test/crypto/crypto.js | 16 ++++---- test/general/openpgp.js | 38 ++++++++----------- test/general/util.js | 12 +++--- 8 files changed, 45 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 8aee81e5..97fb0214 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch * 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. -* If the user's browser supports [native WebCrypto](http://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.useNative = false`. +* If the user's browser supports [native WebCrypto](http://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 provided by WebCrypto/Node. This makes symmetric encryption about 30x faster on supported platforms (with hardware acceleration). Since the specification has not been finalized and other OpenPGP implementations like GnuPG have not adopted it yet, the feature is currently hidden behind a flag. You can activate it by setting `openpgp.config.aead_protect = true`. diff --git a/src/config/config.js b/src/config/config.js index 317c9ae1..6a070b76 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -41,8 +41,8 @@ export default { integrity_protect: true, // use integrity protection for symmetric encryption ignore_mdc_error: false, // fail on decrypt if message is not integrity protected rsa_blinding: true, - useNative: true, // use native node.js crypto and Web Crypto apis (if available) - zeroCopy: false, // use transferable objects between the Web Worker and main thread + use_native: true, // use native node.js crypto and Web Crypto apis (if available) + zero_copy: false, // use transferable objects between the Web Worker and main thread debug: false, show_version: true, show_comment: true, diff --git a/src/crypto/gcm.js b/src/crypto/gcm.js index 663dfbfd..b06b7318 100644 --- a/src/crypto/gcm.js +++ b/src/crypto/gcm.js @@ -46,9 +46,9 @@ export function encrypt(cipher, plaintext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - if (webCrypto && config.useNative && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + if (webCrypto && config.use_native && 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 && config.useNative) { // Node crypto library + } else if (nodeCrypto && config.use_native) { // Node crypto library return nodeEncrypt(plaintext, key, iv) ; } else { // asm.js fallback return Promise.resolve(asmCrypto.AES_GCM.encrypt(plaintext, key, iv)); @@ -68,9 +68,9 @@ export function decrypt(cipher, ciphertext, key, iv) { return Promise.reject(new Error('GCM mode supports only AES cipher')); } - if (webCrypto && config.useNative && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support + if (webCrypto && config.use_native && 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 && config.useNative) { // Node crypto library + } else if (nodeCrypto && config.use_native) { // Node crypto library return nodeDecrypt(ciphertext, key, iv); } else { // asm.js fallback return Promise.resolve(asmCrypto.AES_GCM.decrypt(ciphertext, key, iv)); diff --git a/src/packet/sym_encrypted_integrity_protected.js b/src/packet/sym_encrypted_integrity_protected.js index 415bf789..8347e2ae 100644 --- a/src/packet/sym_encrypted_integrity_protected.js +++ b/src/packet/sym_encrypted_integrity_protected.js @@ -95,7 +95,7 @@ SymEncryptedIntegrityProtected.prototype.encrypt = function (sessionKeyAlgorithm if(sessionKeyAlgorithm.substr(0,3) === 'aes') { // AES optimizations. Native code for node, asmCrypto for browser. var blockSize = crypto.cipher[sessionKeyAlgorithm].blockSize; - if(nodeCrypto) { // Node crypto library. Only loaded if config.useNative === true + if(nodeCrypto) { // Node crypto library. Only loaded if config.use_native === true var cipherObj = new nodeCrypto.createCipheriv('aes-' + sessionKeyAlgorithm.substr(3,3) + '-cfb', new Buffer(key), new Buffer(new Uint8Array(blockSize))); this.encrypted = new Uint8Array(cipherObj.update(new Buffer(util.concatUint8Array([prefix, tohash])))); @@ -127,7 +127,7 @@ SymEncryptedIntegrityProtected.prototype.decrypt = function (sessionKeyAlgorithm if(sessionKeyAlgorithm.substr(0,3) === 'aes') { // AES optimizations. Native code for node, asmCrypto for browser. var blockSize = crypto.cipher[sessionKeyAlgorithm].blockSize; - if(nodeCrypto) { // Node crypto library. Only loaded if config.useNative === true + if(nodeCrypto) { // Node crypto library. Only loaded if config.use_native === true var decipherObj = new nodeCrypto.createDecipheriv('aes-' + sessionKeyAlgorithm.substr(3,3) + '-cfb', new Buffer(key), new Buffer(new Uint8Array(blockSize))); decrypted = new Uint8Array(decipherObj.update(new Buffer(this.encrypted))); diff --git a/src/util.js b/src/util.js index e937274d..32f22445 100644 --- a/src/util.js +++ b/src/util.js @@ -61,7 +61,7 @@ export default { * @return {Array} an array of binary data to be passed */ getTransferables: function(obj) { - if (config.zeroCopy && Object.prototype.isPrototypeOf(obj)) { + if (config.zero_copy && Object.prototype.isPrototypeOf(obj)) { const transferables = []; this.collectBuffers(obj, transferables); return transferables.length ? transferables : undefined; @@ -452,11 +452,11 @@ export default { /** * Get native Web Cryptography api, only the current version of the spec. * The default configuration is to use the api when available. But it can - * be deactivated with config.useNative + * be deactivated with config.use_native * @return {Object} The SubtleCrypto api or 'undefined' */ getWebCrypto: function() { - if (!config.useNative) { + if (!config.use_native) { return; } @@ -467,11 +467,11 @@ export default { * Get native Web Cryptography api for all browsers, including legacy * implementations of the spec e.g IE11 and Safari 8/9. The default * configuration is to use the api when available. But it can be deactivated - * with config.useNative + * with config.use_native * @return {Object} The SubtleCrypto api or 'undefined' */ getWebCryptoAll: function() { - if (!config.useNative) { + if (!config.use_native) { return; } @@ -528,11 +528,11 @@ export default { /** * Get native Node.js crypto api. The default configuration is to use - * the api when available. But it can also be deactivated with config.useNative + * the api when available. But it can also be deactivated with config.use_native * @return {Object} The crypto module or 'undefined' */ getNodeCrypto: function() { - if (!this.detectNode() || !config.useNative) { + if (!this.detectNode() || !config.use_native) { return; } diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 67b0f0c1..c4dda83f 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -328,26 +328,26 @@ describe('API functional testing', function() { }); describe('Symmetric AES-GCM (native)', function() { - var useNativeVal; + var use_nativeVal; beforeEach(function() { - useNativeVal = openpgp.config.useNative; - openpgp.config.useNative = true; + use_nativeVal = openpgp.config.use_native; + openpgp.config.use_native = true; }); afterEach(function() { - openpgp.config.useNative = useNativeVal; + openpgp.config.use_native = use_nativeVal; }); testAESGCM("12345678901234567890123456789012345678901234567890"); }); describe('Symmetric AES-GCM (asm.js fallback)', function() { - var useNativeVal; + var use_nativeVal; beforeEach(function() { - useNativeVal = openpgp.config.useNative; - openpgp.config.useNative = false; + use_nativeVal = openpgp.config.use_native; + openpgp.config.use_native = false; }); afterEach(function() { - openpgp.config.useNative = useNativeVal; + openpgp.config.use_native = use_nativeVal; }); testAESGCM("12345678901234567890123456789012345678901234567890"); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 913c184f..82544c8d 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -342,19 +342,19 @@ describe('OpenPGP.js public api tests', function() { }); describe('generateKey - integration tests', function() { - var useNativeVal; + var use_nativeVal; beforeEach(function() { - useNativeVal = openpgp.config.useNative; + use_nativeVal = openpgp.config.use_native; }); afterEach(function() { - openpgp.config.useNative = useNativeVal; + openpgp.config.use_native = use_nativeVal; openpgp.destroyWorker(); }); it('should work in JS (without worker)', function(done) { - openpgp.config.useNative = false; + openpgp.config.use_native = false; openpgp.destroyWorker(); var opt = { userIds: [{ name: 'Test User', email: 'text@example.com' }], @@ -370,7 +370,7 @@ describe('OpenPGP.js public api tests', function() { }); it('should work in JS (with worker)', function(done) { - openpgp.config.useNative = false; + openpgp.config.use_native = false; openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); var opt = { userIds: [{ name: 'Test User', email: 'text@example.com' }], @@ -386,7 +386,7 @@ describe('OpenPGP.js public api tests', function() { }); it('should work in with native crypto', function(done) { - openpgp.config.useNative = true; + openpgp.config.use_native = true; var opt = { userIds: [{ name: 'Test User', email: 'text@example.com' }], numBits: 512 @@ -403,7 +403,7 @@ describe('OpenPGP.js public api tests', function() { }); describe('encrypt, decrypt, sign, verify - integration tests', function() { - var privateKey, publicKey, zeroCopyVal, useNativeVal, aead_protectVal; + var privateKey, publicKey, zero_copyVal, use_nativeVal, aead_protectVal; beforeEach(function() { publicKey = openpgp.key.readArmored(pub_key); @@ -412,14 +412,14 @@ describe('OpenPGP.js public api tests', function() { privateKey = openpgp.key.readArmored(priv_key); expect(privateKey.keys).to.have.length(1); expect(privateKey.err).to.not.exist; - zeroCopyVal = openpgp.config.zeroCopy; - useNativeVal = openpgp.config.useNative; + zero_copyVal = openpgp.config.zero_copy; + use_nativeVal = openpgp.config.use_native; aead_protectVal = openpgp.config.aead_protect; }); afterEach(function() { - openpgp.config.zeroCopy = zeroCopyVal; - openpgp.config.useNative = useNativeVal; + openpgp.config.zero_copy = zero_copyVal; + openpgp.config.use_native = use_nativeVal; openpgp.config.aead_protect = aead_protectVal; }); @@ -434,7 +434,7 @@ describe('OpenPGP.js public api tests', function() { tryTests('CFB mode (asm.js)', tests, { if: true, beforeEach: function() { - openpgp.config.useNative = false; + openpgp.config.use_native = true; openpgp.config.aead_protect = false; } }); @@ -445,7 +445,7 @@ describe('OpenPGP.js public api tests', function() { openpgp.initWorker({ path:'../dist/openpgp.worker.js' }); }, beforeEach: function() { - openpgp.config.useNative = false; + openpgp.config.use_native = true; openpgp.config.aead_protect = false; }, after: function() { @@ -453,18 +453,10 @@ describe('OpenPGP.js public api tests', function() { } }); - tryTests('GCM mode (asm.js)', tests, { - if: true, - beforeEach: function() { - openpgp.config.useNative = false; - openpgp.config.aead_protect = true; - } - }); - tryTests('GCM mode (native)', tests, { if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(), beforeEach: function() { - openpgp.config.useNative = true; + openpgp.config.use_native = true; openpgp.config.aead_protect = true; } }); @@ -893,7 +885,7 @@ describe('OpenPGP.js public api tests', function() { }); it('should encrypt and decrypt with binary data and transferable objects', function(done) { - openpgp.config.zeroCopy = true; // activate transferable objects + openpgp.config.zero_copy = true; // activate transferable objects var encOpt = { data: new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01]), passwords: password1, diff --git a/test/general/util.js b/test/general/util.js index 7ea3441c..d805eef1 100644 --- a/test/general/util.js +++ b/test/general/util.js @@ -145,7 +145,7 @@ describe('Util unit tests', function() { }); describe('getTransferables', function() { - var zeroCopyVal, + var zero_copyVal, buf1 = new Uint8Array(1), buf2 = new Uint8Array(1), obj = { @@ -157,16 +157,16 @@ describe('Util unit tests', function() { }; beforeEach(function() { - zeroCopyVal = openpgp.config.zeroCopy; - openpgp.config.zeroCopy = true; + zero_copyVal = openpgp.config.zero_copy; + openpgp.config.zero_copy = true; }); afterEach(function() { - openpgp.config.zeroCopy = zeroCopyVal; + openpgp.config.zero_copy = zero_copyVal; }); - it('should return undefined when zeroCopy is false', function() { - openpgp.config.zeroCopy = false; + it('should return undefined when zero_copy is false', function() { + openpgp.config.zero_copy = false; expect(openpgp.util.getTransferables(obj)).to.be.undefined; }); it('should return undefined for no input', function() { From c42556920e9d7e3e3f65f2a383dc7ed2e5b32cf3 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 24 Mar 2016 21:29:37 +0800 Subject: [PATCH 28/30] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 97fb0214..73b5f6e9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch * If the user's browser supports [native WebCrypto](http://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 provided by WebCrypto/Node. This makes symmetric encryption about 30x faster on supported platforms (with hardware acceleration). Since the specification has not been finalized and other OpenPGP implementations like GnuPG have not adopted it yet, the feature is currently hidden behind a flag. You can activate it by setting `openpgp.config.aead_protect = true`. +* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ford-openpgp-format-00) for authenticated encryption using native AES-GCM. 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`. * For environments that don't provide native crypto, the library falls back to [asm.js](http://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). @@ -49,7 +49,7 @@ Here are some examples of how to use the v2.x api. For more elaborate examples a 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 native AES-GCM ([see Performance](https://github.com/openpgpjs/openpgpjs#performance)) +openpgp.config.aead_protect = true // activate fast AES-GCM mode (experimental) ``` #### Encrypt and decrypt *String* data with a password From cb89069002cdc589a267c236571853d33e35c8b8 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 25 Mar 2016 11:04:02 +0800 Subject: [PATCH 29/30] Add unstable IE11 travis build to allow_failures[D --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3b96a0ea..2114a728 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,10 +19,10 @@ matrix: env: OPENPGPJSTEST='end2end-4' BROWSER='chrome 46' - node_js: "4" env: OPENPGPJSTEST='end2end-1' BROWSER='firefox 42' - - node_js: "4" - env: OPENPGPJSTEST='end2end-6' BROWSER='internet explorer 11' - node_js: "4" env: OPENPGPJSTEST='end2end-9' BROWSER='safari 9' + - node_js: "4" + env: OPENPGPJSTEST='end2end-6' BROWSER='internet explorer 11' - node_js: "4" env: OPENPGPJSTEST='end2end-0' BROWSER='firefox 38' - node_js: "4" @@ -48,6 +48,7 @@ matrix: - env: OPENPGPJSTEST='end2end-2' BROWSER='firefox beta' - env: OPENPGPJSTEST='end2end-3' BROWSER='chrome 38' - env: OPENPGPJSTEST='end2end-5' BROWSER='chrome beta' + - env: OPENPGPJSTEST='end2end-6' BROWSER='internet explorer 11' - env: OPENPGPJSTEST='end2end-7' BROWSER='microsoft edge 20.10240' - env: OPENPGPJSTEST='end2end-8' BROWSER='safari 8' - env: OPENPGPJSTEST='end2end-10' BROWSER='android 4.4' From 031218313e346ceb072f173b42678d8c2fbb7b31 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 25 Mar 2016 14:34:33 +0800 Subject: [PATCH 30/30] Minor Update to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 73b5f6e9..235a7f6e 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Here are some examples of how to use the v2.x api. For more elaborate examples a 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 (experimental) ```