Skip to content

Commit

Permalink
Add HKDF fallback for Node 14, where SubtleCrypto is not available
Browse files Browse the repository at this point in the history
  • Loading branch information
larabr committed Jul 25, 2023
1 parent ee4ad89 commit ef953ce
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 9 deletions.
50 changes: 45 additions & 5 deletions src/crypto/hkdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,53 @@ import util from '../util';

const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
const nodeSubtleCrypto = nodeCrypto && nodeCrypto.webcrypto && nodeCrypto.webcrypto.subtle;

export default async function HKDF(hashAlgo, key, salt, info, length) {
export default async function HKDF(hashAlgo, inputKey, salt, info, outLen) {
const hash = enums.read(enums.webHash, hashAlgo);
if (!hash) throw new Error('Hash algo not supported with HKDF');

const crypto = webCrypto || nodeCrypto.webcrypto.subtle;
const importedKey = await crypto.importKey('raw', key, 'HKDF', false, ['deriveBits']);
const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, length * 8);
return new Uint8Array(bits);
if (webCrypto || nodeSubtleCrypto) {
const crypto = webCrypto || nodeSubtleCrypto;
const importedKey = await crypto.importKey('raw', inputKey, 'HKDF', false, ['deriveBits']);
const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, outLen * 8);
return new Uint8Array(bits);
}

if (nodeCrypto) {
const hashAlgoName = enums.read(enums.hash, hashAlgo);
// Node-only HKDF implementation based on https://www.rfc-editor.org/rfc/rfc5869

const computeHMAC = (hmacKey, hmacMessage) => nodeCrypto.createHmac(hashAlgoName, hmacKey).update(hmacMessage).digest();
// Step 1: Extract
// PRK = HMAC-Hash(salt, IKM)
const pseudoRandomKey = computeHMAC(salt, inputKey);

const hashLen = pseudoRandomKey.length;

// Step 2: Expand
// HKDF-Expand(PRK, info, L) -> OKM
const n = Math.ceil(outLen / hashLen);
const outputKeyingMaterial = new Uint8Array(n * hashLen);

// HMAC input buffer updated at each iteration
const roundInput = new Uint8Array(hashLen + info.length + 1);
// T_i and last byte are updated at each iteration, but `info` remains constant
roundInput.set(info, hashLen);

for (let i = 0; i < n; i++) {
// T(0) = empty string (zero length)
// T(i) = HMAC-Hash(PRK, T(i-1) | info | i)
roundInput[roundInput.length - 1] = i + 1;
// t = T(i+1)
const t = computeHMAC(pseudoRandomKey, i > 0 ? roundInput : roundInput.subarray(hashLen));
roundInput.set(t, 0);

outputKeyingMaterial.set(t, i * hashLen);
}

return outputKeyingMaterial.subarray(0, outLen);
}

throw new Error('No HKDF implementation available');
}
2 changes: 1 addition & 1 deletion src/crypto/public_key/elliptic/ecdh_x.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const HKDF_INFO = {
/**
* Generate ECDH key for Montgomery curves
* @param {module:enums.publicKey} algo - Algorithm identifier
* @returns Promise<{ A, k }>
* @returns {Promise<{ A: Uint8Array, k: Uint8Array }>}
*/
export async function generate(algo) {
switch (algo) {
Expand Down
5 changes: 2 additions & 3 deletions src/crypto/public_key/elliptic/eddsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ nacl.hash = bytes => new Uint8Array(sha512().update(bytes).digest());
/**
* Generate (non-legacy) EdDSA key
* @param {module:enums.publicKey} algo - Algorithm identifier
* @returns Promise<{ A, seed }>
* @returns {Promise<{ A: Uint8Array, seed: Uint8Array }>}
*/
export async function generate(algo) {
switch (algo) {
Expand All @@ -56,8 +56,7 @@ export async function generate(algo) {
* @param {Uint8Array} privateKey - Private key used to sign the message
* @param {Uint8Array} hashed - The hashed message
* @returns {Promise<{
* r: Uint8Array,
* s: Uint8Array
* RS: Uint8Array
* }>} Signature of the message
* @async
*/
Expand Down
47 changes: 47 additions & 0 deletions test/crypto/hkdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { expect } = require('chai');

const computeHKDF = require('../../src/crypto/hkdf');
const enums = require('../../src/enums');
const util = require('../../src/util');

// WebCrypto implements HKDF natively, no need to test it
const maybeDescribe = util.getNodeCrypto() ? describe : describe;

module.exports = () => maybeDescribe('HKDF test vectors', function() {
// Vectors from https://www.rfc-editor.org/rfc/rfc5869#appendix-A
it('Test Case 1', async function() {
const inputKey = util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
const salt = util.hexToUint8Array('000102030405060708090a0b0c');
const info = util.hexToUint8Array('f0f1f2f3f4f5f6f7f8f9');
const outLen = 42;

const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen);
const expected = util.hexToUint8Array('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865');

expect(actual).to.deep.equal(expected);
});

it('Test Case 2', async function() {
const inputKey = util.hexToUint8Array('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f');
const salt = util.hexToUint8Array('606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf');
const info = util.hexToUint8Array('b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff');
const outLen = 82;

const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen);
const expected = util.hexToUint8Array('b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87');

expect(actual).to.deep.equal(expected);
});

it('Test Case 3', async function() {
const inputKey = util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
const salt = new Uint8Array();
const info = new Uint8Array();
const outLen = 42;

const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen);
const expected = util.hexToUint8Array('8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8');

expect(actual).to.deep.equal(expected);
});
});
1 change: 1 addition & 0 deletions test/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = () => describe('Crypto', function () {
require('./ecdh')();
require('./pkcs5')();
require('./aes_kw')();
require('./hkdf')();
require('./gcm')();
require('./eax')();
require('./ocb')();
Expand Down

0 comments on commit ef953ce

Please sign in to comment.