CodeSampleX

示例

bcryptjs 3.0.3: Hash and verify passwords on Node without a native module, choosing between node:crypto's scrypt and bcryptjs

已验证示例 — npm bcryptjs 3.0.3: Hash and verify passwords on Node without a native module, choosing between node:crypto's scrypt and bcryptjs. contract 在 node 22…

sha256:bf227476fbe2d665af4c03c6862eecaa6a1760441669b86d3def01bcc85a4d35

本网络只提供一件事:能构建的样本。它在沙箱中运行并保留签名回执。它不评级、不担保——同样的代码能否在你的环境构建,它没有测量过。 提交了通过的契约回执的不同签名密钥数量。为 1 表示只有作者;大于 1 表示还有其他人构建过。密钥是自行生成的,背后没有注册身份,因此计的是密钥而非人。 MIT-0

执行证据

声明的环境与签名的运行分开呈现,你可以看到这个样本究竟运行了什么、在哪里运行。

证据依据
签名契约通过
验证回执
3
构建过它的签名密钥
3
声明的环境 node 22 linux x64 node 22 javascript npm

验证运行环境

环境 契约 阶段 运行日期
node 22 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-14
node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-18
node 22 · linux alpine/x64 · docker ed25519:c1973797be207ac4 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1node:22-alpine@sha256:c610fcdfb1d5…
2026-09-07

案例

HOW
目标
Hash and verify passwords on Node without a native module, choosing between node:crypto's scrypt and bcryptjs
包
符号
  • crypto.scryptSync
  • crypto.timingSafeEqual
  • crypto.randomBytes
  • bcrypt.hashSync
  • bcrypt.compareSync
  • bcrypt.getSalt
  • bcrypt.truncates
环境
node 22
创建时间
2026-08-14T13:20:18Z

契约

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. assert scryptSync's defaults are N=16384, r=8 and p=1 by deriving an identical key with them written out
  15. 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
  16. 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
  17. 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
  18. 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

下载源代码构件 (tar.gz)

源代码

csx.json
{"case":{"caseId":"case:sha256:d55698685500cd8e9be7d3e7baf277005c55a6d62bea0d62e15222436a823ea1","constraints":{"moduleSystem":"esm","runtime":"node"},"contract":["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"],"goal":"Hash and verify passwords on Node without a native module, choosing between node:crypto's scrypt and bcryptjs","kind":"HOW","packages":["pkg:npm/bcryptjs@3.0.3"],"schemaVersion":1,"symbols":["crypto.scryptSync","crypto.timingSafeEqual","crypto.randomBytes","bcrypt.hashSync","bcrypt.compareSync","bcrypt.getSalt","bcrypt.truncates"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"javascript","moduleSystem":"esm","os":"linux","packageManager":"npm","runtime":"node","runtimeVersion":"22","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/bcryptjs@3.0.3"],"schemaVersion":1,"symbols":["crypto.scryptSync","crypto.timingSafeEqual","crypto.randomBytes","bcrypt.hashSync","bcrypt.compareSync","bcrypt.getSalt","bcrypt.truncates"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "csx-password-hash-scrypt-bcryptjs",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "csx-password-hash-scrypt-bcryptjs",
      "version": "1.0.0",
      "license": "MIT-0",
      "dependencies": {
        "bcryptjs": "3.0.3"
      }
    },
    "node_modules/bcryptjs": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
      "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
      "license": "BSD-3-Clause",
      "bin": {
        "bcrypt": "bin/bcrypt"
      }
    }
  }
}
package.json
{
  "name": "csx-password-hash-scrypt-bcryptjs",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "license": "MIT-0",
  "dependencies": {
    "bcryptjs": "3.0.3"
  }
}
src/passwords.mjs
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import * as bcrypt from "bcryptjs";

