This commit is contained in:
Sean McArthur 2013-12-16 10:16:17 -08:00
Родитель cedbdc4edf
Коммит 03c4bbad7e
2 изменённых файлов: 184 добавлений и 123 удалений

28
.jshintrc Normal file
Просмотреть файл

@ -0,0 +1,28 @@
{
"bitwise": false,
"boss": true,
"browser": true,
"camelcase": true,
"curly": true,
"esnext": true,
"eqeqeq": true,
"eqnull": true,
"expr": true,
"forin": false,
"indent": 2,
"latedef": true,
"laxbreak": true,
"laxcomma": true,
"maxcomplexity": 10,
"maxlen": 80,
"maxerr": 100,
"node": true,
"noarg": true,
"passfail": false,
"shadow": true,
"strict": false,
"supernew": false,
"trailing": true,
"undef": true,
"unused": true
}

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

@ -24,12 +24,17 @@ function base64urldecode(arg) {
var s = arg;
s = s.replace(/-/g, '+'); // 62nd char of encoding
s = s.replace(/_/g, '/'); // 63rd char of encoding
switch (s.length % 4) // Pad with trailing '='s
{
case 0: break; // No pad chars in this case
case 2: s += "=="; break; // Two pad chars
case 3: s += "="; break; // One pad char
default: throw new Error("Illegal base64url string!");
switch (s.length % 4) { // Pad with trailing '='s
case 0:
break; // No pad chars in this case
case 2:
s += "==";
break; // Two pad chars
case 3:
s += "=";
break; // One pad char
default:
throw new Error("Illegal base64url string!");
}
return new Buffer(s, 'base64'); // Standard base64 decoder
}
@ -44,132 +49,144 @@ function deriveKey(master, type) {
function constantTimeEquals(a, b) {
// Ideally this would be a native function, so it's less sensitive to how the
// JS engine might optimize.
if (a.length != b.length)
if (a.length !== b.length) {
return false;
}
var ret = 0;
for (var i = 0; i < a.length; i++) {
ret |= a.readUInt8(i) ^ b.readUInt8(i);
}
return ret == 0;
return ret === 0;
}
function encode(opts, content, duration, createdAt){
// format will be:
// iv.ciphertext.createdAt.duration.hmac
// format will be:
// iv.ciphertext.createdAt.duration.hmac
if (!opts.cookieName) {
throw new Error('cookieName option required');
} else if (String(opts.cookieName).indexOf(COOKIE_NAME_SEP) != -1) {
throw new Error('cookieName cannot include "="');
}
if (!opts.cookieName) {
throw new Error('cookieName option required');
} else if (String(opts.cookieName).indexOf(COOKIE_NAME_SEP) !== -1) {
throw new Error('cookieName cannot include "="');
}
if (!opts.encryptionKey) {
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.encryptionKey) {
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.signatureKey) {
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
}
if (!opts.signatureKey) {
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
}
duration = duration || 24*60*60*1000;
createdAt = createdAt || new Date().getTime();
duration = duration || 24*60*60*1000;
createdAt = createdAt || new Date().getTime();
// generate iv
var iv = crypto.randomBytes(16);
// generate iv
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');
// 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');
// 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());
// 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 = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
hmac = new Buffer(hmac, 'binary');
var hmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
hmac = new Buffer(hmac, 'binary');
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + createdAt + "." + duration + "." + base64urlencode(hmac);
return [
base64urlencode(iv),
base64urlencode(ciphertext),
createdAt,
duration,
base64urlencode(hmac)
].join('.');
}
function decode(opts, content) {
// stop at any time if there's an issue
var components = content.split(".");
if (components.length != 5)
return;
// stop at any time if there's an issue
var components = content.split(".");
if (components.length !== 5) {
return;
}
if (!opts.cookieName) {
throw new Error("cookieName option required");
}
if (!opts.cookieName) {
throw new Error("cookieName option required");
}
if (!opts.encryptionKey) {
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.encryptionKey) {
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.signatureKey) {
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
}
if (!opts.signatureKey) {
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
}
var iv = base64urldecode(components[0]);
var ciphertext = base64urldecode(components[1]);
var createdAt = parseInt(components[2], 10);
var duration = parseInt(components[3], 10);
var hmac = base64urldecode(components[4]);
var iv = base64urldecode(components[0]);
var ciphertext = base64urldecode(components[1]);
var createdAt = parseInt(components[2], 10);
var duration = parseInt(components[3], 10);
var hmac = base64urldecode(components[4]);
// make sure IV is right length
if (iv.length != 16)
return;
// make sure IV is right length
if (iv.length !== 16) {
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());
// 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 expected_hmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
expected_hmac = new Buffer(expected_hmac, 'binary');
var expectedHmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
expectedHmac = new Buffer(expectedHmac, 'binary');
if (!constantTimeEquals(hmac, expected_hmac))
return;
if (!constantTimeEquals(hmac, expectedHmac)) {
return;
}
// decrypt
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');
plaintext += cipher.final('utf8');
// decrypt
var cipher = crypto.createDecipheriv('aes256', 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) {
return;
}
var cookieName = plaintext.substring(0, plaintext.indexOf(COOKIE_NAME_SEP));
if (cookieName !== opts.cookieName) {
return;
}
try {
return {
content: JSON.parse(plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)),
createdAt: createdAt,
duration: duration
};
} catch (x) {
return;
}
try {
return {
content: JSON.parse(
plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)
),
createdAt: createdAt,
duration: duration
};
} catch (x) {
return;
}
}
/*
@ -205,21 +222,27 @@ function Session(req, res, cookies, opts) {
}
// here, we check that the security bits are set correctly
var secure = (res.socket && res.socket.encrypted) || (req.connection && req.connection.proxySecure);
if (opts.cookie.secure && !secure)
throw new Error("you cannot have a secure cookie unless the socket is secure or you declare req.connection.proxySecure to be true.");
var secure = (res.socket && res.socket.encrypted) ||
(req.connection && req.connection.proxySecure);
if (opts.cookie.secure && !secure) {
throw new Error("you cannot have a secure cookie unless the socket is " +
" secure or you declare req.connection.proxySecure to be true.");
}
}
Session.prototype = {
updateDefaultExpires: function() {
if (this.opts.cookie.maxAge) return;
if (this.opts.cookie.maxAge) {
return;
}
if (this.opts.cookie.ephemeral) {
this.expires = null;
} 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 truncates the milliseconds
// we add an extra second because the conversion to a date
// truncates the milliseconds
this.expires = new Date(time + this.duration + 1000);
}
},
@ -228,8 +251,9 @@ Session.prototype = {
var self = this;
Object.keys(this._content).forEach(function(k) {
// exclude this key if it's meant to be preserved
if (keysToPreserve && (keysToPreserve.indexOf(k) > -1))
if (keysToPreserve && (keysToPreserve.indexOf(k) > -1)) {
return;
}
delete self._content[k];
});
@ -248,8 +272,9 @@ Session.prototype = {
if (ephemeral && this.opts.cookie.maxAge) {
throw new Error("you cannot have an ephemeral cookie with a maxAge.");
}
if (!this.loaded)
if (!this.loaded) {
this.loadFromCookie(true);
}
this.dirty = true;
this.duration = newDuration;
this.createdAt = new Date().getTime();
@ -267,7 +292,9 @@ Session.prototype = {
this.clearContent();
var unboxed = decode(this.opts, content);
if (!unboxed) return;
if (!unboxed) {
return;
}
var self = this;
@ -295,7 +322,7 @@ Session.prototype = {
}
},
loadFromCookie: function(force_reset) {
loadFromCookie: function(forceReset) {
var cookie = this.cookies.get(this.opts.cookieName);
if (cookie) {
this.unbox(cookie);
@ -303,16 +330,16 @@ Session.prototype = {
var expiresAt = this.createdAt + this.duration;
var now = Date.now();
// should we reset this session?
if (expiresAt < now)
if (expiresAt < now) {
this.reset();
// if expiration is soon, push back a few minutes to not interrupt user
else if (expiresAt - now < this.activeDuration) {
} else if (expiresAt - now < this.activeDuration) {
this.createdAt += this.activeDuration;
this.dirty = true;
this.updateDefaultExpires();
}
} else {
if (force_reset) {
if (forceReset) {
this.reset();
} else {
return false; // didn't actually load the cookie
@ -338,11 +365,11 @@ Object.defineProperty(Session.prototype, 'content', {
return this._content;
},
set: function setContent(value) {
Object.defineProperty(value, 'reset', {
Object.defineProperty(value, 'reset', {
enumerable: false,
value: this.reset.bind(this)
});
Object.defineProperty(value, 'setDuration', {
Object.defineProperty(value, 'setDuration', {
enumerable: false,
value: this.setDuration.bind(this)
});
@ -352,21 +379,25 @@ Object.defineProperty(Session.prototype, 'content', {
function clientSessionFactory(opts) {
if (!opts)
throw "no options provided, some are required"; // XXX rename opts?
if (!opts) {
throw new Error("no options provided, some are required");
}
if (!opts.secret)
throw "cannot set up sessions without a secret";
if (!opts.secret) {
throw new Error("cannot set up sessions without a secret");
}
// defaults
opts.cookieName = opts.cookieName || "session_state";
opts.duration = opts.duration || 24*60*60*1000;
opts.activeDuration = 'activeDuration' in opts ? opts.activeDuration : ACTIVE_DURATION;
opts.activeDuration = 'activeDuration' in opts ?
opts.activeDuration : ACTIVE_DURATION;
// set up cookie defaults
opts.cookie = opts.cookie || {};
if (typeof(opts.cookie.httpOnly) == 'undefined')
if (typeof opts.cookie.httpOnly === 'undefined') {
opts.cookie.httpOnly = true;
}
// let's not default to secure just yet,
// as this depends on the socket being secure,
@ -393,7 +424,9 @@ function clientSessionFactory(opts) {
rawSession = new Session(req, res, cookies, opts);
} catch (x) {
// this happens only if there's a big problem
process.nextTick(function() {next("client-sessions error: " + x.toString());});
process.nextTick(function() {
next("client-sessions error: " + x.toString());
});
return;
}
@ -415,11 +448,11 @@ function clientSessionFactory(opts) {
res.writeHead = function () {
rawSession.updateCookie();
return writeHead.apply(res, arguments);
}
};
next();
};
};
}
module.exports = clientSessionFactory;