Skip to content
File

Blob: src/workerd/api/node/tests/crypto_pbkdf2-test.js

javascript330 lines
1// Copyright (c) 2017-2022 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
26'use strict';
27 
28import * as assert from 'node:assert';
29import * as crypto from 'node:crypto';
30 
31function mustNotCall() {
32 return () => {};
33}
34 
35async function runPBKDF2(password, salt, iterations, keylen, hash) {
36 const syncResult = crypto.pbkdf2Sync(
37 password,
38 salt,
39 iterations,
40 keylen,
41 hash
42 );
43 
44 const p = Promise.withResolvers();
45 crypto.pbkdf2(
46 password,
47 salt,
48 iterations,
49 keylen,
50 hash,
51 (err, asyncResult) => {
52 assert.deepStrictEqual(asyncResult, syncResult);
53 p.resolve();
54 }
55 );
56 await p.promise;
57 
58 return syncResult;
59}
60 
61async function testPBKDF2(
62 password,
63 salt,
64 iterations,
65 keylen,
66 expected,
67 encoding
68) {
69 const actual = await runPBKDF2(password, salt, iterations, keylen, 'sha256');
70 assert.strictEqual(actual.toString(encoding || 'latin1'), expected);
71}
72 
73//
74// Test PBKDF2 with RFC 6070 test vectors (except #4)
75//
76 
77export const pbkdf2_correctness_tests = {
78 async test(ctrl, env, ctx) {
79 await testPBKDF2(
80 'password',
81 'salt',
82 1,
83 20,
84 '\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52' +
85 '\x56\xc4\xf8\x37\xa8\x65\x48\xc9'
86 );
87 
88 await testPBKDF2(
89 'password',
90 'salt',
91 2,
92 20,
93 '\xae\x4d\x0c\x95\xaf\x6b\x46\xd3\x2d\x0a\xdf\xf9' +
94 '\x28\xf0\x6d\xd0\x2a\x30\x3f\x8e'
95 );
96 
97 await testPBKDF2(
98 'password',
99 'salt',
100 4096,
101 20,
102 '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' +
103 '\x84\x5c\x4c\x8d\x96\x28\x93\xa0'
104 );
105 
106 await testPBKDF2(
107 'passwordPASSWORDpassword',
108 'saltSALTsaltSALTsaltSALTsaltSALTsalt',
109 4096,
110 25,
111 '\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11' +
112 '\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c'
113 );
114 
115 await testPBKDF2(
116 'pass\0word',
117 'sa\0lt',
118 4096,
119 16,
120 '\x89\xb6\x9d\x05\x16\xf8\x29\x89\x3c\x69\x62\x26\x65' + '\x0a\x86\x87'
121 );
122 
123 await testPBKDF2(
124 'password',
125 'salt',
126 32,
127 32,
128 '64c486c55d30d4c5a079b8823b7d7cb37ff0556f537da8410233bcec330ed956',
129 'hex'
130 );
131 },
132};
133 
134export const pbkdf2_no_callback_test = {
135 test(ctrl, env, ctx) {
136 // Error path should not leak memory (check with Valgrind).
137 assert.throws(() => crypto.pbkdf2('password', 'salt', 1, 20, 'sha1'), {
138 code: 'ERR_INVALID_ARG_TYPE',
139 name: 'TypeError',
140 });
141 },
142};
143 
144export const pbkdf2_out_of_range_tests = {
145 test(ctrl, env, ctx) {
146 for (const iterations of [-1, 0, 2147483648]) {
147 assert.throws(
148 () => crypto.pbkdf2Sync('password', 'salt', iterations, 20, 'sha1'),
149 {
150 code: 'ERR_OUT_OF_RANGE',
151 name: 'RangeError',
152 }
153 );
154 }
155 
156 ['str', null, undefined, [], {}].forEach((notNumber) => {
157 assert.throws(
158 () => {
159 crypto.pbkdf2Sync('password', 'salt', 1, notNumber, 'sha256');
160 },
161 {
162 code: 'ERR_INVALID_ARG_TYPE',
163 name: 'TypeError',
164 }
165 );
166 });
167 
168 [Infinity, -Infinity, NaN].forEach((input) => {
169 assert.throws(
170 () => {
171 crypto.pbkdf2('password', 'salt', 1, input, 'sha256', mustNotCall());
172 },
173 {
174 code: 'ERR_OUT_OF_RANGE',
175 name: 'RangeError',
176 message:
177 'The value of "keylen" is out of range. It ' +
178 `must be an integer. Received ${input}`,
179 }
180 );
181 });
182 
183 [-1, 2147483648, 4294967296].forEach((input) => {
184 assert.throws(
185 () => {
186 crypto.pbkdf2('password', 'salt', 1, input, 'sha256', mustNotCall());
187 },
188 {
189 code: 'ERR_OUT_OF_RANGE',
190 name: 'RangeError',
191 }
192 );
193 });
194 },
195};
196 
197export const empty_pwd_test = {
198 async test(ctrl, env, ctx) {
199 // Should not get FATAL ERROR with empty password and salt
200 // https://github.com/nodejs/node/issues/8571
201 const p = Promise.withResolvers();
202 crypto.pbkdf2('', '', 1, 32, 'sha256', (err, prime) => {
203 p.resolve();
204 });
205 await p.promise;
206 },
207};
208 
209export const invalid_arg_tests = {
210 test(ctrl, env, ctx) {
211 assert.throws(
212 () => crypto.pbkdf2('password', 'salt', 8, 8, mustNotCall()),
213 {
214 code: 'ERR_INVALID_ARG_TYPE',
215 name: 'TypeError',
216 message:
217 'The "digest" argument must be of type string. ' +
218 'Received undefined',
219 }
220 );
221 
222 assert.throws(() => crypto.pbkdf2Sync('password', 'salt', 8, 8), {
223 code: 'ERR_INVALID_ARG_TYPE',
224 name: 'TypeError',
225 message:
226 'The "digest" argument must be of type string. ' + 'Received undefined',
227 });
228 
229 assert.throws(() => crypto.pbkdf2Sync('password', 'salt', 8, 8, null), {
230 code: 'ERR_INVALID_ARG_TYPE',
231 name: 'TypeError',
232 message:
233 'The "digest" argument must be of type string. ' + 'Received null',
234 });
235 [1, {}, [], true, undefined, null].forEach((input) => {
236 assert.throws(
237 () => crypto.pbkdf2(input, 'salt', 8, 8, 'sha256', mustNotCall()),
238 {
239 code: 'ERR_INVALID_ARG_TYPE',
240 name: 'TypeError',
241 }
242 );
243 
244 assert.throws(
245 () => crypto.pbkdf2('pass', input, 8, 8, 'sha256', mustNotCall()),
246 {
247 code: 'ERR_INVALID_ARG_TYPE',
248 name: 'TypeError',
249 }
250 );
251 
252 assert.throws(() => crypto.pbkdf2Sync(input, 'salt', 8, 8, 'sha256'), {
253 code: 'ERR_INVALID_ARG_TYPE',
254 name: 'TypeError',
255 });
256 
257 assert.throws(() => crypto.pbkdf2Sync('pass', input, 8, 8, 'sha256'), {
258 code: 'ERR_INVALID_ARG_TYPE',
259 name: 'TypeError',
260 });
261 });
262 
263 ['test', {}, [], true, undefined, null].forEach((i) => {
264 assert.throws(
265 () => crypto.pbkdf2('pass', 'salt', i, 8, 'sha256', mustNotCall()),
266 {
267 code: 'ERR_INVALID_ARG_TYPE',
268 name: 'TypeError',
269 }
270 );
271 
272 assert.throws(() => crypto.pbkdf2Sync('pass', 'salt', i, 8, 'sha256'), {
273 code: 'ERR_INVALID_ARG_TYPE',
274 name: 'TypeError',
275 });
276 });
277 },
278};
279 
280export const TypedArray_tests = {
281 async test(ctrl, env, ctx) {
282 // Any TypedArray should work for password and salt.
283 for (const SomeArray of [
284 Uint8Array,
285 Uint16Array,
286 Uint32Array,
287 Float16Array,
288 Float32Array,
289 Float64Array,
290 ArrayBuffer,
291 ]) {
292 await runPBKDF2(new SomeArray(10), 'salt', 8, 8, 'sha256');
293 await runPBKDF2('pass', new SomeArray(10), 8, 8, 'sha256');
294 }
295 },
296};
297 
298export const invalid_digest_tests = {
299 async test(ctrl, env, ctx) {
300 {
301 const p = Promise.withResolvers();
302 crypto.pbkdf2('pass', 'salt', 8, 8, 'md55', (err, prime) => {
303 if (err) return p.reject(err);
304 });
305 await assert.rejects(p.promise);
306 }
307 
308 assert.throws(() => crypto.pbkdf2Sync('pass', 'salt', 8, 8, 'md55'), {
309 name: 'TypeError',
310 });
311 
312 // TODO(soon): Enable this once crypto.getHashes() is available. Note that shake* is not
313 // supported by BoringSSL so there's no need to filter it out, but we may want to filter other
314 // functions.
315 // const kNotPBKDF2Supported = ['shake128', 'shake256'];
316 // crypto.getHashes()
317 // .filter((hash) => !kNotPBKDF2Supported.includes(hash))
318 // .forEach((hash) => {
319 // runPBKDF2(new Uint8Array(10), 'salt', 8, 8, hash);
320 // });
321 
322 {
323 // This should not crash.
324 assert.throws(() => crypto.pbkdf2Sync('1', '2', 1, 1, '%'), {
325 name: 'TypeError',
326 });
327 }
328 },
329};