289 lines
7.3 KiB
JavaScript
289 lines
7.3 KiB
JavaScript
/*!
|
|
* algorithms/aes-kw.js - AES-KW Key-Wrapping
|
|
*
|
|
* Copyright (c) 2015 Cisco Systems, Inc. See LICENSE file.
|
|
*/
|
|
"use strict";
|
|
|
|
var helpers = require("./helpers.js"),
|
|
forge = require("../deps/forge.js"),
|
|
DataBuffer = require("../util/databuffer.js");
|
|
|
|
var A0 = Buffer.from("a6a6a6a6a6a6a6a6", "hex");
|
|
|
|
// ### helpers
|
|
function xor(a, b) {
|
|
var len = Math.max(a.length, b.length);
|
|
var result = Buffer.alloc(len);
|
|
for (var idx = 0; len > idx; idx++) {
|
|
result[idx] = (a[idx] || 0) ^ (b[idx] || 0);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function split(input, size) {
|
|
var output = [];
|
|
for (var idx = 0; input.length > idx; idx += size) {
|
|
output.push(input.slice(idx, idx + size));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function longToBigEndian(input) {
|
|
var hi = Math.floor(input / 4294967296),
|
|
lo = input % 4294967296;
|
|
var output = Buffer.alloc(8);
|
|
output[0] = 0xff & (hi >>> 24);
|
|
output[1] = 0xff & (hi >>> 16);
|
|
output[2] = 0xff & (hi >>> 8);
|
|
output[3] = 0xff & (hi >>> 0);
|
|
output[4] = 0xff & (lo >>> 24);
|
|
output[5] = 0xff & (lo >>> 16);
|
|
output[6] = 0xff & (lo >>> 8);
|
|
output[7] = 0xff & (lo >>> 0);
|
|
return output;
|
|
}
|
|
|
|
function kwEncryptFN(size) {
|
|
function commonChecks(key, data) {
|
|
if (size !== (key.length << 3)) {
|
|
throw new Error("invalid key size");
|
|
}
|
|
if (0 < data.length && 0 !== (data.length % 8)) {
|
|
throw new Error("invalid data length");
|
|
}
|
|
}
|
|
|
|
// ### 'fallback' implementation -- uses forge
|
|
var fallback = function(key, pdata) {
|
|
try {
|
|
commonChecks(key, pdata);
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
// setup cipher
|
|
var cipher = forge.cipher.createCipher("AES", new DataBuffer(key));
|
|
|
|
// split input into chunks
|
|
var R = split(pdata, 8);
|
|
var A,
|
|
B,
|
|
count;
|
|
A = A0;
|
|
for (var jdx = 0; 6 > jdx; jdx++) {
|
|
for (var idx = 0; R.length > idx; idx++) {
|
|
count = (R.length * jdx) + idx + 1;
|
|
B = Buffer.concat([A, R[idx]]);
|
|
cipher.start();
|
|
cipher.update(new DataBuffer(B));
|
|
cipher.finish();
|
|
B = Buffer.from(cipher.output.bytes(), "binary");
|
|
|
|
A = xor(B.slice(0, 8),
|
|
longToBigEndian(count));
|
|
R[idx] = B.slice(8, 16);
|
|
}
|
|
}
|
|
R = [A].concat(R);
|
|
var cdata = Buffer.concat(R);
|
|
return Promise.resolve({
|
|
data: cdata
|
|
});
|
|
};
|
|
// ### WebCryptoAPI implementation
|
|
var webcrypto = function(key, pdata) {
|
|
try {
|
|
commonChecks(key, pdata);
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
var alg = {
|
|
name: "AES-KW"
|
|
};
|
|
var promise = [
|
|
helpers.subtleCrypto.importKey("raw", pdata, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]),
|
|
helpers.subtleCrypto.importKey("raw", key, alg, true, ["wrapKey"])
|
|
];
|
|
promise = Promise.all(promise);
|
|
promise = promise.then(function(keys) {
|
|
return helpers.subtleCrypto.wrapKey("raw",
|
|
keys[0], // key
|
|
keys[1], // wrappingKey
|
|
alg);
|
|
});
|
|
promise = promise.then(function(result) {
|
|
result = Buffer.from(result);
|
|
|
|
return {
|
|
data: result
|
|
};
|
|
});
|
|
return promise;
|
|
};
|
|
var node = function(key, pdata) {
|
|
try {
|
|
commonChecks(key, pdata);
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
// split input into chunks
|
|
var R = split(pdata, 8),
|
|
iv = Buffer.alloc(16);
|
|
var A,
|
|
B,
|
|
count;
|
|
A = A0;
|
|
for (var jdx = 0; 6 > jdx; jdx++) {
|
|
for (var idx = 0; R.length > idx; idx++) {
|
|
count = (R.length * jdx) + idx + 1;
|
|
B = Buffer.concat([A, R[idx]]);
|
|
var cipher = helpers.nodeCrypto.createCipheriv("AES" + size, key, iv);
|
|
B = cipher.update(B);
|
|
|
|
A = xor(B.slice(0, 8),
|
|
longToBigEndian(count));
|
|
R[idx] = B.slice(8, 16);
|
|
}
|
|
}
|
|
R = [A].concat(R);
|
|
var cdata = Buffer.concat(R);
|
|
return Promise.resolve({
|
|
data: cdata
|
|
});
|
|
};
|
|
|
|
return helpers.setupFallback(node, webcrypto, fallback);
|
|
}
|
|
function kwDecryptFN(size) {
|
|
function commonChecks(key, data) {
|
|
if (size !== (key.length << 3)) {
|
|
throw new Error("invalid key size");
|
|
}
|
|
if (0 < (data.length - 8) && 0 !== (data.length % 8)) {
|
|
throw new Error("invalid data length");
|
|
}
|
|
}
|
|
|
|
// ### 'fallback' implementation -- uses forge
|
|
var fallback = function(key, cdata) {
|
|
try {
|
|
commonChecks(key, cdata);
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
// setup cipher
|
|
var cipher = forge.cipher.createDecipher("AES", new DataBuffer(key));
|
|
|
|
// prepare inputs
|
|
var R = split(cdata, 8),
|
|
A,
|
|
B,
|
|
count;
|
|
A = R[0];
|
|
R = R.slice(1);
|
|
for (var jdx = 5; 0 <= jdx; --jdx) {
|
|
for (var idx = R.length - 1; 0 <= idx; --idx) {
|
|
count = (R.length * jdx) + idx + 1;
|
|
B = xor(A,
|
|
longToBigEndian(count));
|
|
B = Buffer.concat([B, R[idx]]);
|
|
cipher.start();
|
|
cipher.update(new DataBuffer(B));
|
|
cipher.finish();
|
|
B = Buffer.from(cipher.output.bytes(), "binary");
|
|
|
|
A = B.slice(0, 8);
|
|
R[idx] = B.slice(8, 16);
|
|
}
|
|
}
|
|
if (A.toString() !== A0.toString()) {
|
|
return Promise.reject(new Error("decryption failed"));
|
|
}
|
|
var pdata = Buffer.concat(R);
|
|
return Promise.resolve(pdata);
|
|
};
|
|
// ### WebCryptoAPI implementation
|
|
var webcrypto = function(key, cdata) {
|
|
try {
|
|
commonChecks(key, cdata);
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
var alg = {
|
|
name: "AES-KW"
|
|
};
|
|
var promise = helpers.subtleCrypto.importKey("raw", key, alg, true, ["unwrapKey"]);
|
|
promise = promise.then(function(key) {
|
|
return helpers.subtleCrypto.unwrapKey("raw", cdata, key, alg, {name: "HMAC", hash: "SHA-256"}, true, ["sign"]);
|
|
});
|
|
promise = promise.then(function(result) {
|
|
// unwrapped CryptoKey -- extract raw
|
|
return helpers.subtleCrypto.exportKey("raw", result);
|
|
});
|
|
promise = promise.then(function(result) {
|
|
result = Buffer.from(result);
|
|
return result;
|
|
});
|
|
return promise;
|
|
};
|
|
var node = function(key, cdata) {
|
|
try {
|
|
commonChecks(key, cdata);
|
|
} catch (err) {
|
|
return Promise.reject(err);
|
|
}
|
|
|
|
// prepare inputs
|
|
var R = split(cdata, 8),
|
|
iv = Buffer.alloc(16),
|
|
A,
|
|
B,
|
|
count;
|
|
A = R[0];
|
|
R = R.slice(1);
|
|
for (var jdx = 5; 0 <= jdx; --jdx) {
|
|
for (var idx = R.length - 1; 0 <= idx; --idx) {
|
|
count = (R.length * jdx) + idx + 1;
|
|
B = xor(A,
|
|
longToBigEndian(count));
|
|
B = Buffer.concat([B, R[idx], iv]);
|
|
var cipher = helpers.nodeCrypto.createDecipheriv("AES" + size, key, iv);
|
|
B = cipher.update(B);
|
|
|
|
A = B.slice(0, 8);
|
|
R[idx] = B.slice(8, 16);
|
|
}
|
|
}
|
|
if (A.toString() !== A0.toString()) {
|
|
return Promise.reject(new Error("decryption failed"));
|
|
}
|
|
var pdata = Buffer.concat(R);
|
|
return Promise.resolve(pdata);
|
|
};
|
|
|
|
return helpers.setupFallback(node, webcrypto, fallback);
|
|
}
|
|
|
|
// ### Public API
|
|
// * [name].encrypt
|
|
// * [name].decrypt
|
|
var aesKw = {};
|
|
[
|
|
"A128KW",
|
|
"A192KW",
|
|
"A256KW"
|
|
].forEach(function(alg) {
|
|
var size = parseInt(/A(\d+)KW/g.exec(alg)[1]);
|
|
aesKw[alg] = {
|
|
encrypt: kwEncryptFN(size),
|
|
decrypt: kwDecryptFN(size)
|
|
};
|
|
});
|
|
|
|
module.exports = aesKw;
|