From df7938f3c901a8538c0402f7f692853545902663 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 26 Jan 2022 01:25:17 -0500 Subject: [PATCH] crypto: Implement HKDF for Node.js < 15.0.0 --- src/node/utils/crypto.js | 43 +++++++++++++++++++++++++++++++ src/tests/backend/specs/crypto.js | 41 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/node/utils/crypto.js create mode 100644 src/tests/backend/specs/crypto.js diff --git a/src/node/utils/crypto.js b/src/node/utils/crypto.js new file mode 100644 index 000000000..071b450b1 --- /dev/null +++ b/src/node/utils/crypto.js @@ -0,0 +1,43 @@ +'use strict'; + +const {Buffer} = require('buffer'); +const crypto = require('crypto'); +const util = require('util'); + +// TODO: Delete this once support for Node.js < 15.0.0 is dropped. +const hkdfFallback = async (digest, ikm, salt, info, keylen) => { + // https://datatracker.ietf.org/doc/html/rfc5869#section-2.2 + const prkHmac = crypto.createHmac(digest, salt); + prkHmac.update(ikm); + const prk = prkHmac.digest(); + + // https://datatracker.ietf.org/doc/html/rfc5869#section-2.3 + let len = 0; + const t = [Buffer.alloc(0)]; + while (len < keylen) { + const hmac = crypto.createHmac(digest, prk); + hmac.update(t[t.length - 1]); + hmac.update(info); + hmac.update(Buffer.from([t.length % 256])); + const tn = hmac.digest(); + t.push(tn); + len += tn.length; + } + const buf = Buffer.concat(t); + return (buf.byteOffset === 0 && buf.buffer.byteLength === keylen + ? buf : Uint8Array.prototype.slice.call(buf, 0, keylen)).buffer; +}; + +/** + * Promisified version of Node.js's crypto.hkdf. + */ +exports.hkdf = crypto.hkdf ? util.promisify(crypto.hkdf) : hkdfFallback; + +/** + * Promisified version of Node.js's crypto.randomBytes + */ +exports.randomBytes = util.promisify(crypto.randomBytes); + +exports.exportedForTesting = { + hkdfFallback, +}; diff --git a/src/tests/backend/specs/crypto.js b/src/tests/backend/specs/crypto.js new file mode 100644 index 000000000..6472b2e85 --- /dev/null +++ b/src/tests/backend/specs/crypto.js @@ -0,0 +1,41 @@ +'use strict'; + +const assert = require('assert').strict; +const {Buffer} = require('buffer'); +const crypto = require('../../../node/utils/crypto'); +const nodeCrypto = require('crypto'); +const util = require('util'); + +const nodeHkdf = nodeCrypto.hkdf ? util.promisify(nodeCrypto.hkdf) : null; + +const ab2hex = (ab) => Buffer.from(ab).toString('hex'); + +describe(__filename, function () { + describe('hkdf fallback', function () { + before(async function () { + if (!nodeHkdf) this.skip(); + }); + + const testCases = [ + ['minimal', 'sha256', 1, 0, 0, 1], + ['huge', 'sha512', 1024, 1024, 1024, 16320], + ]; + + for (const [desc, digest, ikmLen, saltLen, infoLen, keyLen] of testCases) { + for (const strings of [false, true]) { + it(`${desc} (${strings ? 'strings' : 'buffers'})`, async function () { + let isi = await Promise.all([ + crypto.randomBytes(ikmLen), + crypto.randomBytes(saltLen), + crypto.randomBytes(infoLen), + ]); + if (strings) isi = isi.map((b) => b.toString('hex').slice(0, b.length)); + const args = [digest, ...isi, keyLen]; + assert.equal( + ab2hex(await crypto.exportedForTesting.hkdfFallback(...args)), + ab2hex(await nodeHkdf(...args))); + }); + } + } + }); +});