/**
 * Two ways to store a password on Node without a native module: node:crypto's
 * built-in scrypt, and bcryptjs, which is bcrypt reimplemented in plain
 * JavaScript.
 *
 * The reason usually given for reaching for bcryptjs is that the native
 * `bcrypt` package needs node-gyp and a toolchain and therefore cannot be
 * installed on an Alpine image. That is out of date, and believing it is how
 * you end up choosing a password hash for a reason that stopped being true:
 * `npm i bcrypt` in node:22-alpine installs bcrypt 6.0.0 in under a second
 * with no compiler present, and the result hashes and verifies. bcrypt 6
 * ships prebuildify N-API binaries tagged by libc — prebuilds/linux-x64
 * carries both bcrypt.glibc.node and bcrypt.musl.node — and node-gyp-build
 * picks one at require time, so it survives --ignore-scripts too. Measured in
 * this image; the contract below does not assert it, because pulling bcrypt in
 * to prove a negative is not worth making it a dependency of the sample.
 *
 * What is still true is narrower, and it is a property of the artifact rather
 * than of one release: bcryptjs has zero dependencies and ships no binary, so
 * there is no libc, architecture or Node-ABI matrix to match and nothing to
 * re-prebuild for a platform outside the seven bcrypt 6 covers. scrypt ships
 * nothing at all, being already in node:crypto.
 *
 * The two differ in one way that decides most of the code you write around
 * them: bcrypt writes the salt and the cost into the hash string, and scrypt
 * does not. A scrypt derived key is 64 opaque bytes and nothing else, so the
 * salt is yours to generate, yours to store, and yours to hand back at
 * verification time. Lose it and every account is locked out.
 */

export const SCRYPT_SALT_BYTES = 16;
export const SCRYPT_KEYLEN = 64;

/**
 * scrypt's parameters, spelled out rather than left to the default, because
 * they have to be written into the stored record anyway: a row hashed at one
 * cost must still verify after you raise the cost for new signups. These are
 * the values Node uses when you pass no options at all, which the contract
 * confirms by deriving the same key both ways.
 */
export const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };

/**
 * The storage format is invented here, and having to invent it is the point.
 * bcrypt's `$2b$10$...` is this same idea standardised twenty years ago; with
 * scrypt you are writing the parser yourself, so the cost parameters go in
 * beside the salt or you can never change them.
 *
 * The salt is stored as hex and decoded back to bytes before use. That
 * round-trip is not cosmetic: scryptSync takes a string salt as its UTF-8
 * bytes, so handing it the 32-character hex string derives a different key
 * than handing it the 16 bytes the string encodes. Mix the two up across
 * signup and login and every password is wrong, with nothing in the logs to
 * say why.
 */
export function scryptHash(password, salt = randomBytes(SCRYPT_SALT_BYTES)) {
  const { N, r, p } = SCRYPT_PARAMS;
  const key = scryptSync(password, salt, SCRYPT_KEYLEN, { N, r, p });
  return `scrypt$${N}$${r}$${p}$${salt.toString("hex")}$${key.toString("hex")}`;
}

export function scryptVerify(password, stored) {
  const [scheme, N, r, p, saltHex, keyHex] = stored.split("$");
  if (scheme !== "scrypt") return false;
  const key = scryptSync(password, Buffer.from(saltHex, "hex"), keyHex.length / 2, {
    N: Number(N),
    r: Number(r),
    p: Number(p),
  });
  return constantTimeEqual(key, Buffer.from(keyHex, "hex"));
}

/**
 * timingSafeEqual is the whole reason to reach for node:crypto here, and it
 * has one edge that turns a failed login into a 500: given two buffers of
 * different byte lengths it throws a RangeError instead of returning false.
 * It cannot do otherwise — the guarantee it makes is that the comparison takes
 * the same time whatever the contents are, and it has no way to compare
 * different lengths without the duration leaking which is which.
 *
 * So the length check goes first and returns false. Length is not a secret:
 * a stored hash's length is fixed by the algorithm, so revealing that a row
 * is 32 bytes rather than 64 reveals which algorithm wrote it, not anything
 * about the password. That case is exactly how this fires in production —
 * rows left over from an older sha256 scheme meeting freshly derived
 * 64-byte scrypt keys.
 */
