From ded8926b27aeba8cbf10c5c9521f110bfac3cdbc Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Tue, 22 Mar 2016 02:41:10 +0800 Subject: [PATCH] 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; + }); +};