diff --git a/doc/api/crypto.md b/doc/api/crypto.md index e84c10dfa4b469..2076ac9948c91e 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -681,6 +681,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 possessing the key. +Use `timingSafeEqual(a, b)` to compare digest values. + 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. @@ -1211,6 +1218,16 @@ 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 +HMAC digests or secret values like authentication cookies or +[capability urls](http://www.w3.org/TR/capability-urls/). + +A `TypeError` will be thrown if either `a` or `b` is not a [`Buffer`][] +instance. + ### crypto.privateEncrypt(private_key, buffer) Encrypts `buffer` with `private_key`. diff --git a/lib/crypto.js b/lib/crypto.js index 688ac34e4ad767..e4ea34dfce7547 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -661,6 +661,33 @@ function filterDuplicates(names) { }).sort(); } +// 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. +exports.timingSafeEqual = timingSafeEqual; +function timingSafeEqual(a, b) { + if (!(a instanceof Buffer)) + throw new TypeError('First argument must be a Buffer'); + if (!(b instanceof Buffer)) + throw new TypeError('Second argument must be a Buffer'); + const key = randomBytes(32); + const ah = new Hmac('sha256', key).update(a).digest(); + const bh = new Hmac('sha256', key).update(b).digest(); + // The second test, for a.equals(b), is just in case of the vanishingly small + // chance of a collision. It only fires if the digest comparison passes and so + // doesn't leak timing information. + return ah.equals(bh) && a.equals(b); +} + // Legacy API exports.__defineGetter__('createCredentials', internalUtil.deprecate(function() { diff --git a/test/parallel/test-crypto-timing-safe-equal.js b/test/parallel/test-crypto-timing-safe-equal.js new file mode 100644 index 00000000000000..4913594d3058e4 --- /dev/null +++ b/test/parallel/test-crypto-timing-safe-equal.js @@ -0,0 +1,14 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +if (!common.hasCrypto) { + console.log('1..0 # Skipped: missing crypto'); + return; +} +const crypto = require('crypto'); + +assert.ok(crypto.timingSafeEqual(Buffer.from('alpha'), Buffer.from('alpha')), + 'equal strings not equal'); +assert.ok(!crypto.timingSafeEqual(Buffer.from('alpha'), Buffer.from('beta')), + 'inequal strings considered equal');