export function constantTimeEqual(a, b) {
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

/** The same comparison without the guard, so the contract can measure the throw. */
export function unguardedEqual(a, b) {
  return timingSafeEqual(a, b);
}

/**
 * What not to do, kept here to be measured rather than asserted about. A bare
 * digest is unsalted and fast: unsalted means two accounts with the same
 * password store the same string, so one cracked row cracks all of them and a
 * precomputed table answers the rest; fast means a GPU walks the whole
 * candidate space. Both failures are properties of the primitive, so no amount
 * of care in the calling code fixes them.
 */
export function sha256Hex(password) {
  return createHash("sha256").update(password, "utf8").digest("hex");
}

export function bcryptHash(password, cost = 10) {
  return bcrypt.hashSync(password, cost);
}

/**
 * No salt argument, and there is nowhere to put one: compareSync reads the
 * version, the cost and the salt back out of the stored string, re-runs the
 * hash with them and compares. One column in the database, and a cost upgrade
 * is a matter of rehashing on next successful login while old rows keep
 * verifying at their old cost.
 */
export function bcryptVerify(password, stored) {
  return bcrypt.compareSync(password, stored);
}

/**
 * Minimum of several runs. For a lower bound on work this is the statistic to
 * use: scheduling noise and a cold cache can only make a run slower, never
 * faster, so the fastest run is the closest thing to the real cost, and a
 * loaded machine cannot turn the assertion into a flake.
 */
export function fastestMillis(fn, runs = 5) {
  let best = Infinity;
  for (let i = 0; i < runs; i++) {
    const started = process.hrtime.bigint();
    fn();
    const elapsed = Number(process.hrtime.bigint() - started) / 1e6;
    if (elapsed < best) best = elapsed;
  }
  return best;
}
test/contract.mjs
import assert from "node:assert/strict";
import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import * as bcrypt from "bcryptjs";

import {
  bcryptHash,
  bcryptVerify,
  constantTimeEqual,
  fastestMillis,
  scryptHash,
  scryptVerify,
  sha256Hex,
  SCRYPT_KEYLEN,
  SCRYPT_PARAMS,
  unguardedEqual,
} from "../src/passwords.mjs";

// bcryptjs 3 declares an exports map with only "." in it, so the subpath the
// rest of the ecosystem reaches for when it wants a version at runtime is not
// reachable at all. Resolve the ESM entry point and read the manifest sitting
// next to it instead.
assert.throws(() => createRequire(import.meta.url)("bcryptjs/package.json"), {
  code: "ERR_PACKAGE_PATH_NOT_EXPORTED",
});
const manifest = JSON.parse(
  readFileSync(new URL("./package.json", import.meta.resolve("bcryptjs")), "utf8"),
);
assert.equal(manifest.version, "3.0.3");

const PASSWORD = "correct horse battery staple";

// ---------------------------------------------------------------------------
// scrypt: the salt is yours to store
// ---------------------------------------------------------------------------

const salt = randomBytes(16);
const key = scryptSync(PASSWORD, salt, SCRYPT_KEYLEN);

// Same password, same salt, same bytes — that is what makes verification
// possible at all.
assert.ok(key.equals(scryptSync(PASSWORD, salt, SCRYPT_KEYLEN)));

// Same password, a different salt, and nothing about the two results relates.
// This is the property that defeats a precomputed table, and it is also the
// reason the salt cannot be thrown away: without the exact bytes used at
// signup there is no way back to this key.
const otherSalt = randomBytes(16);
assert.equal(otherSalt.equals(salt), false);
assert.equal(key.equals(scryptSync(PASSWORD, otherSalt, SCRYPT_KEYLEN)), false);

// And the key does not carry the salt anywhere inside it. scrypt returns
// derived bytes, not a record — there is no field to parse it out of, which is
// the whole difference from bcrypt further down.
assert.equal(key.includes(salt), false);
assert.equal(key.length, SCRYPT_KEYLEN);

// A string salt is taken as its UTF-8 bytes, not decoded as hex. Storing the
// salt as hex and passing the string straight back derives a different key
// than passing the bytes it encodes, so a signup path and a login path that
// disagree about this reject every correct password.
const saltHex = salt.toString("hex");
assert.ok(scryptSync(PASSWORD, saltHex, 32).equals(scryptSync(PASSWORD, Buffer.from(saltHex, "utf8"), 32)));
assert.equal(scryptSync(PASSWORD, saltHex, 32).equals(scryptSync(PASSWORD, Buffer.from(saltHex, "hex"), 32)), false);

// Round-tripped through the storage format, which has to carry the salt and
// the cost because nothing else will.
const stored = scryptHash(PASSWORD);
assert.match(stored, /^scrypt\$16384\$8\$1\$[0-9a-f]{32}\$[0-9a-f]{128}$/);
assert.equal(scryptVerify(PASSWORD, stored), true);
assert.equal(scryptVerify("correct horse battery stapl", stored), false);

// Two accounts that chose the same password store different records, because
// each gets its own salt. Cracking one says nothing about the other.
assert.notEqual(scryptHash(PASSWORD), scryptHash(PASSWORD));

// Which is precisely what a bare digest cannot do. sha256 has nowhere to put a
// salt, so the same password is the same string in every row: the table itself
// tells an attacker which accounts share a password, and one guess tested
// against one row is a guess tested against all of them.
assert.equal(sha256Hex(PASSWORD), sha256Hex(PASSWORD));
assert.equal(sha256Hex(PASSWORD).length, 64);
assert.equal(createHash("sha256").update(PASSWORD).digest("hex"), sha256Hex(PASSWORD));

// ---------------------------------------------------------------------------
// timingSafeEqual throws on a length mismatch — it does not return false
// ---------------------------------------------------------------------------

const wrongLength = createHash("sha256").update(PASSWORD).digest(); // 32 bytes
assert.equal(wrongLength.length, 32);
assert.notEqual(wrongLength.length, key.length);

assert.throws(
  () => unguardedEqual(key, wrongLength),
  (err) => {
    assert.ok(err instanceof RangeError);
    assert.equal(err.code, "ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH");
    assert.equal(err.message, "Input buffers must have the same byte length");
    return true;
  },
);

// That is the shape of a real outage: a row still holding a 32-byte digest
// from an older scheme, compared against a fresh 64-byte scrypt key, throws
// out of the login handler as a 500 instead of answering "wrong password".
// Checking the lengths first turns it back into an answer. Length is not
// secret — it is fixed by the algorithm, not by the password.
assert.equal(constantTimeEqual(key, wrongLength), false);

// The guard changes nothing about the equal-length case, which is the one that
// actually needs to be constant time.
assert.equal(constantTimeEqual(key, Buffer.from(key)), true);
const flipped = Buffer.from(key);
flipped[0] ^= 0x01;
assert.equal(constantTimeEqual(key, flipped), false);
assert.equal(timingSafeEqual(key, flipped), false);

// Strings are rejected outright, so hex-comparing with === is not a shortcut
// past this — it is a different, timing-leaky comparison.
assert.throws(() => timingSafeEqual(saltHex, saltHex), { code: "ERR_INVALID_ARG_TYPE" });

// ---------------------------------------------------------------------------
// bcryptjs carries its own salt and cost
// ---------------------------------------------------------------------------

const hash = bcryptHash(PASSWORD, 10);
assert.equal(hash.length, 60);
assert.ok(hash.startsWith("$2b$10$"));
assert.equal(bcrypt.getRounds(hash), 10);
assert.equal(bcrypt.getSalt(hash), hash.slice(0, 29));

// Every call produces a different string, because genSalt runs per call — and
// both verify against the same password with no salt passed in, because the
// salt each one used is the first 29 characters of itself.
const hashAgain = bcryptHash(PASSWORD, 10);
assert.notEqual(hash, hashAgain);
assert.equal(bcryptVerify(PASSWORD, hash), true);
assert.equal(bcryptVerify(PASSWORD, hashAgain), true);
assert.equal(bcryptVerify("wrong", hash), false);

// This is literally how compare works: re-hash with the salt read back out of
// the stored string and you reproduce the stored string exactly.
assert.equal(bcrypt.hashSync(PASSWORD, bcrypt.getSalt(hash)), hash);

// Published jBCrypt vectors, written by a different implementation years ago
// at cost 6. They verify here, so replacing the native bcrypt addon with this
// pure-JavaScript package does not invalidate a single stored row — which is
// the only reason the swap is safe to make on a live table.
const VECTORS = [
  ["", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s."],
  ["a", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe"],
  ["abc", "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i"],
  ["abcdefghijklmnopqrstuvwxyz", "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC"],
  ["~!@#$%^&*()      ~!@#$%^&*()PNBFRD", "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO"],
];
for (const [plain, vector] of VECTORS) {
  assert.equal(bcryptVerify(plain, vector), true, `vector for ${JSON.stringify(plain)}`);
  assert.equal(bcrypt.hashSync(plain, bcrypt.getSalt(vector)), vector);
  // The cost is readable without verifying anything, so a login can notice a
  // row is below current policy and rehash it while it holds the plaintext.
  assert.equal(bcrypt.getRounds(vector), 6);
}

// A garbage or empty stored hash answers false. A null one does not: bcryptjs
// type-checks its arguments and throws, so a user row whose password column is
// NULL crashes the login path rather than failing it.
assert.equal(bcryptVerify(PASSWORD, ""), false);
assert.equal(bcryptVerify(PASSWORD, "$2b$10$short"), false);
assert.throws(() => bcryptVerify(PASSWORD, null), { message: "Illegal arguments: string, object" });

// ---------------------------------------------------------------------------
// bcrypt silently truncates at 72 bytes
// ---------------------------------------------------------------------------

// The highest-consequence trap here, and it is silent in both directions:
// nothing warns at hash time and nothing warns at verify time. Two different
// passwords sharing their first 72 bytes are the same password to bcrypt, so
// each one logs in as the other.
const prefix = "A".repeat(72);
const longA = `${prefix}-account-recovery-passphrase`;
const longB = `${prefix}-something-else-entirely!!`;
assert.notEqual(longA, longB);

const longHash = bcryptHash(longA, 10);
assert.equal(bcryptVerify(longA, longHash), true);
assert.equal(bcryptVerify(longB, longHash), true); // <- different password, accepted

// The cut is at exactly 72 bytes: the bare 72-byte prefix opens the account,
// 71 bytes does not.
assert.equal(bcryptVerify(prefix, longHash), true);
assert.equal(bcryptVerify("A".repeat(71), longHash), false);

// Bytes, not characters. 36 accented characters are 72 bytes of UTF-8 and
// survive; one more character is 74 bytes and loses the tail. A length check
// written against String.length passes both.
const fits = "é".repeat(36);
const cut = "é".repeat(37);
assert.equal(fits.length, 36);
assert.equal(Buffer.byteLength(fits, "utf8"), 72);
assert.equal(cut.length, 37);
assert.equal(Buffer.byteLength(cut, "utf8"), 74);
assert.equal(bcrypt.truncates(fits), false);
assert.equal(bcrypt.truncates(cut), true);
assert.equal(bcrypt.truncates(longA), true);
assert.equal(bcrypt.truncates(prefix), false);

// truncates() is the check to run at signup, since nothing else will tell you.
// The control for all of the above: below the limit the tail is honoured, so
// two passwords sharing a 60-byte prefix and differing after it do not
// cross-verify. The collision further up is truncation, not a broken compare.
const shortA = `${"B".repeat(60)}-one`;
const shortB = `${"B".repeat(60)}-two`;
assert.equal(bcrypt.truncates(shortA), false);
assert.equal(bcrypt.truncates(shortB), false);
assert.equal(bcryptVerify(shortB, bcryptHash(shortA, 10)), false);

// scrypt has no such limit: the same two passwords derive different keys, so
// a long generated passphrase keeps all of its entropy.
const longStored = scryptHash(longA);
assert.equal(scryptVerify(longA, longStored), true);
assert.equal(scryptVerify(longB, longStored), false);
assert.equal(scryptVerify(prefix, longStored), false);

// ---------------------------------------------------------------------------
// scrypt's default N is slow on purpose
// ---------------------------------------------------------------------------

// The defaults, confirmed by deriving the same key with them written out.
// Worth pinning: the stored record has to name them, and a future Node that
// changed them would break verification of every existing row.
assert.ok(
  scryptSync(PASSWORD, salt, SCRYPT_KEYLEN).equals(
    scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, SCRYPT_PARAMS),
  ),
);
assert.deepEqual(SCRYPT_PARAMS, { N: 16384, r: 8, p: 1 });

// Measured as a lower bound, never an exact figure — the number depends on the
// machine, and only the floor is a fact about the algorithm. N=16384 with r=8
// means touching 16 MB of memory in a chain that cannot be shortcut, and the
// fastest of several runs still cannot get under a few milliseconds. That cost
// is the feature; a password hash that is fast is a password hash that is fast
// to attack.
const scryptMs = fastestMillis(() => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN));
const sha256Ms = fastestMillis(() => createHash("sha256").update(PASSWORD).digest());
assert.ok(scryptMs >= 5, `scrypt at default cost took ${scryptMs.toFixed(2)}ms`);
assert.ok(
  scryptMs > sha256Ms * 50,
  `scrypt ${scryptMs.toFixed(2)}ms vs sha256 ${sha256Ms.toFixed(4)}ms`,
);

// Lowering N lowers the cost, which is how you would confirm the time above is
// the work factor rather than call overhead. Only the direction is asserted:
// the ratio is a fact about this machine, not about scrypt.
const cheapMs = fastestMillis(() => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N: 1024 }));
assert.ok(cheapMs < scryptMs, `N=1024 ${cheapMs.toFixed(2)}ms vs N=16384 ${scryptMs.toFixed(2)}ms`);

// Raising N is where it bites back. maxmem defaults to 32 MiB and scrypt's
// memory grows with N, so Node refuses — with a RangeError quoting OpenSSL
// about a memory limit, an error that names neither N nor maxmem. Raise maxmem
// alongside N, or a cost increase fails only in production, wherever the config
// is set higher than the test suite's.
assert.throws(
  () => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N: 2 ** 17 }),
  (err) => {
    assert.ok(err instanceof RangeError);
    assert.equal(err.code, "ERR_CRYPTO_INVALID_SCRYPT_PARAMS");
    assert.match(err.message, /memory limit exceeded/);
    return true;
  },
);

// And it does not take a large jump to get there. A single doubling of the
// default cost is already over the default maxmem, so the very next setting
// anyone would reach for is the one that fails.
assert.throws(
  () => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N: 2 ** 15 }),
  { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" },
);

// The message is not diagnostic either. An N that is not a power of two is a
// different fault entirely and produces the byte-identical string, so reading
// "memory limit exceeded" as a memory problem sends you off to raise maxmem
// when the cost itself is malformed.
const failureMessage = (options) => {
  try {
    scryptSync(PASSWORD, salt, 32, options);
    return null;
  } catch (err) {
    return err.message;
  }
};
assert.match(failureMessage({ N: 3 }), /memory limit exceeded/);
assert.equal(failureMessage({ N: 3 }), failureMessage({ N: 2 ** 17 }));

