Added public encode and decode methods

This commit is contained in:
Trygve Lie 2013-03-19 18:10:31 +01:00
Родитель e58f347bb1
Коммит eca903cebb
2 изменённых файлов: 164 добавлений и 78 удалений

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

@ -27,6 +27,108 @@ function base64urldecode(arg) {
return new Buffer(s, 'base64'); // Standard base64 decoder
}
function deriveKey(master, type) {
// eventually we want to use HKDF. For now we'll do something simpler.
var hmac = crypto.createHmac('sha256', master);
hmac.update(type);
return hmac.digest('binary');
}
function encode(opts, content, duration, createdAt){
// format will be:
// iv.ciphertext.createdAt.duration.hmac
if (!opts.encryptionKey) {
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
}
if (!opts.signatureKey) {
opts['signatureKey'] = deriveKey(opts.secret, 'cookiesession-signature');
}
duration = duration || 24*60*60*1000;
createdAt = createdAt || new Date().getTime();
// generate iv
var iv = crypto.randomBytes(16);
// encrypt with encryption key
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
var ciphertext = cipher.update(JSON.stringify(content), 'utf8', 'binary');
ciphertext += cipher.final('binary');
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());
var hmac = hmacAlg.digest();
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + createdAt + "." + duration + "." + base64urlencode(hmac);
}
function decode(opts, content) {
// stop at any time if there's an issue
var components = content.split(".");
if (components.length != 5)
return;
if (!opts.encryptionKey) {
opts['encryptionKey'] = deriveKey(opts.secret, 'cookiesession-encryption');
}
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]);
var duration = parseInt(components[3]);
var hmac = base64urldecode(components[4]);
// 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());
var expected_hmac = hmacAlg.digest();
if (hmac.toString('utf8') != expected_hmac.toString('utf8'))
return;
// decrypt
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
var plaintext = cipher.update(ciphertext, 'utf8');
plaintext += cipher.final('utf8');
try {
return {
content: JSON.parse(plaintext),
createdAt: createdAt,
duration: duration
}
} catch (x) {
return;
}
}
/*
* Session object
*
@ -88,86 +190,21 @@ Session.prototype = {
// take the content and do the encrypt-and-sign
// boxing builds in the concept of createdAt
box: function() {
// format will be:
// iv.ciphertext.createdAt.duration.hmac
// generate iv
var iv = crypto.randomBytes(16);
// encrypt with encryption key
var cipher = crypto.createCipheriv('aes256', this.opts.encryptionKey, iv);
var ciphertext = cipher.update(JSON.stringify(this.content), 'utf8', 'binary');
ciphertext += cipher.final('binary');
ciphertext = new Buffer(ciphertext, 'binary');
// hmac it
var hmacAlg = crypto.createHmac('sha256', this.opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(this.createdAt.toString());
hmacAlg.update(".");
hmacAlg.update(this.duration.toString());
var hmac = hmacAlg.digest();
return base64urlencode(iv) + "." + base64urlencode(ciphertext) + "." + this.createdAt + "." + this.duration + "." + base64urlencode(hmac);
return encode(this.opts, this.content, this.duration, this.createdAt);
},
unbox: function(content) {
this.clearContent();
// stop at any time if there's an issue
var components = content.split(".");
if (components.length != 5)
return;
var iv = base64urldecode(components[0]);
var ciphertext = base64urldecode(components[1]);
var createdAt = parseInt(components[2]);
var duration = parseInt(components[3]);
var hmac = base64urldecode(components[4]);
// make sure IV is right length
if (iv.length != 16)
return;
// check hmac
var hmacAlg = crypto.createHmac('sha256', this.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();
if (hmac.toString('utf8') != expected_hmac.toString('utf8'))
return;
// decrypt
var cipher = crypto.createDecipheriv('aes256', this.opts.encryptionKey, iv);
var plaintext = cipher.update(ciphertext, 'utf8');
plaintext += cipher.final('utf8');
var new_content;
try {
new_content = JSON.parse(plaintext);
} catch (x) {
return;
}
var unboxed = decode(this.opts, content);
var self = this;
Object.keys(new_content).forEach(function(k) {
self.content[k] = new_content[k];
Object.keys(unboxed.content).forEach(function(k) {
self.content[k] = unboxed.content[k];
});
// all is well, accept values
this.createdAt = createdAt;
this.duration = duration;
this.createdAt = unboxed.createdAt;
this.duration = unboxed.duration;
},
updateCookie: function() {
@ -259,12 +296,6 @@ Session.prototype = {
};
function deriveKey(master, type) {
// eventually we want to use HKDF. For now we'll do something simpler.
var hmac = crypto.createHmac('sha256', master);
hmac.update(type);
return hmac.digest('binary');
}
var cookieSession = function(opts) {
if (!opts)
@ -316,3 +347,11 @@ var cookieSession = function(opts) {
};
module.exports = cookieSession;
// Expose encode and decode method
module.exports.util = {
encode: encode,
decode: decode
};

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

@ -695,4 +695,51 @@ suite.addBatch({
});
suite.addBatch({
"public encode and decode util methods" : {
topic: function() {
var self = this;
var app = create_app();
app.get("/foo", function(req, res) {
self.callback(null, req);
res.send("hello");
});
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(".");
assert.equal(result_arr.length, 5);
},
"encode and decode - is object" : function(err, req){
var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});
var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
assert.isObject(decoded);
},
"encode and decode - has all values" : function(err, req){
var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar', bar:'foo'});
var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
assert.equal(decoded.content.foo, 'bar');
assert.equal(decoded.content.bar, 'foo');
assert.isNumber(decoded.duration);
assert.isNumber(decoded.createdAt);
},
"encode and decode - override duration and createdAt" : function(err, req){
var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar', bar:'foo'}, 5000, 1355408039221);
var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
assert.equal(decoded.duration, 5000);
assert.equal(decoded.createdAt, 1355408039221);
},
"encode and decode - default duration" : function(err, req){
var encoded = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});
var decoded = cookieSessions.util.decode({cookieName: 'session', secret: 'yo'}, encoded);
assert.equal(decoded.duration, 86400000);
}
}
});
suite.export(module);