Skip to content
Open
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
72 changes: 72 additions & 0 deletions examples/encrypted-cookies/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

/**
* Module dependencies.
*/

var express = require('../../');
var app = (module.exports = express());
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var crypto = require('node:crypto');

// custom log format
if (process.env.NODE_ENV !== 'test') app.use(logger(':method :url'));

// parses request cookies, populating
// req.cookies and req.signedCookies
// when the secret is passed, used
// for signing the cookies.
app.use(cookieParser('my secret here'));

// parses x-www-form-urlencoded
app.use(express.urlencoded());

app.get('/', function (req, res) {
if (req.signedCookies.encryptedCookie) {
res.send('Remembered and encrypted :). Click to <a href="/forget">forget</a>! Click here to decrypt.' +
'<form action="/decryptCookies" method="POST"> <button type="submit">decrypt</button> </form>'
);
} else {
res.send(
'<form method="post"><p>Check to <label>' +
'<input type="checkbox" name="encryptedCookie"/> remember me and encrypt me</label> ' +
'<input type="submit" value="Submit"/>.</p></form>',
);
}
});

app.get('/forget', function (req, res) {
res.clearCookie('encryptedCookie');
res.redirect(req.get('Referrer') || '/');
});

const key = crypto.randomBytes(32);

app.post('/', function (req, res) {
var minute = 60000;

if (req.body && req.body.encryptedCookie) {
res.cookie(
'encryptedCookie',
'I like to hide by cookies under the sofa',
{ signed: true, maxAge: minute },
{ key },
);
}
res.redirect(req.get('Referrer') || '/');
});

app.post('/decryptCookies', function (req, res) {
const encryptedCookie = req.signedCookies.encryptedCookie;

const decryptedCookie = res.decryptCookie(encryptedCookie, key);

res.send(decryptedCookie + '<br> <a href="/">Go back</a>');
});

/* istanbul ignore next */
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
93 changes: 79 additions & 14 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ var extname = path.extname;
var resolve = path.resolve;
var vary = require('vary');
const { Buffer } = require('node:buffer');
var crypto = require('node:crypto')

const encryptionAlgorithm = "aes-256-gcm";


/**
* Response prototype.
Expand Down Expand Up @@ -714,7 +718,6 @@ res.clearCookie = function clearCookie(name, options) {

return this.cookie(name, '', opts);
};

/**
* Set cookie `name` to `value`, with the given `options`.
*
Expand All @@ -732,28 +735,63 @@ res.clearCookie = function clearCookie(name, options) {
* // same as above
* res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
*
* Encrypt:
* - `iv` Initialization Vector used for encryption is recommended you create a entropied random value
* - `key` Key for encrypting and decrypting the encrypted cookie
*
* Examples:
* // Create an encrypted cookie
* res.cookie('encryptedCookie', 'secret thing to be encrypted', {signed: false}, { key: crypto.randomBytes(), iv: crypto.randomBytes(16) })
* res.cookie('encryptedSignedCookie', 'secret thing to be encrypted', {signed: true}, { key: crypto.randomBytes(), iv: crypto.randomBytes(16) })
*
* @param {String} name
* @param {String|Object} value
* @param {Object} [options]
* @param {key: String, iv: Buffer,} encrypt
* @return {ServerResponse} for chaining
* @public
*/

