UNPKG

yescrypt-wasm

Version:
303 lines (287 loc) 11.8 kB
import { describe, before, it } from 'node:test'; import { strict as assert } from 'assert'; import { Yescrypt, YESCRYPT_WORM, YESCRYPT_DEFAULTS } from '../src/index.js'; import { bytesToHex } from '../src/utils.js'; type TestCases = { passwd: string; salt: string; N?: number; r?: number; p?: number; t?: number; g?: number; flags?: number; useScrypt?: boolean; output: string; }[]; const cases: TestCases = [ // Reference tests { passwd: '', salt: '', N: 16, r: 1, useScrypt: true, output: '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906', }, { passwd: 'password', salt: 'NaCl', N: 1024, r: 8, p: 16, useScrypt: true, output: 'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640', }, { passwd: 'pleaseletmein', salt: 'SodiumChloride', N: 16384, r: 8, useScrypt: true, output: '7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887', }, /** * Not supported due to OOM for WASM { passwd: 'pleaseletmein', salt: 'SodiumChloride', N: 1048576, r: 8, useScrypt: true, output: '2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4', }, **/ { passwd: '', salt: '', N: 4, r: 1, useScrypt: true, output: 'efad0c23314cb572bc3cfb1543da42f8a8b073004c866b64ab5055a4f09fa5f571142ebfe7e05a3b92c432f31dea95ad5f9c854b6456462f4bd0f732b7cdc549', }, { passwd: '', salt: '', N: 4, r: 1, output: '0cd5af76eb241df8119a9a122ae36920bcc7f414b9c0d58f45008060dade46b0c80922bdcc16a3ab5d201d4c6140c671be1f75272ca904739d5ad1ff672b0c21', }, { passwd: '', salt: '', N: 4, r: 1, p: 1, t: 1, output: '23b6adf0b60c9a997f58583d80cda48c638cdc2f289edf93a70807725a0d35c468ca362c5557cc04b6811e2e730841f526d8f4f7acfbfa9e06fe1f383a71155e', }, { passwd: 'p', salt: 's', N: 16, r: 8, p: 1, t: 10, output: 'e1f981733a94052fcd7acb1405df0bbde8e499b6a1331b775909b48c2f516c40dcc8301635b7237b8aa9f170addbc21b9896b6b33eae6af780023d1e973e9a3a', }, { passwd: 'p', salt: 's', N: 16, r: 8, output: 'c8c7ff1122b0b291c3f2608948782cd689cc45579017aaa5ff8baa74a632ec99c3d66930fb2023bbc17112f5dac992c01dc3a3ead4c3ffb1316736cfa0bfb9e8', }, // Generated from case { passwd: '875ce1f7465d1feef5d629f754c39360df06741aa0accc9cc3457bcbf41083cd', salt: 'salt', output: '9039d6b87c5fd06fff6efd38f07c597c73eec699f30c7caa1a50e0c0b38746299d5feb4e36fbf9da4ce5b4305151865e91960977782a4e2032930440974040ef', }, { passwd: '970974a6975d6841d4986695868aec7e993babfb12c2ce1ba33f04108561e13a', salt: 'salt', output: '92458b692c470922d92463df5d25700a7dbce5dd8f98486e050baf025ca822b67d967b1655cd1565eb6319384c6d988fd536e0dcb2237152ebfd26867edb8aa4', }, { passwd: 'd9302fababfbaf79bb4e0df24452f7896a4618a5c64ecbe453e243f9952bee73', salt: 'salt', output: 'bca00274a51442932a74c72d7b72a1b9a5a032653f92a71924de085e85a6b3db7c127753238a5a62afa49a3eb6a1964f5bdf20ba723d0a88469943cfba4faa02', }, { passwd: 'b9ddad54f42e2ccd63a853bcb383c8997fd625dae3d3e0d3e6935313023f22fc', salt: 'salt', output: '98a5606bc3bd92265c857407cea5aa35266bd480e059bfa26e4f94eb2177afc9bab1f1cea6ead3fade08408cbcc09ea355daed93c3481b79f726819eef60552c', }, { passwd: '4a96b204357218152d29a0573ef98aba97cabf85daa6a81798c9de441cbd7488', salt: 'salt', output: 'ca12d55b07093524c6e9d2e1e073637ce316c815ee125a8327219f17171a3cabadec6395a18f43d461983351ff0539f4344f6d3b6636ccaedbc283b6ce7c1e0e', }, { passwd: '6dc29548d66f2e79811a209135dc50d0872f2d113f6de6fe22fe4e2c7ca1f91c', salt: 'a5b02d1987e5fdf1cd9f06fdd74c0aee8e43e8305ab367ca150531e8902c1837', output: '5fcfffd773e9ea4b625d6e006d91d498aa09dd2401cfc5a4c4740ae81cf77cf413882518eaca0e20e60e96b3fe68d41b66cd5e8fe29df418f62d0a87b7d2277e', }, { passwd: '2a85312ac74274b5b0277a98603be55298a8b92e6815f855ed75776b0f202e92', salt: '52024d87fad7c2547704a2fbd485bc9f2931515a1448ca38728e93fad362acf1', output: 'ea38f98a2acd1e606f812df5f84ab5d4be2762ef5f04bec3cae350a99a94b55fc4018cedb72f46792c809d37534d6b1d0427625c3105e4a29874864f60980eb8', }, { passwd: '656fdd0a5ac19a13c5de59ffdf1064069fd2f092936ab69ac878f8c3768f5029', salt: '45bf68f1910629ad40a8afc04b89d89ffa445dc3f1c4d2f0e17bb6f98596b13a', output: '0f2828cc4a307161cb523dfdc0f380ebed8a7140a138ba5fbe32c5dd456830bbee63087716eea18829f5ad9d5df943252ba2badd4b2463e2ee6f31b53affece1', }, { passwd: '33d66a2028548f3c8af191401198f60e35b62427756180beeacfacf93b28dc10', salt: '2bb69eb32abc5b0aa2730def5455e1559f4fff989e4c279466568741eccfe07b', output: '759f7995f9808a25c1d65adc8e35d948d34d024b4b3ee2756336837c500ce6807996b73ad13119a0c32235f4651b37e54c051da832b183ee58442c8083109b6f', }, { passwd: 'bb752fe351e5649c87facb83d62507bdaf5263529a468a5050001696675a7ae3', salt: 'dd26ba145e22844642fa5de4d007750378ffaeff9390e62c343819e62900c90f', output: 'b4c7af4e8a5ec6934b2a229282506762ec65ef00a460ef4abd2429758c6dcc051f37ff57594ca1a891f9073289fe30e7d5c76a7343dac68edffd3f76c46a0f66', }, // --- Reference vectors from yescrypt-c/TESTS-OK (yescrypt_kdf with explicit flags) --- // yescrypt("", "", flags=YESCRYPT_DEFAULTS, N=4, r=1, p=1, t=0) -> 64 bytes { passwd: '', salt: '', N: 4, r: 1, p: 1, t: 0, flags: YESCRYPT_DEFAULTS, output: '0cd5af76eb241df8119a9a122ae36920bcc7f414b9c0d58f45008060dade46b0c80922bdcc16a3ab5d201d4c6140c671be1f75272ca904739d5ad1ff672b0c21', }, // yescrypt("", "", flags=YESCRYPT_DEFAULTS, N=4, r=1, p=1, t=1) { passwd: '', salt: '', N: 4, r: 1, p: 1, t: 1, flags: YESCRYPT_DEFAULTS, output: '23b6adf0b60c9a997f58583d80cda48c638cdc2f289edf93a70807725a0d35c468ca362c5557cc04b6811e2e730841f526d8f4f7acfbfa9e06fe1f383a71155e', }, // yescrypt("", "", flags=YESCRYPT_WORM, N=4, r=1, p=1, t=0) { passwd: '', salt: '', N: 4, r: 1, p: 1, t: 0, flags: YESCRYPT_WORM, output: '85dda48c9ec9de2f7f1ae8b4dfeda51f8b6d56f3081be1a7c0833ba2719a36ab02885dae36557d342686b17ba75f2c217792de0970ab1d07a9c750936d31426f', }, // yescrypt("", "", flags=YESCRYPT_WORM, N=4, r=1, p=1, t=1) { passwd: '', salt: '', N: 4, r: 1, p: 1, t: 1, flags: YESCRYPT_WORM, output: '4baa8cd8608ba91f3e3439d9ec4fae8f9fc092d9ca22b7377e31ae5b9ad7877c1168691162dd0e5ef049e570650cbed4384ad60534fb0cbed19ff3f033c94b0c', }, // yescrypt("p", "s", flags=YESCRYPT_DEFAULTS, N=16, r=8, p=1, t=10) -> 40 bytes { passwd: 'p', salt: 's', N: 16, r: 8, p: 1, t: 10, flags: YESCRYPT_DEFAULTS, output: 'e1f981733a94052fcd7acb1405df0bbde8e499b6a1331b775909b48c2f516c40dcc8301635b7237b', }, ]; describe('Yescrypt (WASM)', () => { let yescrypt: Yescrypt; before(async () => { yescrypt = await Yescrypt.init(); }); it('Test Cases', () => { for (const { passwd, salt, N, r, p, t, g, flags, useScrypt, output } of cases) { const dklen = output.length / 2; const hashed = useScrypt ? yescrypt.scrypt_kdf(Buffer.from(passwd), Buffer.from(salt), N, r, p, t, flags, g, dklen) : yescrypt.yescrypt_kdf(Buffer.from(passwd), Buffer.from(salt), N, r, p, t, flags, g, dklen); assert.strictEqual(output, bytesToHex(hashed, false)); } }); // Regression: passwords >= 20 bytes used to mismatch the C reference because // the encoded-hash path computed the password length with strlen() over a // non-NUL-terminated heap buffer. Here we verify that the encoded hash's // digest matches the (verified) raw-KDF digest over the decoded salt, for a // range of password lengths including the previously-broken 20-byte case. it('encoded hash honors full password length (>=20 bytes)', () => { // 16-byte salt, fixed so the test is deterministic. const salt = Buffer.from('0123456789abcdeffedcba9876543210', 'hex'); for (const len of [19, 20, 32, 64, 100]) { const passwd = Buffer.from('a'.repeat(len)); const encoded = yescrypt.yescrypt_hash(passwd, salt); assert.ok(encoded.startsWith('$y$'), `expected $y$ hash, got: ${encoded}`); // Parse "$y$<params>$<saltB64>$<hashB64>" const parts = encoded.split('$'); // ['', 'y', '<params>', '<saltB64>', '<hashB64>'] assert.strictEqual(parts.length, 5, `unexpected hash format: ${encoded}`); const hashB64 = parts[4]; // Recompute the raw 32-byte digest via the verified KDF path, using the // same salt bytes and YESCRYPT_DEFAULTS params (N=4096, r=32, p=1). const raw = yescrypt.yescrypt_kdf(passwd, salt, 4096, 32, 1, 0, YESCRYPT_DEFAULTS, 0, 32); const decoded = decodeYescrypt64(hashB64); assert.strictEqual( bytesToHex(decoded, false), bytesToHex(raw, false), `digest mismatch for ${len}-byte password`, ); } }); it('throws RangeError on invalid parameters', () => { const passwd = Buffer.from('p'); const salt = Buffer.from('s'); // N must be a power of two greater than 1. assert.throws(() => yescrypt.yescrypt_kdf(passwd, salt, 1000), /power of two/); // r * p must be < 2^30. assert.throws(() => yescrypt.yescrypt_kdf(passwd, salt, 16, 1 << 20, 1 << 10), /2\^30/); }); it('handles larger N without crashing (memory growth enabled)', () => { // With ALLOW_MEMORY_GROWTH this no longer aborts the module the way the // old fixed 32 MiB heap did. ~256 MiB for N=131072, r=8. const passwd = Buffer.from('pleaseletmein'); const salt = Buffer.from('SodiumChloride'); const out = yescrypt.scrypt_kdf(passwd, salt, 131072, 8, 1); assert.strictEqual(out.length, 64); }); }); // yescrypt's custom base64 alphabet (itoa64) and little-endian bit packing, // mirroring decode64() in yescrypt-c/yescrypt-common.c. Used only by the test // to cross-check the encoded-hash digest against the raw KDF output. function decodeYescrypt64(str: string): Uint8Array { const itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const out: number[] = []; let i = 0; while (i < str.length) { let value = 0; let bits = 0; while (i < str.length && bits < 24) { const c = itoa64.indexOf(str[i]); if (c < 0) { break; } i++; value |= c << bits; bits += 6; } for (let b = 0; b + 8 <= bits; b += 8) { out.push(value & 0xff); value >>>= 8; } } return Uint8Array.from(out); }