Skip to content

Commit ef953ce

Browse files
committed
Add HKDF fallback for Node 14, where SubtleCrypto is not available
1 parent ee4ad89 commit ef953ce

5 files changed

Lines changed: 96 additions & 9 deletions

File tree

src/crypto/hkdf.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,53 @@ import util from '../util';
99

1010
const webCrypto = util.getWebCrypto();
1111
const nodeCrypto = util.getNodeCrypto();
12+
const nodeSubtleCrypto = nodeCrypto && nodeCrypto.webcrypto && nodeCrypto.webcrypto.subtle;
1213

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

17-
const crypto = webCrypto || nodeCrypto.webcrypto.subtle;
18-
const importedKey = await crypto.importKey('raw', key, 'HKDF', false, ['deriveBits']);
19-
const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, length * 8);
20-
return new Uint8Array(bits);
18+
if (webCrypto || nodeSubtleCrypto) {
19+
const crypto = webCrypto || nodeSubtleCrypto;
20+
const importedKey = await crypto.importKey('raw', inputKey, 'HKDF', false, ['deriveBits']);
21+
const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, outLen * 8);
22+
return new Uint8Array(bits);
23+
}
24+
25+
if (nodeCrypto) {
26+
const hashAlgoName = enums.read(enums.hash, hashAlgo);
27+
// Node-only HKDF implementation based on https://www.rfc-editor.org/rfc/rfc5869
28+
29+
const computeHMAC = (hmacKey, hmacMessage) => nodeCrypto.createHmac(hashAlgoName, hmacKey).update(hmacMessage).digest();
30+
// Step 1: Extract
31+
// PRK = HMAC-Hash(salt, IKM)
32+
const pseudoRandomKey = computeHMAC(salt, inputKey);
33+
34+
const hashLen = pseudoRandomKey.length;
35+
36+
// Step 2: Expand
37+
// HKDF-Expand(PRK, info, L) -> OKM
38+
const n = Math.ceil(outLen / hashLen);
39+
const outputKeyingMaterial = new Uint8Array(n * hashLen);
40+
41+
// HMAC input buffer updated at each iteration
42+
const roundInput = new Uint8Array(hashLen + info.length + 1);
43+
// T_i and last byte are updated at each iteration, but `info` remains constant
44+
roundInput.set(info, hashLen);
45+
46+
for (let i = 0; i < n; i++) {
47+
// T(0) = empty string (zero length)
48+
// T(i) = HMAC-Hash(PRK, T(i-1) | info | i)
49+
roundInput[roundInput.length - 1] = i + 1;
50+
// t = T(i+1)
51+
const t = computeHMAC(pseudoRandomKey, i > 0 ? roundInput : roundInput.subarray(hashLen));
52+
roundInput.set(t, 0);
53+
54+
outputKeyingMaterial.set(t, i * hashLen);
55+
}
56+
57+
return outputKeyingMaterial.subarray(0, outLen);
58+
}
59+
60+
throw new Error('No HKDF implementation available');
2161
}

src/crypto/public_key/elliptic/ecdh_x.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const HKDF_INFO = {
2020
/**
2121
* Generate ECDH key for Montgomery curves
2222
* @param {module:enums.publicKey} algo - Algorithm identifier
23-
* @returns Promise<{ A, k }>
23+
* @returns {Promise<{ A: Uint8Array, k: Uint8Array }>}
2424
*/
2525
export async function generate(algo) {
2626
switch (algo) {

src/crypto/public_key/elliptic/eddsa.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ nacl.hash = bytes => new Uint8Array(sha512().update(bytes).digest());
3333
/**
3434
* Generate (non-legacy) EdDSA key
3535
* @param {module:enums.publicKey} algo - Algorithm identifier
36-
* @returns Promise<{ A, seed }>
36+
* @returns {Promise<{ A: Uint8Array, seed: Uint8Array }>}
3737
*/
3838
export async function generate(algo) {
3939
switch (algo) {
@@ -56,8 +56,7 @@ export async function generate(algo) {
5656
* @param {Uint8Array} privateKey - Private key used to sign the message
5757
* @param {Uint8Array} hashed - The hashed message
5858
* @returns {Promise<{
59-
* r: Uint8Array,
60-
* s: Uint8Array
59+
* RS: Uint8Array
6160
* }>} Signature of the message
6261
* @async
6362
*/

test/crypto/hkdf.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const { expect } = require('chai');
2+
3+
const computeHKDF = require('../../src/crypto/hkdf');
4+
const enums = require('../../src/enums');
5+
const util = require('../../src/util');
6+
7+
// WebCrypto implements HKDF natively, no need to test it
8+
const maybeDescribe = util.getNodeCrypto() ? describe : describe;
9+
10+
module.exports = () => maybeDescribe('HKDF test vectors', function() {
11+
// Vectors from https://www.rfc-editor.org/rfc/rfc5869#appendix-A
12+
it('Test Case 1', async function() {
13+
const inputKey = util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
14+
const salt = util.hexToUint8Array('000102030405060708090a0b0c');
15+
const info = util.hexToUint8Array('f0f1f2f3f4f5f6f7f8f9');
16+
const outLen = 42;
17+
18+
const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen);
19+
const expected = util.hexToUint8Array('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865');
20+
21+
expect(actual).to.deep.equal(expected);
22+
});
23+
24+
it('Test Case 2', async function() {
25+
const inputKey = util.hexToUint8Array('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f');
26+
const salt = util.hexToUint8Array('606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf');
27+
const info = util.hexToUint8Array('b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff');
28+
const outLen = 82;
29+
30+
const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen);
31+
const expected = util.hexToUint8Array('b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87');
32+
33+
expect(actual).to.deep.equal(expected);
34+
});
35+
36+
it('Test Case 3', async function() {
37+
const inputKey = util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b');
38+
const salt = new Uint8Array();
39+
const info = new Uint8Array();
40+
const outLen = 42;
41+
42+
const actual = await computeHKDF(enums.hash.sha256, inputKey, salt, info, outLen);
43+
const expected = util.hexToUint8Array('8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8');
44+
45+
expect(actual).to.deep.equal(expected);
46+
});
47+
});

test/crypto/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = () => describe('Crypto', function () {
66
require('./ecdh')();
77
require('./pkcs5')();
88
require('./aes_kw')();
9+
require('./hkdf')();
910
require('./gcm')();
1011
require('./eax')();
1112
require('./ocb')();

0 commit comments

Comments
 (0)