res.cookie = function (name, value, options) {
var opts = { ...options };
var secret = this.req.secret;
var signed = opts.signed;
res.cookie = function (name, value, options, encrypt) {
var opts = { ...options }
var secret = this.req.secret
var signed = opts.signed

if (signed && !secret) {
throw new Error('cookieParser("secret") required for signed cookies');
throw new Error('cookieParser("secret") required for signed cookies')
}

var val =
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)

if (signed && !encrypt) {
val = 's:' + sign(val, secret)
}

var val = typeof value === 'object'
? 'j:' + JSON.stringify(value)
: String(value);
if (encrypt) {
let { key, iv } = encrypt

if (!iv) {
iv = crypto.randomBytes(16);
}

let cipher = crypto.createCipheriv(encryptionAlgorithm, key, iv)

const encryptedText = Buffer.concat([cipher.update(val), cipher.final()])

const encryptedTextObject = {
encryptedText: encryptedText.toString('base64'),
iv: iv.toString('base64'),
}

// If you will use a encryption algorithm that don't support auth tags please remove this part of the code
encryptedTextObject['authTag'] = cipher.getAuthTag().toString('base64')

if (signed) {
val = 's:' + sign(val, secret);
if (signed) {
val = 's:' + sign(JSON.stringify(encryptedTextObject), secret)
} else {
val = JSON.stringify(encryptedTextObject)
}
}

if (opts.maxAge != null) {
Expand All @@ -766,12 +804,39 @@ res.cookie = function (name, value, options) {
}

if (opts.path == null) {
opts.path = '/';
opts.path = '/'
}

this.append('Set-Cookie', cookie.serialize(name, String(val), opts));
this.append('Set-Cookie', cookie.serialize(name, String(val), opts))

return this;
return this
};

/**
* @param {String} encryptedCookie
* @return {String}
* @public
**/

res.decryptCookie = function decryptCookie(encryptedCookie, key) {
let { encryptedText, iv, authTag } = JSON.parse(encryptedCookie)

iv = Buffer.from(iv, 'base64')

encryptedText = Buffer.from(encryptedText, 'base64')

const decipher = crypto.createDecipheriv(encryptionAlgorithm, key, iv)

if (authTag) {
decipher.setAuthTag(Buffer.from(authTag, 'base64'))
}

const plainText = Buffer.concat([
decipher.update(encryptedText),
decipher.final(),
])

return plainText.toString('utf8')
};

/**
Expand Down
66 changes: 66 additions & 0 deletions test/res.cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,72 @@ describe('res', function(){
})
})

describe('.cookie(name, string, options, encrypt)', function () {
it('should return a stringified json with the encrypted cookie', function (done) {
var app = express()
var { Buffer } = require('node:buffer')

app.use(cookieParser('my-secret'))

app.use(function (req, res) {
res.cookie('name', 'tobi', undefined, {
key: Buffer.from([
0x66, 0xcc, 0xc0, 0xa1, 0x9f, 0x64, 0x26, 0x70, 0x84, 0xfe, 0xc7,
0x0b, 0x2a, 0xf5, 0xf9, 0x45, 0x8e, 0xbc, 0x80, 0x4b, 0x60, 0x64,
0xff, 0xc7, 0x77, 0x4f, 0xde, 0x97, 0xc1, 0xdf, 0x09, 0x5b,
]),
iv: Buffer.from([
0xdf, 0x16, 0x7e, 0xd1, 0xc9, 0x2c, 0x24, 0x1b, 0x02, 0x4f, 0x48,
0x24, 0x62, 0xc6, 0x3b, 0x9b,
]),
})
res.end();
})

request(app)
.get('/')
.expect(
'Set-Cookie',
'name=%7B%22encryptedText%22%3A%22wdYTOw%3D%3D%22%2C%22iv%22%3A%223xZ%2B0cksJBsCT0gkYsY7mw%3D%3D%22%2C%22authTag%22%3A%22pbC2HFCHVKkeAVA46GoNtg%3D%3D%22%7D; Path=/',
)
.expect(200, done)
})

it('should return a stringified json with the encrypted signed cookie', function (done) {
var app = express()
var { Buffer } = require('node:buffer')

app.use(cookieParser('my-secret'))

app.use(function (req, res) {
res.cookie(
'name',
'tobi',
{ signed: true },
{
key: Buffer.from([
0x66, 0xcc, 0xc0, 0xa1, 0x9f, 0x64, 0x26, 0x70, 0x84, 0xfe, 0xc7,
0x0b, 0x2a, 0xf5, 0xf9, 0x45, 0x8e, 0xbc, 0x80, 0x4b, 0x60, 0x64,
0xff, 0xc7, 0x77, 0x4f, 0xde, 0x97, 0xc1, 0xdf, 0x09, 0x5b,
]),
iv: Buffer.from([
0xdf, 0x16, 0x7e, 0xd1, 0xc9, 0x2c, 0x24, 0x1b, 0x02, 0x4f, 0x48,
0x24, 0x62, 0xc6, 0x3b, 0x9b,
]),
},
)
res.end()
});

request(app)
.get('/')
.expect(
'Set-Cookie',
'name=s%3A%7B%22encryptedText%22%3A%22wdYTOw%3D%3D%22%2C%22iv%22%3A%223xZ%2B0cksJBsCT0gkYsY7mw%3D%3D%22%2C%22authTag%22%3A%22pbC2HFCHVKkeAVA46GoNtg%3D%3D%22%7D.%2FbjKv%2BoqY%2BsjNKQp%2FyAgxhemLopKyKnQt1ngpRxhfL0; Path=/',
)
.expect(200, done)
})
})
describe('.cookie(name, string, options)', function(){
it('should set params', function(done){
var app = express();
Expand Down