Пример
Hash and verify passwords on Node without a native module, choosing between node:crypto's scrypt and bcryptjs
sha256:bf227476fbe2d665af4c03c6862eecaa6a1760441669b86d3def01bcc85a4d35
CROSS_PASS
L4_CROSS_PASS
MIT-0
Кейс
- Цель
- Hash and verify passwords on Node without a native module, choosing between node:crypto's scrypt and bcryptjs HOW
- Пакеты
- bcryptjs 3.0.3
- Окружение
- node 22
- Создан
- 2026-08-14T13:20:18Z
Контракт
- assert bcryptjs 3 declares an exports map holding only ".", so require("bcryptjs/package.json") throws ERR_PACKAGE_PATH_NOT_EXPORTED and the installed version has to be read through import.meta.resolve instead
- assert scrypt is deterministic for one salt and unrelated across salts, so the per-password salt has to be stored: the 64-byte derived key does not contain it and there is no field to parse it back out of
- assert a string salt is taken as its UTF-8 bytes rather than decoded as hex, so storing the salt as hex and handing the string back derives a different key than handing back the bytes it encodes
- assert the hand-rolled scrypt record carries the scheme, N, r, p, the salt and the key, so a verifier can rebuild the key from the stored string alone, and that a password one character short of the right one fails
- assert two accounts choosing the same password store different scrypt records, while sha256 of that password is byte-identical every time and has nowhere to put a salt
- assert timingSafeEqual throws RangeError ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH on buffers of different byte length instead of returning false, which is how a 32-byte legacy row met by a 64-byte scrypt key becomes a 500 rather than a failed login
- assert checking the lengths first returns false for that mismatch while leaving the equal-length comparison unchanged, and that timingSafeEqual refuses strings with ERR_INVALID_ARG_TYPE so hex-comparing with === is not a shortcut past it
- assert a bcryptjs hash is 60 characters beginning $2b$10$ that carry its own cost and salt, that getRounds and getSalt read them back, and that rehashing with the embedded salt reproduces the stored string exactly
- assert two hashes of one password differ yet both verify with no salt argument, and that published jBCrypt $2a$06$ vectors written by another implementation still verify and still report their cost
- assert an empty or malformed stored hash compares false while a null one throws Illegal arguments: string, object, so a NULL password column crashes the login path instead of failing it
- assert bcrypt silently truncates at 72 bytes, so two different longer passwords sharing that prefix each verify against the other's hash, the bare 72-byte prefix opens the account, and 71 bytes does not
- assert the limit is bytes and not characters: 36 accented characters are 72 bytes and survive, 37 are 74 bytes and lose the tail, and truncates() reports both
- assert two passwords under the limit differing only in their tail do not cross-verify, so the collision above is truncation rather than a broken comparison, and that scrypt has no such limit
- assert scryptSync's defaults are N=16384, r=8 and p=1 by deriving an identical key with them written out
- assert the default cost is deliberately slow, as a lower bound on the fastest of several runs rather than an exact figure: at least 5 ms and more than fifty times sha256, and lowering N to 1024 lowers it
- assert raising N without raising maxmem throws RangeError ERR_CRYPTO_INVALID_SCRYPT_PARAMS quoting OpenSSL's memory limit, that a single doubling of the default cost is already over the 32 MiB default maxmem, and that a malformed N produces the byte-identical message so the text tells the two faults apart from neither side
- measure that the accepted maxmem minimum is exactly 128 * r * (N + p + 2), pinned to the byte at six parameter sets that vary N, r and p independently, and that the widely quoted 128 * N * r is itself rejected: 2 KB short at the defaults, and 3 KB short at N=32768 where it comes to exactly the 32 MiB default
- assert N=1 and N=3 are rejected while a zero N, r, p or maxmem is not rejected but silently means the default
Файлы
- csx.json
- package-lock.json
- package.json
- src/passwords.mjs
- test/contract.mjs
Исходный сидер
Квитанции проверки
- node 22 · CONTAINER_RUN · compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS · node-typescript@1 · 2026-08-14 · ed25519:a2ec939a4c60e243
- · CONTAINER_RUN · compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS · node-typescript@1 · 2026-08-14 · ed25519:7da3f610facce8bf
- · CONTAINER_RUN · compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS · node-typescript@1 · 2026-08-14 · ed25519:7da3f610facce8bf
- · CONTAINER_RUN · compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS · node-typescript@1 · 2026-08-14 · ed25519:7da3f610facce8bf
- · CONTAINER_RUN · compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS · node-typescript@1 · 2026-08-14 · ed25519:7da3f610facce8bf