Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add encryption #141

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ node_js:
- "11.15"
- "12.12"
- "13.6"
- "14.6"
sudo: false
dist: trusty
cache:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ change signature parameters like the algorithm of the signature.

A string which will be used as single key if `keys` is not provided.

##### encryptionKeys

A list of keys used to derive the decryption key for the cookie. The encryption will use a passphrase derived from the first key and a random initialisation vector.

##### Cookie Options

Other options are passed to `cookies.get()` and `cookies.set()` allowing you
Expand Down
54 changes: 53 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var Buffer = require('safe-buffer').Buffer
var debug = require('debug')('cookie-session')
var Cookies = require('cookies')
var onHeaders = require('on-headers')
var crypto = require('crypto')

/**
* Module exports.
Expand All @@ -34,6 +35,7 @@ module.exports = cookieSession
* @param {boolean} [options.overwrite=true]
* @param {string} [options.secret]
* @param {boolean} [options.signed=true]
* @param {array} [options.encryptionKeys] The keys used for the crypto
* @return {function} middleware
* @public
*/
Expand Down Expand Up @@ -125,7 +127,11 @@ function cookieSession (options) {
} else if ((!sess.isNew || sess.isPopulated) && sess.isChanged) {
// save populated or non-new changed session
debug('save %s', name)
cookies.set(name, Session.serialize(sess), req.sessionOptions)
var serializedSession = Session.serialize(sess)
if (opts.encryptionKeys) {
serializedSession = encryptString(serializedSession, opts.encryptionKeys[0])
}
cookies.set(name, serializedSession, req.sessionOptions)
}
} catch (e) {
debug('error saving session %s', e.message)
Expand Down Expand Up @@ -278,6 +284,10 @@ function tryGetSession (cookies, name, opts) {
return undefined
}

if (opts.encryptionKeys) {
str = decryptString(str, opts.encryptionKeys)
}

debug('parse %s', str)

try {
Expand All @@ -286,3 +296,45 @@ function tryGetSession (cookies, name, opts) {
return undefined
}
}

var inputEncoding = 'base64'
var outputEncoding = 'base64'

function encryptString (cleartext, keyphrase) {
var key = crypto
.createHash('sha256')
.update(keyphrase)
.digest()

var iv = crypto.randomBytes(16)

var cipher = crypto.createCipheriv('aes256', key, iv)
var cipherText = cipher.update(cleartext, inputEncoding, outputEncoding)
cipherText += cipher.final(outputEncoding)
return Buffer.from(iv, 'binary').toString(outputEncoding) + '@' + cipherText.toString()
}

function decryptString (cipherText, keyphrases) {
var encodedVi = cipherText.split('@')[0]
var encryptedText = cipherText.split('@')[1]
var iv = Buffer.from(encodedVi, outputEncoding)
var lastError = null
for (var i = 0; i < keyphrases.length; i++) {
var keyphrase = keyphrases[i]
try {
var key = crypto
.createHash('sha256')
.update(keyphrase)
.digest()

var decipher = crypto.createDecipheriv('aes256', key, iv)
var clearText = decipher.update(encryptedText, outputEncoding, inputEncoding)
clearText += decipher.final(inputEncoding)
return clearText.toString()
} catch (e) {
lastError = e
// ignore and just use the next key
}
}
throw new Error('Could not decrypt the cookie with any key. Caused by \n' + lastError.stack)
}
98 changes: 96 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

process.env.NODE_ENV = 'test'

var assert = require('assert')
var connect = require('connect')
var request = require('supertest')
var session = require('..')
var Buffer = require('safe-buffer').Buffer

describe('Cookie Session', function () {
describe('"httpOnly" option', function () {
Expand Down Expand Up @@ -195,6 +195,93 @@ describe('Cookie Session', function () {
})
})

describe('when the session is encrypted', function () {
it('should still be able to decrypt the cookie', function (done) {
var app = App({ encryptionKeys: ['anyString'] })
const mySession = {
someKey: 'someValue'
}

app.use(function (req, res, next) {
if (req.method === 'POST') {
req.session = mySession
res.statusCode = 200
res.end()
} else {
res.end(JSON.stringify(req.session))
}
})

request(app)
.post('/')
.expect(shouldNotHaveCookieWithValue('session', Buffer.from(JSON.stringify(mySession)).toString('base64')))
.expect(200, function (err, res) {
if (err) return done(err)
request(app)
.get('/')
.set('Cookie', cookieHeader(cookies(res)))
.expect(JSON.stringify(mySession), done)
})
})

it('should be able to use a rotated key to decrypt the cookie', function (done) {
var app = App({ encryptionKeys: ['anyString'] })
var newApp = App({ encryptionKeys: ['newPrimaryKey', 'anyString'] })
const mySession = {
someKey: 'someValue'
}

app.use(function (req, res, next) {
req.session = mySession
res.statusCode = 200
res.end()
})

newApp.use(function (req, res, next) {
res.end(JSON.stringify(req.session))
})

request(app)
.post('/')
.expect(shouldHaveCookie('session'))
.expect(200, function (err, res) {
if (err) return done(err)
request(newApp)
.get('/')
.set('Cookie', cookieHeader(cookies(res)))
.expect(JSON.stringify(mySession), done)
})
})
it('should not be able to to decrypt the cookie without a propper key', function (done) {
var app = App({ encryptionKeys: ['anyString'] })
var newApp = App({ encryptionKeys: ['newPrimaryKeyWithoutOldKey'] })
const mySession = {
someKey: 'someValue'
}

app.use(function (req, res, next) {
req.session = mySession
res.statusCode = 200
res.end()
})

newApp.use(function (req, res, next) {
res.end(JSON.stringify(req.session))
})

request(app)
.post('/')
.expect(shouldHaveCookie('session'))
.expect(200, function (err, res) {
if (err) return done(err)
request(newApp)
.get('/')
.set('Cookie', cookieHeader(cookies(res)))
.expect(500, done)
})
})
})

describe('when the session is invalid', function () {
it('should create new session', function (done) {
var app = App({ name: 'my.session', signed: false })
Expand Down Expand Up @@ -267,7 +354,7 @@ describe('Cookie Session', function () {

request(app)
.get('/')
.expect(shouldHaveCookie('session'))
.expect(shouldHaveCookieWithValue('session', Buffer.from(JSON.stringify({ message: 'hello' })).toString('base64')))
.expect(200, function (err, res) {
if (err) return done(err)
cookie = cookieHeader(cookies(res))
Expand Down Expand Up @@ -557,6 +644,13 @@ function shouldHaveCookieWithValue (name, value) {
}
}

function shouldNotHaveCookieWithValue (name, value) {
return function (res) {
assert.ok((name in cookies(res)), 'should have cookie "' + name + '"')
assert.notStrictEqual(cookies(res)[name].value, value)
}
}

function shouldNotSetCookies () {
return function (res) {
assert.strictEqual(res.headers['set-cookie'], undefined, 'should not set cookies')
Expand Down