diff --git a/examples/encrypted-cookies/index.js b/examples/encrypted-cookies/index.js
new file mode 100644
index 00000000000..c4ec38e5e48
--- /dev/null
+++ b/examples/encrypted-cookies/index.js
@@ -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 forget! Click here to decrypt.' +
+ '
'
+ );
+ } else {
+ res.send(
+ '',
+ );
+ }
+});
+
+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 + '
Go back');
+});
+
+/* istanbul ignore next */
+if (!module.parent) {
+ app.listen(3000);
+ console.log('Express started on port 3000');
+}
diff --git a/lib/response.js b/lib/response.js
index f965e539dd2..729e178c514 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -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.
@@ -714,7 +718,6 @@ res.clearCookie = function clearCookie(name, options) {
return this.cookie(name, '', opts);
};
-
/**
* Set cookie `name` to `value`, with the given `options`.
*
@@ -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) {
@@ -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')
};
/**
diff --git a/test/res.cookie.js b/test/res.cookie.js
index 180d1be3452..0ccd4aac826 100644
--- a/test/res.cookie.js
+++ b/test/res.cookie.js
@@ -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();