// How much to raise it to is not the 128 * N * r that gets quoted for scrypt:
// budget exactly that and it is still rejected, as the last assertion here
// shows. Bisecting maxmem puts the accepted minimum at exactly
// 128 * r * (N + p + 2) — OpenSSL also charges for the p input blocks and two
// blocks of scratch. The quoted figure is 2 KB short at the defaults, and being
// short by any amount produces the same opaque error.
const requiredBytes = (cost, blockSize, parallelism) =>
  128 * blockSize * (cost + parallelism + 2);
const { N, r, p } = SCRYPT_PARAMS;
assert.equal(requiredBytes(N, r, p), 16780288);
assert.equal(
  scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N, r, p, maxmem: requiredBytes(N, r, p) }).length,
  SCRYPT_KEYLEN,
);
assert.throws(
  () => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N, r, p, maxmem: requiredBytes(N, r, p) - 1 }),
  { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" },
);
assert.throws(
  () => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N, r, p, maxmem: 128 * N * r }),
  { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" },
);

// One parameter set cannot tell "+ p + 2" apart from a flat "+ 3", and two
// sets that both hold r at 8 and p at 1 cannot either. These vary each term on
// its own — p alone, r alone, then all three at once — and the boundary lands
// on the formula every time, which is what makes it a rule rather than a
// coincidence fitted to the defaults.
for (const [cost, blockSize, parallelism, expected] of [
  [2 ** 17, 8, 1, 134220800],
  [16384, 8, 5, 16784384],
  [16384, 2, 1, 4195072],
  [1024, 4, 3, 526848],
  [32768, 8, 1, 33557504],
]) {
  const options = { N: cost, r: blockSize, p: parallelism };
  const need = requiredBytes(cost, blockSize, parallelism);
  assert.equal(need, expected, `N=${cost} r=${blockSize} p=${parallelism}`);
  assert.equal(
    scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { ...options, maxmem: need }).length,
    SCRYPT_KEYLEN,
  );
  assert.throws(
    () => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { ...options, maxmem: need - 1 }),
    { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" },
  );
}

