Skip to content

Commit 30f053b

Browse files
committed
Implement node crypto HKDF, except for KeyObject inputs
1 parent 7967ac1 commit 30f053b

File tree

11 files changed

+588
-17
lines changed

11 files changed

+588
-17
lines changed

src/node/crypto.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ import {
5050
Hmac,
5151
} from 'node-internal:crypto_hash';
5252

53+
import {
54+
hkdf,
55+
hkdfSync,
56+
} from 'node-internal:crypto_hkdf';
57+
5358
import {
5459
pbkdf2,
5560
pbkdf2Sync,
@@ -97,6 +102,9 @@ export {
97102
Hash,
98103
HashOptions,
99104
Hmac,
105+
// Hkdf
106+
hkdf,
107+
hkdfSync,
100108
// Pbkdf2
101109
pbkdf2,
102110
pbkdf2Sync,
@@ -195,6 +203,9 @@ export default {
195203
createHash,
196204
createHmac,
197205
getHashes,
206+
// Hkdf
207+
hkdf,
208+
hkdfSync,
198209
// Pbkdf2
199210
pbkdf2,
200211
pbkdf2Sync,
@@ -286,8 +297,8 @@ export default {
286297
// * [x] crypto.randomInt([min, ]max[, callback])
287298
// * [x] crypto.randomUUID([options])
288299
// * Key Derivation
289-
// * [ ] crypto.hkdf(digest, ikm, salt, info, keylen, callback)
290-
// * [ ] crypto.hkdfSync(digest, ikm, salt, info, keylen)
300+
// * [.] crypto.hkdf(digest, ikm, salt, info, keylen, callback) (* still needs KeyObject support)
301+
// * [.] crypto.hkdfSync(digest, ikm, salt, info, keylen) (* still needs KeyObject support)
291302
// * [x] crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)
292303
// * [x] crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)
293304
// * [ ] crypto.scrypt(password, salt, keylen[, options], callback)

src/node/internal/crypto.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ export class HashHandle {
1919
public copy(xofLen: number): HashHandle;
2020
}
2121

22+
export type ArrayLike = ArrayBuffer|string|Buffer|ArrayBufferView;
23+
2224
export class HmacHandle {
2325
public constructor(algorithm: string, key: ArrayLike | CryptoKey);
2426
public update(data: Buffer | ArrayBufferView): number;
2527
public digest(): ArrayBuffer;
2628
}
2729

30+
// hkdf
31+
export function getHkdf(hash: string, key: ArrayLike, salt: ArrayLike, info: ArrayLike,
32+
length: number): ArrayBuffer;
33+
2834
// pbkdf2
29-
export type ArrayLike = ArrayBuffer|string|Buffer|ArrayBufferView;
3035
export function getPbkdf(password: ArrayLike, salt: ArrayLike, iterations: number, keylen: number,
3136
digest: string): ArrayBuffer;
3237

src/node/internal/crypto_hkdf.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) 2017-2023 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
//
5+
// Copyright Joyent, Inc. and other Node contributors.
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a
8+
// copy of this software and associated documentation files (the
9+
// "Software"), to deal in the Software without restriction, including
10+
// without limitation the rights to use, copy, modify, merge, publish,
11+
// distribute, sublicense, and/or sell copies of the Software, and to permit
12+
// persons to whom the Software is furnished to do so, subject to the
13+
// following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included
16+
// in all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
25+
26+
/* todo: the following is adopted code, enabling linting one day */
27+
/* eslint-disable */
28+
29+
'use strict';
30+
31+
import { default as cryptoImpl } from 'node-internal:crypto';
32+
33+
import {
34+
validateFunction,
35+
validateInteger,
36+
validateString,
37+
} from 'node-internal:validators';
38+
39+
import {
40+
KeyObject,
41+
} from 'node-internal:crypto_keys';
42+
43+
type ArrayLike = cryptoImpl.ArrayLike;
44+
45+
import {
46+
kMaxLength,
47+
} from 'node-internal:internal_buffer';
48+
49+
import {
50+
toBuf,
51+
validateByteSource,
52+
} from 'node-internal:crypto_util';
53+
54+
import {
55+
isAnyArrayBuffer,
56+
isArrayBufferView,
57+
} from 'node-internal:internal_types';
58+
59+
import {
60+
NodeError,
61+
ERR_INVALID_ARG_TYPE,
62+
ERR_OUT_OF_RANGE,
63+
} from 'node-internal:internal_errors';
64+
65+
function validateParameters(hash: string, key: ArrayLike | KeyObject, salt: ArrayLike,
66+
info: ArrayLike, length: number) {
67+
// TODO(soon): Add support for KeyObject input.
68+
if (key instanceof KeyObject) {
69+
throw new NodeError("ERR_METHOD_NOT_IMPLEMENTED", "KeyObject support for hkdf() and " +
70+
"hkdfSync() is not yet implemented. Use ArrayBuffer, TypedArray, " +
71+
"DataView, or Buffer instead.");
72+
}
73+
74+
validateString(hash, 'digest');
75+
key = prepareKey(key as unknown as ArrayLike);
76+
salt = validateByteSource(salt, 'salt');
77+
info = validateByteSource(info, 'info');
78+
79+
validateInteger(length, 'length', 0, kMaxLength);
80+
81+
if (info.byteLength > 1024) {
82+
throw new ERR_OUT_OF_RANGE(
83+
'info',
84+
'must not contain more than 1024 bytes',
85+
info.byteLength);
86+
}
87+
88+
return {
89+
hash,
90+
key,
91+
salt,
92+
info,
93+
length,
94+
};
95+
}
96+
97+
function prepareKey(key: ArrayLike): ArrayLike {
98+
key = toBuf(key);
99+
100+
if (!isAnyArrayBuffer(key) && !isArrayBufferView(key)) {
101+
throw new ERR_INVALID_ARG_TYPE(
102+
'ikm',
103+
[
104+
'string',
105+
'SecretKeyObject',
106+
'ArrayBuffer',
107+
'TypedArray',
108+
'DataView',
109+
'Buffer',
110+
],
111+
key);
112+
}
113+
114+
return key;
115+
}
116+
117+
export function hkdf(hash: string, key: ArrayLike | KeyObject, salt: ArrayLike, info: ArrayLike,
118+
length: number,
119+
callback: (err: Error|null, derivedKey?: ArrayBuffer) => void): void {
120+
({
121+
hash,
122+
key,
123+
salt,
124+
info,
125+
length,
126+
} = validateParameters(hash, key, salt, info, length));
127+
128+
validateFunction(callback, 'callback');
129+
130+
new Promise<ArrayBuffer>((res, rej) => {
131+
try {
132+
res(cryptoImpl.getHkdf(hash, key as ArrayLike, salt, info, length));
133+
} catch(err) {
134+
rej(err);
135+
}
136+
}).then((val: ArrayBuffer) => callback(null, val), (err) => callback(err));
137+
}
138+
139+
export function hkdfSync(hash: string, key: ArrayLike | KeyObject, salt: ArrayLike,
140+
info: ArrayLike, length: number): ArrayBuffer {
141+
({
142+
hash,
143+
key,
144+
salt,
145+
info,
146+
length,
147+
} = validateParameters(hash, key, salt, info, length));
148+
149+
return cryptoImpl.getHkdf(hash, key, salt, info, length);
150+
}

src/node/internal/crypto_pbkdf2.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
// Copyright (c) 2017-2022 Cloudflare, Inc.
1+
// Copyright (c) 2017-2023 Cloudflare, Inc.
22
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
33
// https://opensource.org/licenses/Apache-2.0
44
//
5-
// Adapted from Deno and Node.js:
6-
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
7-
//
8-
// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
5+
// Copyright Joyent, Inc. and other Node contributors.
96
//
107
// Permission is hereby granted, free of charge, to any person obtaining a
118
// copy of this software and associated documentation files (the

src/node/internal/crypto_util.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
// Copyright (c) 2017-2022 Cloudflare, Inc.
1+
// Copyright (c) 2017-2023 Cloudflare, Inc.
22
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
33
// https://opensource.org/licenses/Apache-2.0
44
//
5-
// Adapted from Deno and Node.js:
6-
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
7-
//
8-
// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
5+
// Copyright Joyent, Inc. and other Node contributors.
96
//
107
// Permission is hereby granted, free of charge, to any person obtaining a
118
// copy of this software and associated documentation files (the
@@ -48,6 +45,9 @@ import {
4845
validateString,
4946
} from 'node-internal:validators';
5047

48+
import { default as cryptoImpl } from 'node-internal:crypto';
49+
type ArrayLike = cryptoImpl.ArrayLike;
50+
5151
export const kHandle = Symbol('kHandle');
5252
export const kFinalized = Symbol('kFinalized');
5353
export const kState = Symbol('kFinalized');
@@ -63,8 +63,9 @@ export function getArrayBufferOrView(buffer: Buffer | ArrayBuffer | ArrayBufferV
6363
if (isAnyArrayBuffer(buffer))
6464
return buffer as ArrayBuffer;
6565
if (typeof buffer === 'string') {
66-
if (encoding === undefined || encoding === 'buffer')
66+
if (encoding === undefined || encoding === 'buffer') {
6767
encoding = 'utf8';
68+
}
6869
return Buffer.from(buffer, encoding);
6970
}
7071
if (!isArrayBufferView(buffer)) {
@@ -112,11 +113,32 @@ export function arrayBufferToUnsignedBigInt(buf: ArrayBuffer): bigint {
112113
// This is here because many functions accepted binary strings without
113114
// any explicit encoding in older versions of node, and we don't want
114115
// to break them unnecessarily.
115-
export function toBuf(val: string | ArrayBuffer | Buffer | ArrayBufferView, encoding: string): string | ArrayBuffer | Buffer | ArrayBufferView {
116+
export function toBuf(val: ArrayLike, encoding?: string): Buffer|ArrayBuffer|ArrayBufferView {
116117
if (typeof val === 'string') {
117-
if (encoding === 'buffer')
118+
if (encoding === 'buffer') {
118119
encoding = 'utf8';
120+
}
119121
return Buffer.from(val, encoding);
120122
}
121123
return val;
122-
}
124+
}
125+
126+
export function validateByteSource(val: ArrayLike,
127+
name: string): Buffer|ArrayBuffer|ArrayBufferView {
128+
val = toBuf(val);
129+
130+
if (isAnyArrayBuffer(val) || isArrayBufferView(val)) {
131+
return val;
132+
}
133+
134+
throw new ERR_INVALID_ARG_TYPE(
135+
name,
136+
[
137+
'string',
138+
'ArrayBuffer',
139+
'TypedArray',
140+
'DataView',
141+
'Buffer',
142+
],
143+
val);
144+
}

src/workerd/api/node/crypto.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ class CryptoImpl final: public jsg::Object {
105105
kj::Own<HMAC_CTX> hmac_ctx;
106106
};
107107

108+
// Hkdf
109+
kj::Array<kj::byte> getHkdf(kj::String hash, kj::Array<kj::byte> key, kj::Array<kj::byte> salt,
110+
kj::Array<kj::byte> info, uint32_t length);
111+
108112
// Pbkdf2
109113
kj::Array<kj::byte> getPbkdf(kj::Array<kj::byte> password, kj::Array<kj::byte> salt,
110114
uint32_t num_iterations, uint32_t keylen, kj::String name);
@@ -187,6 +191,8 @@ class CryptoImpl final: public jsg::Object {
187191
// Hash and Hmac
188192
JSG_NESTED_TYPE(HashHandle);
189193
JSG_NESTED_TYPE(HmacHandle);
194+
// Hkdf
195+
JSG_METHOD(getHkdf);
190196
// Pbkdf2
191197
JSG_METHOD(getPbkdf);
192198
// Keys

src/workerd/api/node/crypto_hash-test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
// Copyright (c) 2017-2023 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
//
5+
// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a
8+
// copy of this software and associated documentation files (the
9+
// "Software"), to deal in the Software without restriction, including
10+
// without limitation the rights to use, copy, modify, merge, publish,
11+
// distribute, sublicense, and/or sell copies of the Software, and to permit
12+
// persons to whom the Software is furnished to do so, subject to the
13+
// following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included
16+
// in all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
25+
126
'use strict';
227

328
import {

0 commit comments

Comments
 (0)