Added public encode and decode methods
This commit is contained in:
Родитель
e58f347bb1
Коммит
eca903cebb
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче