signatureAlgorithm & encryptionAlgorithm

This commit is contained in:
Jeremy Stashewsky 2013-12-17 20:45:14 -08:00
Родитель 18333c724e
Коммит f7922f9df0
2 изменённых файлов: 227 добавлений и 65 удалений

Просмотреть файл

@ -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);