signatureAlgorithm & encryptionAlgorithm
This commit is contained in:
Родитель
18333c724e
Коммит
f7922f9df0
|
@ -11,6 +11,23 @@ const util = require("util");
|
|||
const COOKIE_NAME_SEP = '=';
|
||||
const ACTIVE_DURATION = 1000 * 60 * 5;
|
||||
|
||||
const KDF_ENC = 'cookiesession-encryption';
|
||||
const KDF_MAC = 'cookiesession-signature';
|
||||
|
||||
const ENCRYPTION_ALGORITHMS = [
|
||||
'aes128', // implicit CBC mode
|
||||
'aes192',
|
||||
'aes256'
|
||||
];
|
||||
const DEFAULT_ENCRYPTION_ALGO = 'aes256';
|
||||
|
||||
const SIGNATURE_ALGORITHMS = [
|
||||
'sha256', 'sha256-drop128',
|
||||
'sha384', 'sha384-drop192',
|
||||
'sha512', 'sha512-drop256'
|
||||
];
|
||||
const DEFAULT_SIGNATURE_ALGO = 'sha256';
|
||||
|
||||
function isObject(val) {
|
||||
return Object.prototype.toString.call(val) === '[object Object]';
|
||||
}
|
||||
|
@ -43,6 +60,14 @@ function base64urldecode(arg) {
|
|||
return new Buffer(s, 'base64'); // Standard base64 decoder
|
||||
}
|
||||
|
||||
function forceBuffer(binaryOrBuffer) {
|
||||
if (Buffer.isBuffer(binaryOrBuffer)) {
|
||||
return binaryOrBuffer;
|
||||
} else {
|
||||
return new Buffer(binaryOrBuffer, 'binary');
|
||||
}
|
||||
}
|
||||
|
||||
function deriveKey(master, type) {
|
||||
// eventually we want to use HKDF. For now we'll do something simpler.
|
||||
var hmac = crypto.createHmac('sha256', master);
|
||||
|
@ -50,6 +75,25 @@ function deriveKey(master, type) {
|
|||
return hmac.digest('binary');
|
||||
}
|
||||
|
||||
function setupKeys(opts) {
|
||||
// derive two keys, one for signing one for encrypting, from the secret.
|
||||
if (!opts.encryptionKey) {
|
||||
opts.encryptionKey = deriveKey(opts.secret, KDF_ENC);
|
||||
}
|
||||
|
||||
if (!opts.signatureKey) {
|
||||
opts.signatureKey = deriveKey(opts.secret, KDF_MAC);
|
||||
}
|
||||
|
||||
if (!opts.signatureAlgorithm) {
|
||||
opts.signatureAlgorithm = DEFAULT_SIGNATURE_ALGO;
|
||||
}
|
||||
|
||||
if (!opts.encryptionAlgorithm) {
|
||||
opts.encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGO;
|
||||
}
|
||||
}
|
||||
|
||||
function constantTimeEquals(a, b) {
|
||||
// Ideally this would be a native function, so it's less sensitive to how the
|
||||
// JS engine might optimize.
|
||||
|
@ -63,6 +107,59 @@ function constantTimeEquals(a, b) {
|
|||
return ret === 0;
|
||||
}
|
||||
|
||||
// it's good cryptographic pracitice to not leave buffers with sensitive
|
||||
// contents hanging around.
|
||||
function zeroBuffer(buf) {
|
||||
for (var i = 0; i < buf.length; i++) {
|
||||
buf[i] = 0;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
function hmacInit(algo, key) {
|
||||
var match = algo.match(/^([^-]+)(?:-drop(\d+))?$/);
|
||||
var baseAlg = match[1];
|
||||
var drop = match[2] ? parseInt(match[2], 10) : 0;
|
||||
|
||||
var hmacAlg = crypto.createHmac(baseAlg, key);
|
||||
var origDigest = hmacAlg.digest;
|
||||
|
||||
if (drop === 0) {
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when dropping
|
||||
// 0.8 support.
|
||||
hmacAlg.digest = function() {
|
||||
return forceBuffer(origDigest.call(this));
|
||||
};
|
||||
} else {
|
||||
var N = drop / 8; // bits to bytes
|
||||
hmacAlg.digest = function dropN() {
|
||||
var result = forceBuffer(origDigest.call(this));
|
||||
// Throw away the second half of the 512-bit result, leaving the first
|
||||
// 256-bits.
|
||||
var truncated = new Buffer(N);
|
||||
result.copy(truncated, 0, 0, N);
|
||||
zeroBuffer(result);
|
||||
return truncated;
|
||||
};
|
||||
}
|
||||
|
||||
return hmacAlg;
|
||||
}
|
||||
|
||||
function computeHmac(opts, iv, ciphertext, duration, createdAt) {
|
||||
var hmacAlg = hmacInit(opts.signatureAlgorithm, opts.signatureKey);
|
||||
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
|
||||
return hmacAlg.digest();
|
||||
}
|
||||
|
||||
function encode(opts, content, duration, createdAt){
|
||||
// format will be:
|
||||
// iv.ciphertext.createdAt.duration.hmac
|
||||
|
@ -73,13 +170,7 @@ function encode(opts, content, duration, createdAt){
|
|||
throw new Error('cookieName cannot include "="');
|
||||
}
|
||||
|
||||
if (!opts.encryptionKey) {
|
||||
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
}
|
||||
|
||||
if (!opts.signatureKey) {
|
||||
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
}
|
||||
setupKeys(opts);
|
||||
|
||||
duration = duration || 24*60*60*1000;
|
||||
createdAt = createdAt || new Date().getTime();
|
||||
|
@ -88,39 +179,44 @@ function encode(opts, content, duration, createdAt){
|
|||
var iv = crypto.randomBytes(16);
|
||||
|
||||
// encrypt with encryption key
|
||||
var plaintext = opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content);
|
||||
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
|
||||
var ciphertext = cipher.update(plaintext, 'utf8', 'binary');
|
||||
ciphertext += cipher.final('binary');
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
ciphertext = new Buffer(ciphertext, 'binary');
|
||||
var plaintext = new Buffer(
|
||||
opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content),
|
||||
'utf8'
|
||||
);
|
||||
var cipher = crypto.createCipheriv(
|
||||
opts.encryptionAlgorithm,
|
||||
opts.encryptionKey,
|
||||
iv
|
||||
);
|
||||
var ciphertextStart = cipher.update(plaintext);
|
||||
zeroBuffer(plaintext);
|
||||
var ciphertextEnd = cipher.final();
|
||||
var ciphertext = Buffer.concat([ciphertextStart, ciphertextEnd]);
|
||||
zeroBuffer(ciphertextStart);
|
||||
zeroBuffer(ciphertextEnd);
|
||||
|
||||
// hmac it
|
||||
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
var hmac = computeHmac(opts, iv, ciphertext, duration, createdAt);
|
||||
|
||||
var hmac = hmacAlg.digest();
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
hmac = new Buffer(hmac, 'binary');
|
||||
|
||||
return [
|
||||
var result = [
|
||||
base64urlencode(iv),
|
||||
base64urlencode(ciphertext),
|
||||
createdAt,
|
||||
duration,
|
||||
base64urlencode(hmac)
|
||||
].join('.');
|
||||
|
||||
zeroBuffer(iv);
|
||||
zeroBuffer(ciphertext);
|
||||
zeroBuffer(hmac);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function decode(opts, content) {
|
||||
if (!opts.cookieName) {
|
||||
throw new Error("cookieName option required");
|
||||
}
|
||||
|
||||
// stop at any time if there's an issue
|
||||
var components = content.split(".");
|
||||
|
@ -128,17 +224,7 @@ function decode(opts, content) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!opts.cookieName) {
|
||||
throw new Error("cookieName option required");
|
||||
}
|
||||
|
||||
if (!opts.encryptionKey) {
|
||||
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
}
|
||||
|
||||
if (!opts.signatureKey) {
|
||||
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
}
|
||||
setupKeys(opts);
|
||||
|
||||
var iv = base64urldecode(components[0]);
|
||||
var ciphertext = base64urldecode(components[1]);
|
||||
|
@ -146,51 +232,58 @@ function decode(opts, content) {
|
|||
var duration = parseInt(components[3], 10);
|
||||
var hmac = base64urldecode(components[4]);
|
||||
|
||||
function cleanup() {
|
||||
zeroBuffer(iv);
|
||||
zeroBuffer(ciphertext);
|
||||
zeroBuffer(hmac);
|
||||
if (expectedHmac) { // declared below
|
||||
zeroBuffer(expectedHmac);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure IV is right length
|
||||
if (iv.length !== 16) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// check hmac
|
||||
var hmacAlg = crypto.createHmac('sha256', opts.signatureKey);
|
||||
hmacAlg.update(iv);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(ciphertext);
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(createdAt.toString());
|
||||
hmacAlg.update(".");
|
||||
hmacAlg.update(duration.toString());
|
||||
|
||||
var expectedHmac = hmacAlg.digest();
|
||||
// Before 0.10, crypto returns binary-encoded strings. Remove when
|
||||
// dropping 0.8 support.
|
||||
expectedHmac = new Buffer(expectedHmac, 'binary');
|
||||
var expectedHmac = computeHmac(opts, iv, ciphertext, duration, createdAt);
|
||||
|
||||
if (!constantTimeEquals(hmac, expectedHmac)) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// decrypt
|
||||
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
|
||||
var cipher = crypto.createDecipheriv(
|
||||
opts.encryptionAlgorithm,
|
||||
opts.encryptionKey,
|
||||
iv
|
||||
);
|
||||
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');
|
||||
plaintext += cipher.final('utf8');
|
||||
|
||||
var cookieName = plaintext.substring(0, plaintext.indexOf(COOKIE_NAME_SEP));
|
||||
if (cookieName !== opts.cookieName) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
var result;
|
||||
try {
|
||||
return {
|
||||
result = {
|
||||
content: JSON.parse(
|
||||
plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)
|
||||
),
|
||||
createdAt: createdAt,
|
||||
duration: duration
|
||||
};
|
||||
} catch (x) {
|
||||
return;
|
||||
} catch (ignored) {
|
||||
}
|
||||
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -245,7 +338,7 @@ Session.prototype = {
|
|||
} else {
|
||||
var time = this.createdAt || new Date().getTime();
|
||||
// the cookie should expire when it becomes invalid
|
||||
// we add an extra second because the conversion to a date
|
||||
// we add an extra second because the conversion to a date
|
||||
// truncates the milliseconds
|
||||
this.expires = new Date(time + this.duration + 1000);
|
||||
}
|
||||
|
@ -387,8 +480,9 @@ function clientSessionFactory(opts) {
|
|||
throw new Error("no options provided, some are required");
|
||||
}
|
||||
|
||||
if (!opts.secret) {
|
||||
throw new Error("cannot set up sessions without a secret");
|
||||
if (!(opts.secret || (opts.encryptionKey && opts.signatureKey))) {
|
||||
throw new Error("cannot set up sessions without a secret "+
|
||||
"or encryptionKey/signatureKey pair");
|
||||
}
|
||||
|
||||
// defaults
|
||||
|
@ -397,6 +491,22 @@ function clientSessionFactory(opts) {
|
|||
opts.activeDuration = 'activeDuration' in opts ?
|
||||
opts.activeDuration : ACTIVE_DURATION;
|
||||
|
||||
var encAlg = opts.encryptionAlgorithm || DEFAULT_ENCRYPTION_ALGO;
|
||||
encAlg = encAlg.toLowerCase();
|
||||
if (ENCRYPTION_ALGORITHMS.indexOf(encAlg) === -1) {
|
||||
throw new Error('invalid encryptionAlgorithm, supported are: '+
|
||||
ENCRYPTION_ALGORITHMS.join(', '));
|
||||
}
|
||||
opts.encryptionAlgorithm = encAlg;
|
||||
|
||||
var sigAlg = opts.signatureAlgorithm || DEFAULT_SIGNATURE_ALGO;
|
||||
sigAlg = sigAlg.toLowerCase();
|
||||
if (SIGNATURE_ALGORITHMS.indexOf(sigAlg) === -1) {
|
||||
throw new Error('invalid signatureAlgorithm, supported are: '+
|
||||
SIGNATURE_ALGORITHMS.join(', '));
|
||||
}
|
||||
opts.signatureAlgorithm = sigAlg;
|
||||
|
||||
// set up cookie defaults
|
||||
opts.cookie = opts.cookie || {};
|
||||
if (typeof opts.cookie.httpOnly === 'undefined') {
|
||||
|
@ -411,9 +521,7 @@ function clientSessionFactory(opts) {
|
|||
opts.cookie.secure = true;
|
||||
*/
|
||||
|
||||
// derive two keys, one for signing one for encrypting, from the secret.
|
||||
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
|
||||
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
|
||||
setupKeys(opts);
|
||||
|
||||
const propertyName = opts.requestKey || opts.cookieName;
|
||||
|
||||
|
@ -447,7 +555,7 @@ function clientSessionFactory(opts) {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
var writeHead = res.writeHead;
|
||||
res.writeHead = function () {
|
||||
rawSession.updateCookie();
|
||||
|
@ -465,5 +573,6 @@ module.exports = clientSessionFactory;
|
|||
|
||||
module.exports.util = {
|
||||
encode: encode,
|
||||
decode: decode
|
||||
decode: decode,
|
||||
computeHmac: computeHmac
|
||||
};
|
||||
|
|
|
@ -826,7 +826,6 @@ suite.addBatch({
|
|||
var browser = tobi.createBrowser(app);
|
||||
browser.get("/foo", function(res, $) {});
|
||||
},
|
||||
|
||||
"encode " : function(err, req){
|
||||
var result = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});
|
||||
var result_arr = result.split(".");
|
||||
|
@ -1187,4 +1186,58 @@ suite.addBatch({
|
|||
}
|
||||
});
|
||||
|
||||
var HMAC_EXPECT = {
|
||||
// aligned so you can see the dropN effect:
|
||||
'sha256':
|
||||
'ib46vUCBTNOcEl9P6dRwlxZNGNQMHDMulJEc+sAcET8=',
|
||||
'sha256-drop128':
|
||||
'ib46vUCBTNOcEl9P6dRwlw==',
|
||||
'sha384':
|
||||
'JRnBFUT/W+/EjpBdWmQ/hctq1g1/IUaD6Sqyi9qGH4R2a8uv+86vZXvY72fYNTYw',
|
||||
'sha384-drop192':
|
||||
'JRnBFUT/W+/EjpBdWmQ/hctq1g1/IUaD',
|
||||
'sha512':
|
||||
'l4D3LI09OMccrCXQcXl/biDZM1t1yDHEqZbz5DXb1IxUnX956imItOBuJu/bP0Zr'+
|
||||
'wzWl2vJxNvOBWpfdA9xl4Q==',
|
||||
'sha512-drop256':
|
||||
'l4D3LI09OMccrCXQcXl/biDZM1t1yDHEqZbz5DXb1Iw='
|
||||
};
|
||||
|
||||
function testHmac(algo) {
|
||||
var block = {};
|
||||
block.topic = function() {
|
||||
var sixteen = new Buffer('0123456789abcdef','binary');
|
||||
|
||||
var opts = {
|
||||
signatureAlgorithm: algo,
|
||||
signatureKey: sixteen
|
||||
};
|
||||
var iv = sixteen;
|
||||
var ciphertext = new Buffer('0123456789abcdef0123','binary');
|
||||
var duration = 876543210;
|
||||
var createdAt = 1234567890;
|
||||
|
||||
return cookieSessions.util.computeHmac(
|
||||
opts, iv, ciphertext, duration, createdAt
|
||||
).toString('base64');
|
||||
};
|
||||
|
||||
block['equals test vector'] = function(val) {
|
||||
assert.equal(val, HMAC_EXPECT[algo]);
|
||||
};
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
suite.addBatch({
|
||||
"computeHmac": {
|
||||
"sha256": testHmac('sha256'),
|
||||
"sha256-drop128": testHmac('sha256-drop128'),
|
||||
"sha384": testHmac('sha384'),
|
||||
"sha384-drop192": testHmac('sha384-drop192'),
|
||||
"sha512": testHmac('sha512'),
|
||||
"sha512-drop256": testHmac('sha512-drop256'),
|
||||
}
|
||||
});
|
||||
|
||||
suite.export(module);
|
||||
|
|
Загрузка…
Ссылка в новой задаче