Socket
Socket
Sign inDemoInstall

client-sessions

Package Overview
Dependencies
1
Maintainers
3
Versions
19
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.5.0 to 0.6.0

278

lib/client-sessions.js

@@ -14,2 +14,24 @@ /* This Source Code Form is subject to the terms of the Mozilla Public

const KDF_ENC = 'cookiesession-encryption';
const KDF_MAC = 'cookiesession-signature';
/* map from cipher algorithm to exact key byte length */
const ENCRYPTION_ALGORITHMS = {
aes128: 16, // implicit CBC mode
aes192: 24,
aes256: 32
};
const DEFAULT_ENCRYPTION_ALGO = 'aes256';
/* map from hmac algorithm to _minimum_ key byte length */
const SIGNATURE_ALGORITHMS = {
'sha256': 32,
'sha256-drop128': 32,
'sha384': 48,
'sha384-drop192': 48,
'sha512': 64,
'sha512-drop256': 64
};
const DEFAULT_SIGNATURE_ALGO = 'sha256';
function isObject(val) {

@@ -47,2 +69,10 @@ return Object.prototype.toString.call(val) === '[object Object]';

function forceBuffer(binaryOrBuffer) {
if (Buffer.isBuffer(binaryOrBuffer)) {
return binaryOrBuffer;
} else {
return new Buffer(binaryOrBuffer, 'binary');
}
}
function deriveKey(master, type) {

@@ -52,5 +82,55 @@ // eventually we want to use HKDF. For now we'll do something simpler.

hmac.update(type);
return hmac.digest('binary');
return forceBuffer(hmac.digest());
}
function setupKeys(opts) {
// derive two keys, one for signing one for encrypting, from the secret.
if (!opts.encryptionKey) {
opts.encryptionKey = deriveKey(opts.secret, KDF_ENC);
}
if (!opts.signatureKey) {
opts.signatureKey = deriveKey(opts.secret, KDF_MAC);
}
if (!opts.signatureAlgorithm) {
opts.signatureAlgorithm = DEFAULT_SIGNATURE_ALGO;
}
if (!opts.encryptionAlgorithm) {
opts.encryptionAlgorithm = DEFAULT_ENCRYPTION_ALGO;
}
}
function keyConstraints(opts) {
if (!Buffer.isBuffer(opts.encryptionKey)) {
throw new Error('encryptionKey must be a Buffer');
}
if (!Buffer.isBuffer(opts.signatureKey)) {
throw new Error('signatureKey must be a Buffer');
}
if (constantTimeEquals(opts.encryptionKey, opts.signatureKey)) {
throw new Error('Encryption and Signature keys must be different');
}
var encAlgo = opts.encryptionAlgorithm;
var required = ENCRYPTION_ALGORITHMS[encAlgo];
if (opts.encryptionKey.length !== required) {
throw new Error(
'Encryption Key for '+encAlgo+' must be exactly '+required+' bytes '+
'('+(required*8)+' bits)'
);
}
var sigAlgo = opts.signatureAlgorithm;
var minimum = SIGNATURE_ALGORITHMS[sigAlgo];
if (opts.signatureKey.length < minimum) {
throw new Error(
'Encryption Key for '+sigAlgo+' must be at least '+minimum+' bytes '+
'('+(minimum*8)+' bits)'
);
}
}
function constantTimeEquals(a, b) {

@@ -69,2 +149,55 @@ // Ideally this would be a native function, so it's less sensitive to how the

// it's good cryptographic pracitice to not leave buffers with sensitive
// contents hanging around.
function zeroBuffer(buf) {
for (var i = 0; i < buf.length; i++) {
buf[i] = 0;
}
return buf;
}
function hmacInit(algo, key) {
var match = algo.match(/^([^-]+)(?:-drop(\d+))?$/);
var baseAlg = match[1];
var drop = match[2] ? parseInt(match[2], 10) : 0;
var hmacAlg = crypto.createHmac(baseAlg, key);
var origDigest = hmacAlg.digest;
if (drop === 0) {
// Before 0.10, crypto returns binary-encoded strings. Remove when dropping
// 0.8 support.
hmacAlg.digest = function() {
return forceBuffer(origDigest.call(this));
};
} else {
var N = drop / 8; // bits to bytes
hmacAlg.digest = function dropN() {
var result = forceBuffer(origDigest.call(this));
// Throw away the second half of the 512-bit result, leaving the first
// 256-bits.
var truncated = new Buffer(N);
result.copy(truncated, 0, 0, N);
zeroBuffer(result);
return truncated;
};
}
return hmacAlg;
}
function computeHmac(opts, iv, ciphertext, duration, createdAt) {
var hmacAlg = hmacInit(opts.signatureAlgorithm, opts.signatureKey);
hmacAlg.update(iv);
hmacAlg.update(".");
hmacAlg.update(ciphertext);
hmacAlg.update(".");
hmacAlg.update(createdAt.toString());
hmacAlg.update(".");
hmacAlg.update(duration.toString());
return hmacAlg.digest();
}
function encode(opts, content, duration, createdAt){

@@ -80,10 +213,4 @@ // format will be:

if (!opts.encryptionKey) {
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
}
setupKeys(opts);
if (!opts.signatureKey) {
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
}
duration = duration || 24*60*60*1000;

@@ -96,26 +223,23 @@ createdAt = createdAt || new Date().getTime();

// encrypt with encryption key
var plaintext = opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content);
var cipher = crypto.createCipheriv('aes256', opts.encryptionKey, iv);
var ciphertext = cipher.update(plaintext, 'utf8', 'binary');
ciphertext += cipher.final('binary');
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
ciphertext = new Buffer(ciphertext, 'binary');
var plaintext = new Buffer(
opts.cookieName + COOKIE_NAME_SEP + JSON.stringify(content),
'utf8'
);
var cipher = crypto.createCipheriv(
opts.encryptionAlgorithm,
opts.encryptionKey,
iv
);
var ciphertextStart = forceBuffer(cipher.update(plaintext));
zeroBuffer(plaintext);
var ciphertextEnd = forceBuffer(cipher.final());
var ciphertext = Buffer.concat([ciphertextStart, ciphertextEnd]);
zeroBuffer(ciphertextStart);
zeroBuffer(ciphertextEnd);
// 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 = computeHmac(opts, iv, ciphertext, duration, createdAt);
var hmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
hmac = new Buffer(hmac, 'binary');
return [
var result = [
base64urlencode(iv),

@@ -127,5 +251,14 @@ base64urlencode(ciphertext),

].join('.');
zeroBuffer(iv);
zeroBuffer(ciphertext);
zeroBuffer(hmac);
return result;
}
function decode(opts, content) {
if (!opts.cookieName) {
throw new Error("cookieName option required");
}

@@ -138,14 +271,4 @@ // stop at any time if there's an issue

if (!opts.cookieName) {
throw new Error("cookieName option required");
}
setupKeys(opts);
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]);

@@ -157,4 +280,14 @@ var ciphertext = base64urldecode(components[1]);

function cleanup() {
zeroBuffer(iv);
zeroBuffer(ciphertext);
zeroBuffer(hmac);
if (expectedHmac) { // declared below
zeroBuffer(expectedHmac);
}
}
// make sure IV is right length
if (iv.length !== 16) {
cleanup();
return;

@@ -164,17 +297,6 @@ }

// 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 expectedHmac = computeHmac(opts, iv, ciphertext, duration, createdAt);
var expectedHmac = hmacAlg.digest();
// Before 0.10, crypto returns binary-encoded strings. Remove when
// dropping 0.8 support.
expectedHmac = new Buffer(expectedHmac, 'binary');
if (!constantTimeEquals(hmac, expectedHmac)) {
cleanup();
return;

@@ -184,3 +306,7 @@ }

// decrypt
var cipher = crypto.createDecipheriv('aes256', opts.encryptionKey, iv);
var cipher = crypto.createDecipheriv(
opts.encryptionAlgorithm,
opts.encryptionKey,
iv
);
var plaintext = cipher.update(ciphertext, 'binary', 'utf8');

@@ -191,7 +317,9 @@ plaintext += cipher.final('utf8');

if (cookieName !== opts.cookieName) {
cleanup();
return;
}
var result;
try {
return {
result = {
content: JSON.parse(

@@ -203,5 +331,7 @@ plaintext.substring(plaintext.indexOf(COOKIE_NAME_SEP) + 1)

};
} catch (x) {
return;
} catch (ignored) {
}
cleanup();
return result;
}

@@ -261,3 +391,3 @@

// the cookie should expire when it becomes invalid
// we add an extra second because the conversion to a date
// we add an extra second because the conversion to a date
// truncates the milliseconds

@@ -398,3 +528,2 @@ this.expires = new Date(time + this.duration + 1000);

function clientSessionFactory(opts) {

@@ -405,4 +534,5 @@ if (!opts) {

if (!opts.secret) {
throw new Error("cannot set up sessions without a secret");
if (!(opts.secret || (opts.encryptionKey && opts.signatureKey))) {
throw new Error("cannot set up sessions without a secret "+
"or encryptionKey/signatureKey pair");
}

@@ -416,2 +546,18 @@

var encAlg = opts.encryptionAlgorithm || DEFAULT_ENCRYPTION_ALGO;
encAlg = encAlg.toLowerCase();
if (!ENCRYPTION_ALGORITHMS[encAlg]) {
throw new Error('invalid encryptionAlgorithm, supported are: '+
Object.keys(ENCRYPTION_ALGORITHMS).join(', '));
}
opts.encryptionAlgorithm = encAlg;
var sigAlg = opts.signatureAlgorithm || DEFAULT_SIGNATURE_ALGO;
sigAlg = sigAlg.toLowerCase();
if (!SIGNATURE_ALGORITHMS[sigAlg]) {
throw new Error('invalid signatureAlgorithm, supported are: '+
Object.keys(SIGNATURE_ALGORITHMS).join(', '));
}
opts.signatureAlgorithm = sigAlg;
// set up cookie defaults

@@ -431,5 +577,4 @@ opts.cookie = opts.cookie || {};

// derive two keys, one for signing one for encrypting, from the secret.
opts.encryptionKey = deriveKey(opts.secret, 'cookiesession-encryption');
opts.signatureKey = deriveKey(opts.secret, 'cookiesession-signature');
setupKeys(opts);
keyConstraints(opts);

@@ -468,3 +613,3 @@ const propertyName = opts.requestKey || opts.cookieName;

var writeHead = res.writeHead;

@@ -487,3 +632,4 @@ res.writeHead = function () {

encode: encode,
decode: decode
decode: decode,
computeHmac: computeHmac
};
{
"name" : "client-sessions",
"version" : "0.5.0",
"version" : "0.6.0",
"description" : "secure sessions stored in cookies",

@@ -5,0 +5,0 @@ "main" : "lib/client-sessions",

@@ -78,3 +78,3 @@ [![build status](https://secure.travis-ci.org/mozilla/node-client-sessions.png)](http://travis-ci.org/mozilla/node-client-sessions)

requestKey: 'forcedSessionKey', // requestKey overrides cookieName for the key name added to the request object.
secret: 'blargadeeblargblarg', // should be a large unguessable string
secret: 'blargadeeblargblarg', // should be a large unguessable string or Buffer
duration: 24 * 60 * 60 * 1000, // how long the session will stay valid in ms

@@ -93,2 +93,146 @@ }));

## Cryptography
A pair of encryption and signature keys are derived from the `secret` option
via HMAC-SHA-256; the `secret` isn't used directly to encrypt or compute the
MAC.
The key-derivation function, in pseudocode:
```text
encKey := HMAC-SHA-256(secret, 'cookiesession-encryption');
sigKey := HMAC-SHA-256(secret, 'cookiesession-signature');
```
The **AES-256-CBC** cipher is used to encrypt the session contents, with an
**HMAC-SHA-256** authentication tag (via **Encrypt-then-Mac** composition). A
random 128-bit Initialization Vector (IV) is generated for each encryption
operation (this is the AES block size regardless of the key size). The
CBC-mode input is padded with the usual PKCS#5 scheme.
In pseudocode, the encryption looks like the following, with `||` denoting
concatenation. The `createdAt` and `duration` parameters are decimal strings.
```text
iv := secureRandom(16 bytes)
ciphertext := AES-256-CBC(encKey, iv, sessionJson)
payload := iv || '.' || ciphertext || '.' || createdAt || '.' || duration
hmac := HMAC-SHA-256(sigKey, payload)
cookie := base64url(iv) || '.' ||
base64url(ciphertext) || '.' ||
createdAt || '.' ||
duration || '.' ||
base64url(hmac)
```
For decryption, a constant-time equality operation is used to verify the HMAC
output to avoid the plausible timing attack.
### Advanced Cryptographic Options
The defaults are secure, but may not suit your requirements. Some example scenarios:
- You want to use randomly-generated keys instead of using the key-derivation
function used in this module.
- AES-256 is overkill for the type of data you store in the session (e.g. not
personally-identifiable or sensitive) and you'd like to trade-off decreasing
the security level for CPU economy.
- SHA-256 is maybe too weak for your application and you want to have more
MAC security by using SHA-512, which grows the size of your cookies slightly.
If the defaults don't suit your needs, you can customize client-sessions.
**Beware: Changing keys and/or algorithms will make previously-generated
Cookies invalid!**
#### Configuring Keys
To configure independent encryption and signature (HMAC) keys:
```js
app.use(sessions({
encryptionKey: loadFromKeyStore('session-encryption-key'),
signatureKey: loadFromKeyStore('session-signature-key'),
// ... other options discussed above ...
}));
```
#### Configuring Algorithms
To specify custom algorithms and keys:
```js
app.use(sessions({
// use WEAKER-than-default encryption:
encryptionAlgorithm: 'aes128',
encryptionKey: loadFromKeyStore('session-encryption-key'),
// use a SHORTER-than-default MAC:
signatureAlgorithm: 'sha256-drop128',
signatureKey: loadFromKeyStore('session-signature-key'),
// ... other options discussed above ...
}));
```
#### Encryption Algorithms
Supported CBC-mode `encryptionAlgorithm`s (and key length requirements):
| Cipher | Key length |
| ------ | ---------- |
| aes128 | 16 bytes |
| aes192 | 24 bytes |
| aes256 | 32 bytes |
These key lengths are exactly as required by the [Advanced Encryption
Standard](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
#### Signature (HMAC) Algorithms
Supported HMAC `signatureAlgorithm`s (and key length requirements):
| HMAC | Minimum Key Length | Maximum Key Length |
| -------------- | ------------------ | ------------------ |
| sha256 | 32 bytes | 64 bytes |
| sha256-drop128 | 32 bytes | 64 bytes |
| sha384 | 48 bytes | 128 bytes |
| sha384-drop192 | 48 bytes | 128 bytes |
| sha512 | 64 bytes | 128 bytes |
| sha512-drop256 | 64 bytes | 128 bytes |
The HMAC key length requirements are derived from [RFC 2104 section
3](https://tools.ietf.org/html/rfc2104#section-3). The maximum key length can
be exceeded, but it doesn't increase the security of the signature.
The `-dropN` algorithms discard the latter half of the HMAC output, which
provides some additional protection against SHA2 length-extension attacks on
top of HMAC. The same technique is used in the upcoming [JSON Web Algorithms
`AES_CBC_HMAC_SHA2` authenticated
cipher](http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-19#section-5.2).
#### Generating Keys
One can easily generate both AES and HMAC-SHA2 keys via command line: `openssl
rand -base64 32` for a 32-byte (256-bit) key. It's easy to then parse that
output into a `Buffer`:
```js
function loadKeyFromStore(name) {
var text = myConfig.keys[name];
return new Buffer(text, 'base64');
}
```
#### Key Constraints
If you specify `encryptionKey` or `signatureKey`, you must supply the other as
well.
The following constraints must be met or an `Error` will be thrown:
1. both keys must be `Buffer`s.
2. the keys must be _different_.
3. the encryption key are _exactly_ the length required (see above).
4. the signature key has _at least_ the length required (see above).
Based on the above, please note that if you specify a `secret` _and_ a
`signatureAlgorithm`, you need to use `sha256` or `sha256-drop128`.
## License

@@ -95,0 +239,0 @@

@@ -829,3 +829,2 @@ // a NODE_ENV of test will supress console output to stderr which

},
"encode " : function(err, req){

@@ -1191,2 +1190,57 @@ var result = cookieSessions.util.encode({cookieName: 'session', secret: 'yo'}, {foo:'bar'});

var sixtyFourByteKey = new Buffer(
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'binary'
);
var HMAC_EXPECT = {
// aligned so you can see the dropN effect:
'sha256':
'PRYaxV/8RkMyIT/Ib+tIUOWiSn+0EvodJ5rtG1FQHz0=',
'sha256-drop128':
'PRYaxV/8RkMyIT/Ib+tIUA==',
'sha384':
'MND9nz6pxbQC5m41ZPRXhJIuqTj9/hu4gtWZ8t8LgdFLQFWQfC8jhijB0NHLpeA7',
'sha384-drop192':
'MND9nz6pxbQC5m41ZPRXhJIuqTj9/hu4',
'sha512':
'Hr4KLVLyglIwQ43C9U2bmieWBVLnD/F+lzCSF072Ds2b87MK+gbnR0p75A+I+5ez+aiemMGuMZyKVAUWfMMaUA==',
'sha512-drop256':
'Hr4KLVLyglIwQ43C9U2bmieWBVLnD/F+lzCSF072Ds0='
};
function testHmac(algo) {
var block = {};
block.topic = function() {
var opts = {
signatureAlgorithm: algo,
signatureKey: sixtyFourByteKey
};
var iv = new Buffer('01234567890abcdef','binary'); // 128-bits
var ciphertext = new Buffer('0123456789abcdef0123','binary');
var duration = 876543210;
var createdAt = 1234567890;
return cookieSessions.util.computeHmac(
opts, iv, ciphertext, duration, createdAt
).toString('base64');
};
block['equals test vector'] = function(val) {
assert.equal(val, HMAC_EXPECT[algo]);
};
return block;
}
suite.addBatch({
"computeHmac": {
"sha256": testHmac('sha256'),
"sha256-drop128": testHmac('sha256-drop128'),
"sha384": testHmac('sha384'),
"sha384-drop192": testHmac('sha384-drop192'),
"sha512": testHmac('sha512'),
"sha512-drop256": testHmac('sha512-drop256'),
}
});
suite.export(module);

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc