Skip to content

Commit

Permalink
Add Hmac.validate() and timingSafeEquals().
Browse files Browse the repository at this point in the history
  • Loading branch information
jsha committed Feb 11, 2016
1 parent 9bee03a commit 1af1602
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 3 deletions.
24 changes: 24 additions & 0 deletions doc/api/crypto.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,14 @@ encoding of `'binary'` is enforced. If `data` is a [`Buffer`][] then

This can be called many times with new data as it is streamed.

### hmac.validate(inputHmac)

Return true if and only if the computed hmac matches the input hmac,
provided as a [Buffer](buffer.html). This uses a timing-safe comparison.

The `hmac` object can not be used after `validate()` method has been
called.

## Class: Hmac

The `Hmac` Class is a utility for creating cryptographic HMAC digests. It can
Expand Down Expand Up @@ -665,6 +673,13 @@ Calculates the HMAC digest of all of the data passed using `hmac.update()`. The
`encoding` can be `'hex'`, `'binary'` or `'base64'`. If `encoding` is provided
a string is returned; otherwise a [`Buffer`][] is returned;

Caution: Code that uses `digest()` directly for comparison with an input value
is very likely to introduce a
[timing attack](http://codahale.com/a-lesson-in-timing-attacks/).
Such a timing attack would allow someone to construct an
HMAC value for a message of their choosing without posessing the key.
Prefer `validate()`, which does a timing-safe comparison.

The `Hmac` object can not be used again after `hmac.digest()` has been
called. Multiple calls to `hmac.digest()` will result in an error being thrown.

Expand Down Expand Up @@ -1153,6 +1168,15 @@ keys:

All paddings are defined in the `constants` module.

## crypto.timingSafeEqual(a, b)

Returns true if a is equal to b, without leaking timing information that would
help an attacker guess one of the values. This is suitable for comparing secret
values like authentication cookies or
[capability urls](http://www.w3.org/TR/capability-urls/).

`a` and `b` can be strings or [Buffers](buffer.html).

### crypto.privateEncrypt(private_key, buffer)

Encrypts `buffer` with `private_key`.
Expand Down
28 changes: 28 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ exports.createHmac = exports.Hmac = Hmac;
function Hmac(hmac, key, options) {
if (!(this instanceof Hmac))
return new Hmac(hmac, key, options);
this.key = key;
this._handle = new binding.Hmac();
this._handle.init(hmac, toBuf(key));
LazyTransform.call(this, options);
Expand All @@ -95,6 +96,25 @@ Hmac.prototype.digest = Hash.prototype.digest;
Hmac.prototype._flush = Hash.prototype._flush;
Hmac.prototype._transform = Hash.prototype._transform;

// This implements Brad Hill's Double HMAC pattern from
// https://www.nccgroup.trust/us/about-us/
// newsroom-and-events/blog/2011/february/double-hmac-verification/.
// In short, it's near-impossible to write a reliable constant-time compare in a
// high level language like JS, because of the many layers that can optimize
// away attempts at being constant time.
//
// Double HMAC avoids that problem by blinding the timing channel instead. After
// running the inputs through a second round of HMAC, we are free to
// short-circuit comparison, because the time it takes to reach the
// short-circuit has no relation to the similarity between the guessed digest
// and the correct one.
//
// Note: hmac object can not be used after validate() method has been called.
Hmac.prototype.validate = function(inputBuffer) {
var ah = new Hmac('sha256', this.key).update(this.digest()).digest();
var bh = new Hmac('sha256', this.key).update(inputBuffer).digest();
return ah.equals(bh);
};

function getDecoder(decoder, encoding) {
if (encoding === 'utf-8') encoding = 'utf8'; // Normalize encoding.
Expand Down Expand Up @@ -662,6 +682,14 @@ function filterDuplicates(names) {
}).sort();
}

// timingSafeEqual reuses the timing-safe Hmac.equals function (see above) for
// comparison of non-hash inputs, like capability URLs or authentication tokens.
exports.timingSafeEqual = function(a, b) {
var key = randomBytes(32);
var ah = new Hmac('sha256', key).update(a);
return ah.validate(new Hmac('sha256', key).update(b).digest());
};

// Legacy API
exports.__defineGetter__('createCredentials',
internalUtil.deprecate(function() {
Expand Down
21 changes: 18 additions & 3 deletions test/parallel/test-crypto-hmac.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,24 @@ var crypto = require('crypto');
// Test HMAC
var h1 = crypto.createHmac('sha1', 'Node')
.update('some data')
.update('to hmac')
.digest('hex');
assert.equal(h1, '19fd6e1ba73d9ed2224dd5094a71babe85d9a892', 'test HMAC');
.update('to hmac');
assert.equal(h1.digest('hex'),
'19fd6e1ba73d9ed2224dd5094a71babe85d9a892',
'test HMAC');

var h2 = crypto.createHmac('sha1', 'Node')
.update('some data')
.update('to hmac');
assert.ok(h2.validate(
new Buffer('19fd6e1ba73d9ed2224dd5094a71babe85d9a892', 'hex'),
'test HMAC valid'));

var h3 = crypto.createHmac('sha1', 'Node')
.update('some data')
.update('to hmac');
assert.ok(!h3.validate(
new Buffer('6bdee6ee47fb42c53a4f44c3e4bb97591c0c3635', 'hex'),
'test HMAC not valid'));

// Test HMAC (Wikipedia Test Cases)
var wikipedia = [
Expand Down
13 changes: 13 additions & 0 deletions test/parallel/test-crypto-timing-safe-equal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';
var common = require('../common');
var assert = require('assert');

if (!common.hasCrypto) {
console.log('1..0 # Skipped: missing crypto');
return;
}
var crypto = require('crypto');

assert.ok(crypto.timingSafeEqual('alpha', 'alpha'), 'equal strings not equal');
assert.ok(!crypto.timingSafeEqual('alpha', 'beta'),
'inequal strings considered equal');

0 comments on commit 1af1602

Please sign in to comment.