/*! * algorithms/aes-cbc-hmac-sha2.js - AES-CBC-HMAC-SHA2 Composited Encryption * * Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file. */ "use strict"; var helpers = require("./helpers.js"), HMAC = require("./hmac.js"), sha = require("./sha.js"), forge = require("../deps/forge.js"), DataBuffer = require("../util/databuffer.js"), util = require("../util"); function checkIv(iv) { if (16 !== iv.length) { throw new Error("invalid iv"); } } function commonCbcEncryptFN(size) { // ### 'fallback' implementation -- uses forge var fallback = function(encKey, pdata, iv) { try { checkIv(iv); } catch (err) { return Promise.reject(err); } var promise = Promise.resolve(); promise = promise.then(function() { var cipher = forge.cipher.createCipher("AES-CBC", new DataBuffer(encKey)); cipher.start({ iv: new DataBuffer(iv) }); // TODO: chunk data cipher.update(new DataBuffer(pdata)); if (!cipher.finish()) { return Promise.reject(new Error("encryption failed")); } var cdata = Buffer.from(cipher.output.bytes(), "binary"); return cdata; }); return promise; }; // ### WebCryptoAPI implementation // TODO: cache CryptoKey sooner var webcrypto = function(encKey, pdata, iv) { try { checkIv(iv); } catch (err) { return Promise.reject(err); } var promise = Promise.resolve(); promise = promise.then(function() { var alg = { name: "AES-CBC" }; return helpers.subtleCrypto.importKey("raw", encKey, alg, true, ["encrypt"]); }); promise = promise.then(function(key) { var alg = { name: "AES-CBC", iv: iv }; return helpers.subtleCrypto.encrypt(alg, key, pdata); }); promise = promise.then(function(cdata) { cdata = Buffer.from(cdata); return cdata; }); return promise; }; // ### NodeJS implementation var nodejs = function(encKey, pdata, iv) { try { checkIv(iv); } catch (err) { return Promise.reject(err); } var promise = Promise.resolve(pdata); promise = promise.then(function(pdata) { var name = "AES-" + size + "-CBC"; var cipher = helpers.nodeCrypto.createCipheriv(name, encKey, iv); var cdata = Buffer.concat([ cipher.update(pdata), cipher.final() ]); return cdata; }); return promise; }; return helpers.setupFallback(nodejs, webcrypto, fallback); } function commonCbcDecryptFN(size) { // ### 'fallback' implementation -- uses forge var fallback = function(encKey, cdata, iv) { // validate inputs try { checkIv(iv); } catch (err) { return Promise.reject(err); } var promise = Promise.resolve(); promise = promise.then(function() { var cipher = forge.cipher.createDecipher("AES-CBC", new DataBuffer(encKey)); cipher.start({ iv: new DataBuffer(iv) }); // TODO: chunk data cipher.update(new DataBuffer(cdata)); if (!cipher.finish()) { return Promise.reject(new Error("encryption failed")); } var pdata = Buffer.from(cipher.output.bytes(), "binary"); return pdata; }); return promise; }; // ### WebCryptoAPI implementation // TODO: cache CryptoKey sooner var webcrypto = function(encKey, cdata, iv) { // validate inputs try { checkIv(iv); } catch (err) { return Promise.reject(err); } var promise = Promise.resolve(); promise = promise.then(function() { var alg = { name: "AES-CBC" }; return helpers.subtleCrypto.importKey("raw", encKey, alg, true, ["decrypt"]); }); promise = promise.then(function(key) { var alg = { name: "AES-CBC", iv: iv }; return helpers.subtleCrypto.decrypt(alg, key, cdata); }); promise = promise.then(function(pdata) { pdata = Buffer.from(pdata); return pdata; }); return promise; }; // ### NodeJS implementation var nodejs = function(encKey, cdata, iv) { // validate inputs try { checkIv(iv); } catch (err) { return Promise.reject(err); } var promise = Promise.resolve(); promise = promise.then(function() { var name = "AES-" + size + "-CBC"; var cipher = helpers.nodeCrypto.createDecipheriv(name, encKey, iv); var pdata = Buffer.concat([ cipher.update(cdata), cipher.final() ]); return pdata; }); return promise; }; return helpers.setupFallback(nodejs, webcrypto, fallback); } function checkKey(key, size) { if ((size << 1) !== (key.length << 3)) { throw new Error("invalid encryption key size"); } } function cbcHmacEncryptFN(size) { var commonEncrypt = commonCbcEncryptFN(size); return function(key, pdata, props) { // validate inputs try { checkKey(key, size); } catch (err) { return Promise.reject(err); } var eKey = key.slice(size / 8), iKey = key.slice(0, size / 8), iv = props.iv || Buffer.alloc(0), adata = props.aad || props.adata || Buffer.alloc(0); // STEP 1 -- Encrypt var promise = commonEncrypt(eKey, pdata, iv); // STEP 2 -- MAC promise = promise.then(function(cdata){ var mdata = Buffer.concat([ adata, iv, cdata, helpers.int64ToBuffer(adata.length * 8) ]); var promise; promise = HMAC["HS" + (size * 2)].sign(iKey, mdata, { length: size }); promise = promise.then(function(result) { // TODO: move slice to hmac.js var tag = result.mac.slice(0, size / 8); return { data: cdata, tag: tag }; }); return promise; }); return promise; }; } function cbcHmacDecryptFN(size) { var commonDecrypt = commonCbcDecryptFN(size); return function(key, cdata, props) { // validate inputs try { checkKey(key, size); } catch (err) { return Promise.reject(err); } var eKey = key.slice(size / 8), iKey = key.slice(0, size / 8), iv = props.iv || Buffer.alloc(0), adata = props.aad || props.adata || Buffer.alloc(0), tag = props.tag || props.mac || Buffer.alloc(0); var promise = Promise.resolve(); // STEP 1 -- MAC promise = promise.then(function() { var promise; // construct MAC input var mdata = Buffer.concat([ adata, iv, cdata, helpers.int64ToBuffer(adata.length * 8) ]); promise = HMAC["HS" + (size * 2)].verify(iKey, mdata, tag, { length: size }); promise = promise.then(function() { return cdata; }, function() { // failure -- invalid tag error throw new Error("mac check failed"); }); return promise; }); // STEP 2 -- Decrypt promise = promise.then(function(){ return commonDecrypt(eKey, cdata, iv); }); return promise; }; } var EncryptionLabel = Buffer.from("Encryption", "utf8"); var IntegrityLabel = Buffer.from("Integrity", "utf8"); var DotLabel = Buffer.from(".", "utf8"); function generateCek(masterKey, alg, epu, epv) { var masterSize = masterKey.length * 8; var cekSize = masterSize / 2; var promise = Promise.resolve(); promise = promise.then(function(){ var input = Buffer.concat([ helpers.int32ToBuffer(1), masterKey, helpers.int32ToBuffer(cekSize), Buffer.from(alg, "utf8"), epu, epv, EncryptionLabel ]); return input; }); promise = promise.then( function(input) { return sha["SHA-" + masterSize].digest(input).then(function(digest) { return digest.slice(0, cekSize / 8); }); }); promise = Promise.resolve(promise); return promise; } function generateCik(masterKey, alg, epu, epv) { var masterSize = masterKey.length * 8; var cikSize = masterSize; var promise = Promise.resolve(); promise = promise.then(function(){ var input = Buffer.concat([ helpers.int32ToBuffer(1), masterKey, helpers.int32ToBuffer(cikSize), Buffer.from(alg, "utf8"), epu, epv, IntegrityLabel ]); return input; }); promise = promise.then( function(input) { return sha["SHA-" + masterSize].digest(input).then(function(digest) { return digest.slice(0, cikSize / 8); }); }); promise = Promise.resolve(promise); return promise; } function concatKdfCbcHmacEncryptFN(size, alg) { var commonEncrypt = commonCbcEncryptFN(size); return function(key, pdata, props) { var epu = props.epu || helpers.int32ToBuffer(0), epv = props.epv || helpers.int32ToBuffer(0), iv = props.iv || Buffer.alloc(0), adata = props.aad || props.adata || Buffer.alloc(0), kdata = props.kdata || Buffer.alloc(0); // Pre Step 1 -- Generate Keys var promises = [ generateCek(key, alg, epu, epv), generateCik(key, alg, epu, epv) ]; var cek, cik; var promise = Promise.all(promises).then(function(keys) { cek = keys[0]; cik = keys[1]; }); // STEP 1 -- Encrypt promise = promise.then(function(){ return commonEncrypt(cek, pdata, iv); }); // STEP 2 -- Mac promise = promise.then(function(cdata){ var mdata = Buffer.concat([ adata, DotLabel, Buffer.from(kdata), DotLabel, Buffer.from(util.base64url.encode(iv), "utf8"), DotLabel, Buffer.from(util.base64url.encode(cdata), "utf8") ]); return Promise.all([ Promise.resolve(cdata), HMAC["HS" + (size * 2)].sign(cik, mdata, { length: size }) ]); }); promise = promise.then(function(result){ return { data: result[0], tag: result[1].mac }; }); return promise; }; } function concatKdfCbcHmacDecryptFN(size, alg) { var commonDecrypt = commonCbcDecryptFN(size); return function(key, cdata, props) { var epu = props.epu || helpers.int32ToBuffer(0), epv = props.epv || helpers.int32ToBuffer(0), iv = props.iv || Buffer.alloc(0), adata = props.aad || props.adata || Buffer.alloc(0), kdata = props.kdata || Buffer.alloc(0), tag = props.tag || props.mac || Buffer.alloc(0); // Pre Step 1 -- Generate Keys var promises = [ generateCek(key, alg, epu, epv), generateCik(key, alg, epu, epv) ]; var cek, cik; var promise = Promise.all(promises).then(function(keys){ cek = keys[0]; cik = keys[1]; }); // STEP 1 -- MAC promise = promise.then(function() { // construct MAC input var mdata = Buffer.concat([ adata, DotLabel, Buffer.from(kdata), DotLabel, Buffer.from(util.base64url.encode(iv), "utf8"), DotLabel, Buffer.from(util.base64url.encode(cdata), "utf8") ]); try { return HMAC["HS" + (size * 2)].verify(cik, mdata, tag, { loose: false }); } catch (e) { throw new Error("mac check failed"); } }); // STEP 2 -- Decrypt promise = promise.then(function(){ return commonDecrypt(cek, cdata, iv); }); return promise; }; } // ### Public API // * [name].encrypt // * [name].decrypt var aesCbcHmacSha2 = {}; [ "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ].forEach(function(alg) { var size = parseInt(/A(\d+)CBC-HS(\d+)?/g.exec(alg)[1]); aesCbcHmacSha2[alg] = { encrypt: cbcHmacEncryptFN(size), decrypt: cbcHmacDecryptFN(size) }; }); [ "A128CBC+HS256", "A192CBC+HS384", "A256CBC+HS512" ].forEach(function(alg) { var size = parseInt(/A(\d+)CBC\+HS(\d+)?/g.exec(alg)[1]); aesCbcHmacSha2[alg] = { encrypt: concatKdfCbcHmacEncryptFN(size, alg), decrypt: concatKdfCbcHmacDecryptFN(size, alg) }; }); module.exports = aesCbcHmacSha2;