Skip to content
This repository has been archived by the owner on Jul 5, 2020. It is now read-only.

Commit

Permalink
feat: add support for encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Apr 6, 2017
1 parent a07de48 commit aaf2e65
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 16 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -17,7 +17,8 @@
"license": "MIT",
"dependencies": {
"cookie": "^0.3.1",
"cookie-signature": "^1.0.6"
"cookie-signature": "^1.0.6",
"simple-encryptor": "^1.1.0"
},
"devDependencies": {
"coveralls": "^2.13.0",
Expand Down
154 changes: 139 additions & 15 deletions src/Cookie/index.js
Expand Up @@ -11,6 +11,32 @@

const parser = require('cookie')
const signature = require('cookie-signature')
const simpleEncryptor = require('simple-encryptor')

const encrypters = {}
/**
* Returns an encrypter instance to be used for
* encrypting the cookie. Since creating a new
* instance each time is expensive, we cache
* the instances based on secret and it is
* less likely that someone will use a different
* secret for each HTTP request.
*
* @method getEncrypter
*
* @param {String} secret
*
* @return {Object}
*/
const getEncrypter = function (secret) {
if (!encrypters[secret]) {
encrypters[secret] = simpleEncryptor({
key: secret,
hmac: false
})
}
return encrypters[secret]
}

/**
* Cookie parser is a simple utility module to read
Expand All @@ -32,6 +58,8 @@ let Cookie = exports = module.exports = {}
* @param {String} value
*
* @return {String|Object|Null}
*
* @private
*/
Cookie._parseJSON = function (value) {
if (typeof (value) === 'string' && value.substr(0, 2) !== 'j:') {
Expand Down Expand Up @@ -93,6 +121,7 @@ Cookie._signValue = function (value, secret = null) {
* @private
*/
Cookie._unSignValue = function (value, secret = null) {
value = String(value)
/**
* Value is not signed, return as it is.
*/
Expand Down Expand Up @@ -124,13 +153,71 @@ Cookie._unSignValue = function (value, secret = null) {
* @param {String} cookie
*
* @return {void}
*
* @private
*/
Cookie._append = function (res, key, cookie) {
const cookies = res.getHeader('Set-Cookie') || []
Array.isArray(cookies) ? cookies.push(cookie) : [cookies].push(cookie)
res.setHeader('Set-Cookie', cookies.map(String))
}

/**
* Encrypts a string with AES-256 encryption.
*
* @method _encrypt
*
* @param {String} value
* @param {String} secret
*
* @return {String}
*
* @private
*/
Cookie._encrypt = function (value, secret) {
return getEncrypter(secret).encrypt(value)
}

/**
* Decrypts the encrypted value. Make sure the secret
* is same when decrypting values
*
* @method _decrypt
*
* @param {String} value
* @param {String} secret
*
* @return {String}
*
* @private
*/
Cookie._decrypt = function (value, secret) {
return getEncrypter(secret).decrypt(value)
}

/**
* Returns an object of cookies. If cookie header
* has no value, it will return an empty object.
*
* @method _parseCookies
*
* @param {Object} req
*
* @return {Object}
*
* @private
*/
Cookie._parseCookies = function (req) {
const cookieString = req.headers['cookie']

/**
* Return an empty object when header value for
* cookie is empty.
*/
if (!cookieString) return {}
return parser.parse(cookieString)
}

/**
* Parses cookies from HTTP header `Cookie` into
* a javascript object. Also it will unsign
Expand All @@ -146,16 +233,6 @@ Cookie._append = function (res, key, cookie) {
* @return {Object}
*/
Cookie.parse = function (req, secret = null, decrypt = false) {
const cookieString = req.headers['cookie']

/**
* Return an empty object when header value for
* cookie is empty.
*/
if (!cookieString) return {}

const cookies = parser.parse(cookieString)

/**
* We need to parse cookies by unsign them, if secret
* is defined and also converting JSON marked string
Expand All @@ -164,14 +241,54 @@ Cookie.parse = function (req, secret = null, decrypt = false) {
* @type {Object}
*/
const parsedCookies = {}
const cookies = Cookie._parseCookies(req)
Object.keys(cookies).forEach((key) => {
const cookie = Cookie._unSignValue(cookies[key], secret)
parsedCookies[key] = cookie ? Cookie._parseJSON(cookie) : cookie
parsedCookies[key] = Cookie.get(req, key, secret, decrypt, cookies)
})

return parsedCookies
}

/**
* Returns value for a single cookie by its key. It is
* recommended to make use of this function when you
* want to pull a single cookie. Since the `parse`
* method will eagerly unsign and decrypt all the
* cookies.
*
* @method get
*
* @param {Object} req
* @param {String} key
* @param {String} [secret = null]
* @param {Boolean} [decrypt = false]
* @param {Object} [cookies = null] Use existing cookies object over re-parsing them from the header.
*
* @return {Mixed}
*/
Cookie.get = function (req, key, secret = null, decrypt = false, cookies = null) {
cookies = cookies || Cookie._parseCookies(req)
let cookie = cookies[key]

/**
* Return null when cookie value does not
* exists for a given key
*/
if (!cookie) {
return null
}

/**
* Decrypt value when cookie secret is defined
* and decrypt is set to true.
*/
if (secret && decrypt) {
cookie = Cookie._decrypt(cookie, secret)
}

cookie = cookie ? Cookie._unSignValue(cookie, secret) : null
return cookie ? Cookie._parseJSON(cookie) : null
}

/**
* Write cookie to the HTTP response object. It will append
* duplicate cookies to the `Set-Cookie` header, since
Expand All @@ -192,6 +309,14 @@ Cookie.create = function (res, key, value, options = {}, secret = null, encrypt
value = Cookie._stringifyJSON(value)
value = Cookie._signValue(value, secret)

/**
* Encrypt the cookie value only when secret is defined
* and encrypt is set to true
*/
if (secret && encrypt) {
value = Cookie._encrypt(value, secret)
}

const cookie = parser.serialize(key, String(value), options)
Cookie._append(res, key, cookie)
}
Expand All @@ -214,6 +339,5 @@ Cookie.create = function (res, key, value, options = {}, secret = null, encrypt
*/
Cookie.clear = function (res, key, options = {}) {
options.expires = new Date(1)
const cookie = parser.serialize(key, String(''), options)
Cookie._append(res, key, cookie)
Cookie.create(res, key, '', options)
}
138 changes: 138 additions & 0 deletions test/cookie.spec.js
Expand Up @@ -15,6 +15,7 @@ const http = require('http')
const sig = require('cookie-signature')
const Cookie = require('../')
const queryString = require('querystring')
const simpleEncryptor = require('simple-encryptor')

test.group('Parse Cookies', function () {
test('return an empty object when no cookies have been set', async function (assert) {
Expand Down Expand Up @@ -136,6 +137,94 @@ test.group('Parse Cookies', function () {
const res = await supertest(server).get('/').set('Cookie', ['user=foo']).expect(200)
assert.deepEqual(res.body.cookies, {user: 'foo'})
})

test('parse encrypted cookies', async function (assert) {
const SECRET = Math.random().toString(36).substr(2, 16)
const encrypter = simpleEncryptor({
key: SECRET,
hmac: false
})

const server = http.createServer(function (req, res) {
const cookies = Cookie.parse(req, SECRET, true)
res.writeHead(200, {'content-type': 'application/json'})
res.write(JSON.stringify({cookies}))
res.end()
})

const age = `s:${sig.sign('22', SECRET)}`
const res = await supertest(server).get('/').set('Cookie', [`age=${encrypter.encrypt(age)}`]).expect(200)
assert.deepEqual(res.body.cookies, {age: '22'})
})

test('return encrypted value when decrypt is set to false', async function (assert) {
const SECRET = Math.random().toString(36).substr(2, 16)
const encrypter = simpleEncryptor({
key: SECRET,
hmac: false
})

const server = http.createServer(function (req, res) {
const cookies = Cookie.parse(req, SECRET, false)
res.writeHead(200, {'content-type': 'application/json'})
res.write(JSON.stringify({cookies}))
res.end()
})

const age = `s:${sig.sign('22', SECRET)}`
const res = await supertest(server).get('/').set('Cookie', [`age=${encrypter.encrypt(age)}`]).expect(200)
assert.notEqual(res.body.cookies.age, '22')
})

test('return null when secret mis-match', async function (assert) {
const SECRET = Math.random().toString(36).substr(2, 16)
const encrypter = simpleEncryptor({
key: SECRET,
hmac: false
})

const server = http.createServer(function (req, res) {
const cookies = Cookie.parse(req, Math.random().toString(36).substr(2, 16), true)
res.writeHead(200, {'content-type': 'application/json'})
res.write(JSON.stringify({cookies}))
res.end()
})

const age = `s:${sig.sign('22', SECRET)}`
const res = await supertest(server).get('/').set('Cookie', [`age=${encrypter.encrypt(age)}`]).expect(200)
assert.isNull(res.body.cookies.age)
})

test('parse a single cookie', async function (assert) {
const SECRET = Math.random().toString(36).substr(2, 16)
const encrypter = simpleEncryptor({
key: SECRET,
hmac: false
})

const server = http.createServer(function (req, res) {
const age = Cookie.get(req, 'age', SECRET, true)
res.writeHead(200, {'content-type': 'application/json'})
res.write(JSON.stringify({age}))
res.end()
})

const age = `s:${sig.sign('22', SECRET)}`
const res = await supertest(server).get('/').set('Cookie', [`age=${encrypter.encrypt(age)}`]).expect(200)
assert.equal(res.body.age, '22')
})

test('look inside existing cookie set over re-parsing the header', async function (assert) {
const server = http.createServer(function (req, res) {
const age = Cookie.get(req, 'age', null, false, {age: 26})
res.writeHead(200, {'content-type': 'application/json'})
res.write(JSON.stringify({age}))
res.end()
})

const res = await supertest(server).get('/').set('Cookie', ['age=22']).expect(200)
assert.equal(res.body.age, '26')
})
})

// SETTING COOKIES
Expand Down Expand Up @@ -198,4 +287,53 @@ test.group('Set Cookies', function () {
const res = await supertest(server).get('/').expect(200)
assert.deepEqual(res.headers['set-cookie'], ['age=' + queryString.escape('j:' + JSON.stringify({}))])
})

test('set encrypted cookie when encryption is set to true', async function (assert) {
const SECRET = Math.random().toString(36).substr(2, 16)
const valueToBe = sig.sign('22', SECRET)
const encrypter = simpleEncryptor({
key: SECRET,
hmac: false
})

const server = http.createServer(function (req, res) {
Cookie.create(res, 'age', 22, {}, SECRET, true)
res.writeHead(200, {'content-type': 'application/json'})
res.end()
})

const res = await supertest(server).get('/').expect(200)
const value = res.headers['set-cookie'][0].replace('age=', '')
assert.equal(encrypter.decrypt(queryString.unescape(value)), `s:${valueToBe}`)
})

test('set object as encrypted cookie', async function (assert) {
const SECRET = Math.random().toString(36).substr(2, 16)
const valueToBe = sig.sign(`j:${JSON.stringify({name: 'virk'})}`, SECRET)
const encrypter = simpleEncryptor({
key: SECRET,
hmac: false
})

const server = http.createServer(function (req, res) {
Cookie.create(res, 'name', {name: 'virk'}, {}, SECRET, true)
res.writeHead(200, {'content-type': 'application/json'})
res.end()
})

const res = await supertest(server).get('/').expect(200)
const value = res.headers['set-cookie'][0].replace('name=', '')
assert.equal(encrypter.decrypt(queryString.unescape(value)), `s:${valueToBe}`)
})

test('set expiry to past when clear cookie is called', async function (assert) {
const server = http.createServer(function (req, res) {
Cookie.clear(res, 'name')
res.writeHead(200, {'content-type': 'application/json'})
res.end()
})

const res = await supertest(server).get('/').expect(200)
assert.deepEqual(res.headers['set-cookie'], ['name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT'])
})
})

0 comments on commit aaf2e65

Please sign in to comment.