504 lines
12 KiB
JavaScript
504 lines
12 KiB
JavaScript
|
/*!
|
||
|
* 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;
|