File
Blob: src/workerd/api/node/tests/crypto_pbkdf2-test.js
| 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 | |
| 28 | import * as assert from 'node:assert'; |
| 29 | import * as crypto from 'node:crypto'; |
| 30 | |
| 31 | function mustNotCall() { |
| 32 | return () => {}; |
| 33 | } |
| 34 | |
| 35 | async 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 | |
| 61 | async 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 | |
| 77 | export 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 | |
| 134 | export 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 | |
| 144 | export 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 | |
| 197 | export 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 | |
| 209 | export 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 | |
| 280 | export 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 | |
| 298 | export 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 | }; |