connect-redis-crypto
Version:
Redis session store for Connect w/ enrypted session support
327 lines (270 loc) • 7.12 kB
JavaScript
/*!
* Connect - Redis
* Copyright(c) 2012 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var debug = require('debug')('connect:redis');
var crypto = require('crypto');
var debug = require('debug')('connect:redis');
var redis = require('redis');
var default_port = 6379;
var default_host = '127.0.0.1';
var noop = function(){};
/**
* One day in seconds.
*/
var oneDay = 86400;
/**
* Return the `RedisStore` extending `express`'s session Store.
*
* @param {object} express session
* @return {Function}
* @api public
*/
module.exports = function (session) {
/**
* Express's session Store.
*/
var Store = session.Store;
crypto.DEFAULT_ENCODING = 'hex';
/**
* Initialize RedisStore with the given `options`.
*
* @param {Object} options
* @api public
*/
function RedisStore (options) {
var self = this;
options = options || {};
Store.call(this, options);
this.prefix = options.prefix == null
? 'sess:'
: options.prefix;
/* istanbul ignore next */
if (options.url) {
console.error('Warning: "url" param is deprecated and will be removed in a later release: use redis-url module instead');
var url = require('url').parse(options.url);
if (url.protocol === 'redis:') {
if (url.auth) {
var userparts = url.auth.split(':');
options.user = userparts[0];
if (userparts.length === 2) {
options.pass = userparts[1];
}
}
options.host = url.hostname;
options.port = url.port;
if (url.pathname) {
options.db = url.pathname.replace('/', '', 1);
}
}
}
// convert to redis connect params
if (options.client) {
this.client = options.client;
}
else if (options.socket) {
this.client = redis.createClient(options.socket, options);
}
else if (options.port || options.host) {
this.client = redis.createClient(
options.port || default_port,
options.host || default_host,
options
);
}
else {
this.client = redis.createClient(options);
}
if (options.pass) {
this.client.auth(options.pass, function (err) {
if (err) {
throw err;
}
});
}
this.ttl = options.ttl;
this.disableTTL = options.disableTTL;
this.secret = options.secret || false;
this.algorithm = options.algorithm || false;
if (options.unref) this.client.unref();
if ('db' in options) {
if (typeof options.db !== 'number') {
console.error('Warning: connect-redis expects a number for the "db" option');
}
self.client.select(options.db);
self.client.on('connect', function () {
self.client.send_anyways = true;
self.client.select(options.db);
self.client.send_anyways = false;
});
}
self.client.on('error', function () {
self.emit('disconnect');
});
self.client.on('connect', function () {
self.emit('connect');
});
}
/**
* Wrapper to create cipher text, digest & encoded payload
*
* @param {String} payload
* @api private
*/
function encryptData(plaintext){
var pt = encrypt(this.secret, plaintext, this.algo)
, hmac = digest(this.secret, pt)
return {
ct: pt,
mac: hmac
};
}
/**
* Wrapper to extract digest, verify digest & decrypt cipher text
*
* @param {String} payload
* @api private
*/
function decryptData(ciphertext){
ciphertext = JSON.parse(ciphertext)
var hmac = digest(this.secret, ciphertext.ct);
if (hmac != ciphertext.mac) {
throw 'Encrypted session was tampered with!';
}
return decrypt(this.secret, ciphertext.ct, this.algo);
}
/**
* Generates HMAC as digest of cipher text
*
* @param {String} key
* @param {String} obj
* @param {String} algo
* @api private
*/
function digest(key, obj) {
var hmac = crypto.createHmac('sha1', key);
hmac.setEncoding('hex');
hmac.write(obj);
hmac.end();
return hmac.read();
}
/**
* Creates cipher text from plain text
*
* @param {String} key
* @param {String} pt
* @param {String} algo
* @api private
*/
function encrypt(key, pt, algo) {
algo = algo || 'aes-256-ctr';
pt = (Buffer.isBuffer(pt)) ? pt : new Buffer(pt);
var cipher = crypto.createCipher(algo, key)
, ct = [];
ct.push(cipher.update(pt));
ct.push(cipher.final('hex'));
return ct.join('');
}
/**
* Creates plain text from cipher text
*
* @param {String} key
* @param {String} pt
* @param {String} algo
* @api private
*/
function decrypt(key, ct, algo) {
algo = algo || 'aes-256-ctr';
var cipher = crypto.createDecipher(algo, key)
, pt = [];
pt.push(cipher.update(ct, 'hex', 'utf8'));
pt.push(cipher.final('utf8'));
return pt.join('');
}
/**
* Inherit from `Store`.
*/
RedisStore.prototype.__proto__ = Store.prototype;
/**
* Attempt to fetch session by the given `sid`.
*
* @param {String} sid
* @param {Function} fn
* @api public
*/
RedisStore.prototype.get = function (sid, fn) {
var store = this;
var psid = store.prefix + sid;
if (!fn) fn = noop;
debug('GET "%s"', sid);
secret = this.secret || false;
store.client.get(psid, function (er, data) {
if (!data) return fn();
var result;
data = (secret) ? decryptData.call(this, data) : data.toString();
debug('GOT %s', data);
try {
result = JSON.parse(data);
}
catch (er) {
return fn(er);
}
return fn(null, result);
});
};
/**
* Commit the given `sess` object associated with the given `sid`.
*
* @param {String} sid
* @param {Session} sess
* @param {Function} fn
* @api public
*/
RedisStore.prototype.set = function (sid, sess, fn) {
var store = this;
var psid = store.prefix + sid;
if (!fn) fn = noop;
try {
jsess = JSON.stringify(
(this.secret)
? encryptData.call(this, JSON.stringify(sess), this.secret, this.algorithm)
: sess);
}
catch (er) {
return fn(er);
}
if (store.disableTTL) {
debug('SET "%s" %s', sid, jsess);
store.client.set(psid, jsess, function (er) {
debug('SET complete');
fn.apply(null, arguments);
});
return;
}
var maxAge = sess.cookie.maxAge;
var ttl = store.ttl || (typeof maxAge === 'number'
? maxAge / 1000 | 0
: oneDay);
debug('SETEX "%s" ttl:%s %s', sid, ttl, jsess);
store.client.setex(psid, ttl, jsess, function (er) {
debug('SETEX complete');
fn.apply(this, arguments);
});
};
/**
* Destroy the session associated with the given `sid`.
*
* @param {String} sid
* @api public
*/
RedisStore.prototype.destroy = function (sid, fn) {
sid = this.prefix + sid;
debug('DEL "%s"', sid);
this.client.del(sid, fn);
};
return RedisStore;
};