// N=32768 is where the quoted formula is at its most convincing and its most
// wrong. 128 * N * r comes to 33554432 there, which is exactly the 32 MiB
// default maxmem — so the folklore says the next cost up fits, with nothing to
// spare and nothing to configure. It is 3072 bytes short, which is why the
// single doubling asserted above is rejected.
assert.equal(128 * 32768 * 8, 33554432);
assert.equal(requiredBytes(32768, 8, 1) - 128 * 32768 * 8, 3072);

// That the default really is 32 MiB and not merely somewhere in between:
// spelling it out changes neither side of the boundary.
assert.equal(
  scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N, r, p, maxmem: 33554432 }).length,
  SCRYPT_KEYLEN,
);
assert.throws(
  () => scryptSync(PASSWORD, salt, SCRYPT_KEYLEN, { N: 2 ** 15, r: 8, p: 1, maxmem: 33554432 }),
  { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" },
);

// N=1 and N=3 are rejected for not being a power of two greater than one, but
// zero is not rejected — it means "use the default", and not only for N: r, p
// and maxmem each behave the same way. Number("") is 0, so a cost that arrives
// from an empty config value does not fail loudly, it silently is not the cost
// you configured, and the only symptom is that logins got faster.
assert.throws(() => scryptSync(PASSWORD, salt, 32, { N: 1 }), { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" });
assert.throws(() => scryptSync(PASSWORD, salt, 32, { N: 3 }), { code: "ERR_CRYPTO_INVALID_SCRYPT_PARAMS" });
assert.equal(Number(""), 0);
const atDefaults = scryptSync(PASSWORD, salt, 32);
for (const option of ["N", "r", "p", "maxmem"]) {
  assert.ok(scryptSync(PASSWORD, salt, 32, { [option]: 0 }).equals(atDefaults), `${option}: 0`);
}

console.log("contract ok");

原始种子者

anonymous