Skip to content

Commit 1af1602

Browse files
committed
Add Hmac.validate() and timingSafeEquals().
1 parent 9bee03a commit 1af1602

File tree

4 files changed

+83
-3
lines changed

4 files changed

+83
-3
lines changed

doc/api/crypto.markdown

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,14 @@ encoding of `'binary'` is enforced. If `data` is a [`Buffer`][] then
605605

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

608+
### hmac.validate(inputHmac)
609+
610+
Return true if and only if the computed hmac matches the input hmac,
611+
provided as a [Buffer](buffer.html). This uses a timing-safe comparison.
612+
613+
The `hmac` object can not be used after `validate()` method has been
614+
called.
615+
608616
## Class: Hmac
609617

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

676+
Caution: Code that uses `digest()` directly for comparison with an input value
677+
is very likely to introduce a
678+
[timing attack](http://codahale.com/a-lesson-in-timing-attacks/).
679+
Such a timing attack would allow someone to construct an
680+
HMAC value for a message of their choosing without posessing the key.
681+
Prefer `validate()`, which does a timing-safe comparison.
682+
668683
The `Hmac` object can not be used again after `hmac.digest()` has been
669684
called. Multiple calls to `hmac.digest()` will result in an error being thrown.
670685

@@ -1153,6 +1168,15 @@ keys:
11531168

11541169
All paddings are defined in the `constants` module.
11551170

1171+
## crypto.timingSafeEqual(a, b)
1172+
1173+
Returns true if a is equal to b, without leaking timing information that would
1174+
help an attacker guess one of the values. This is suitable for comparing secret
1175+
values like authentication cookies or
1176+
[capability urls](http://www.w3.org/TR/capability-urls/).
1177+
1178+
`a` and `b` can be strings or [Buffers](buffer.html).
1179+
11561180
### crypto.privateEncrypt(private_key, buffer)
11571181

11581182
Encrypts `buffer` with `private_key`.

lib/crypto.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ exports.createHmac = exports.Hmac = Hmac;
8383
function Hmac(hmac, key, options) {
8484
if (!(this instanceof Hmac))
8585
return new Hmac(hmac, key, options);
86+
this.key = key;
8687
this._handle = new binding.Hmac();
8788
this._handle.init(hmac, toBuf(key));
8889
LazyTransform.call(this, options);
@@ -95,6 +96,25 @@ Hmac.prototype.digest = Hash.prototype.digest;
9596
Hmac.prototype._flush = Hash.prototype._flush;
9697
Hmac.prototype._transform = Hash.prototype._transform;
9798

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

99119
function getDecoder(decoder, encoding) {
100120
if (encoding === 'utf-8') encoding = 'utf8'; // Normalize encoding.
@@ -662,6 +682,14 @@ function filterDuplicates(names) {
662682
}).sort();
663683
}
664684

685+
// timingSafeEqual reuses the timing-safe Hmac.equals function (see above) for
686+
// comparison of non-hash inputs, like capability URLs or authentication tokens.
687+
exports.timingSafeEqual = function(a, b) {
688+
var key = randomBytes(32);
689+
var ah = new Hmac('sha256', key).update(a);
690+
return ah.validate(new Hmac('sha256', key).update(b).digest());
691+
};
692+
665693
// Legacy API
666694
exports.__defineGetter__('createCredentials',
667695
internalUtil.deprecate(function() {

test/parallel/test-crypto-hmac.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,24 @@ var crypto = require('crypto');
1111
// Test HMAC
1212
var h1 = crypto.createHmac('sha1', 'Node')
1313
.update('some data')
14-
.update('to hmac')
15-
.digest('hex');
16-
assert.equal(h1, '19fd6e1ba73d9ed2224dd5094a71babe85d9a892', 'test HMAC');
14+
.update('to hmac');
15+
assert.equal(h1.digest('hex'),
16+
'19fd6e1ba73d9ed2224dd5094a71babe85d9a892',
17+
'test HMAC');
18+
19+
var h2 = crypto.createHmac('sha1', 'Node')
20+
.update('some data')
21+
.update('to hmac');
22+
assert.ok(h2.validate(
23+
new Buffer('19fd6e1ba73d9ed2224dd5094a71babe85d9a892', 'hex'),
24+
'test HMAC valid'));
25+
26+
var h3 = crypto.createHmac('sha1', 'Node')
27+
.update('some data')
28+
.update('to hmac');
29+
assert.ok(!h3.validate(
30+
new Buffer('6bdee6ee47fb42c53a4f44c3e4bb97591c0c3635', 'hex'),
31+
'test HMAC not valid'));
1732

1833
// Test HMAC (Wikipedia Test Cases)
1934
var wikipedia = [
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
var common = require('../common');
3+
var assert = require('assert');
4+
5+
if (!common.hasCrypto) {
6+
console.log('1..0 # Skipped: missing crypto');
7+
return;
8+
}
9+
var crypto = require('crypto');
10+
11+
assert.ok(crypto.timingSafeEqual('alpha', 'alpha'), 'equal strings not equal');
12+
assert.ok(!crypto.timingSafeEqual('alpha', 'beta'),
13+
'inequal strings considered equal');

0 commit comments

Comments
 (0)