Files
2026-05-27 23:29:53 +01:00

8.8 KiB

crypto-hash

Purpose

Hashing substrate for the Suaveplan monorepo: SHA-2 digests, HMAC, HKDF, PBKDF2, Argon2id, scrypt, BLAKE2b, password hashing (Argon2id PHC + BLAKE2b MCF), and hex/Base64 encoding helpers. Per-primitive selector layout enables replacing the noble-backed default with a native engine without changing any caller imports.

Requirements

Requirement: Package identity

@suaveplan/hash SHALL be renamed to @suaveplan/crypto-hash to share the crypto-* prefix with the crypto-curve, crypto-cipher, and crypto-utils sibling packages. Every in-tree import of @suaveplan/hash SHALL be updated in the same change.

Scenario: Package name updated

  • WHEN packages/universal/crypto-hash/package.json is inspected
  • THEN name is @suaveplan/crypto-hash
  • AND no other workspace package.json lists @suaveplan/hash as a dependency
  • AND no source file in the repo contains the string "@suaveplan/hash" as an import specifier

Requirement: Per-primitive replaceability layout

Every hash, MAC, and KDF primitive in @suaveplan/crypto-hash SHALL live under src/primitives/<id>/ with both a vendor-forked noble implementation and a native implementation slot, with the active implementation selected per-primitive at build time via the repo-root crypto.config.ts.

Scenario: Every primitive has both impl files

  • WHEN the package is inspected
  • THEN every primitive id — sha256, sha384, sha512, hmac, hkdf, pbkdf2, argon2id — has both src/primitives/<id>/impl/<id>.noble.ts and src/primitives/<id>/impl/<id>.native.ts
  • AND the native.ts file is either a working implementation or a stub that throws PrimitiveNotImplemented('<id>')

Scenario: Selection driven by crypto.config.ts

  • WHEN the root crypto.config.ts maps <id> to 'noble' or 'native'
  • THEN src/primitives/<id>/<id>.select.ts is regenerated to re-export from the chosen impl
  • AND the build fails if any *.select.ts was hand-edited (generated banner check)

Requirement: Shared test vectors and differential equivalence

Every primitive SHALL have an <id>.vectors.ts file containing RFC / NIST / Wycheproof known-answer vectors, executed against every present implementation with cross-impl equivalence asserted.

Scenario: KATs pass on every impl

  • WHEN bunx turbo test --filter=@suaveplan/crypto-hash runs
  • THEN each primitive's vector suite runs against its noble and native impls
  • AND native impls that throw PrimitiveNotImplemented are reported as pending, not failing

Scenario: Cross-impl equivalence

  • WHEN a primitive has both a working noble and native impl
  • THEN the test suite executes a differential-equivalence check on the shared vectors
  • AND any byte-level divergence fails the test

Requirement: Public API stability

The public API surface of @suaveplan/crypto-hash SHALL be identical to the surface that @suaveplan/hash exposed at the point of rename. All subpath exports (./hash, ./mac, ./kdf, ./encoding, ./errors) remain available with identical signatures.

Scenario: Subpath exports preserved

  • WHEN a consumer imports from any subpath previously exposed by @suaveplan/hash
  • THEN the exported functions have identical signatures and behaviour as before the rename

Requirement: CI gate for flipping default to native

A primitive's default SHALL NOT be changed from 'noble' to 'native' in crypto.config.ts unless the flip PR demonstrates:

  1. Full KAT suite passes on the native impl.
  2. Nightly differential fuzz (≥ 1M inputs over 7 days) has zero mismatches.
  3. Constant-time lint passes and dudect statistical test shows no measurable secret-dependent timing.
  4. For hmac and any primitive in the dangerous tier (defined in crypto-curve and crypto-cipher specs), a third-party audit sign-off is attached to the PR, referencing the exact commit SHA of the native impl.

Scenario: Flip PR blocked without evidence

  • WHEN a PR modifies crypto.config.ts to change a crypto-hash entry from 'noble' to 'native'
  • THEN CI requires the flip PR template with links to the KAT run, 7-day fuzz history, timing report, and (for HMAC) audit document
  • AND the merge is blocked if any evidence link is missing or red

Requirement: scrypt KDF primitive

The package SHALL expose scrypt(password, salt, options) from primitives/scrypt/scrypt.ts, where options carries N (CPU/memory cost, must be a power of two ≥ 2), r (block size), p (parallelisation), and dkLen (output length in bytes). The implementation SHALL match RFC 7914 byte-for-byte and SHALL use the @noble/hashes/scrypt::scryptAsync engine in the noble selector. Output is a Uint8Array of length dkLen.

Scenario: matches RFC 7914 known-answer test

  • WHEN scrypt("password", "NaCl", { N: 1024, r: 8, p: 16, dkLen: 64 }) is called
  • THEN the output equals the RFC 7914 §11 KAT for those parameters byte-for-byte

Scenario: invalid N rejected

  • WHEN scrypt(pw, salt, { N: 1000, r: 8, p: 1, dkLen: 32 }) is called (1000 is not a power of two)
  • THEN it throws SuaveplanError with code CRYPTO_HASH.SCRYPT_INVALID_N and context.N === 1000

Verified by: packages/crypto/crypto-hash/src/primitives/scrypt/scrypt.test.ts::"matches RFC 7914 known-answer test for N=1024 r=8 p=16" AND packages/crypto/crypto-hash/src/primitives/scrypt/scrypt.spec-conformance.test.ts::"throws SCRYPT_INVALID_N when N is not a power of two"


Requirement: blake2b hash primitive

The package SHALL expose blake2b(message, options) from primitives/blake2b/blake2b.ts, where options carries dkLen (1..64), optional key (≤ 64 bytes), optional salt (16 bytes), and optional personalization (16 bytes). The implementation SHALL match RFC 7693 byte-for-byte.

Scenario: matches RFC 7693 known-answer test

  • WHEN blake2b("abc", { dkLen: 64 }) is called
  • THEN the output equals the RFC 7693 §B Appendix KAT for the input "abc" byte-for-byte

Scenario: dkLen > 64 rejected

  • WHEN blake2b(msg, { dkLen: 65 }) is called
  • THEN it throws SuaveplanError with code CRYPTO_HASH.BLAKE2B_OUTPUT_TOO_LONG and context.requestedLength === 65

Scenario: key longer than 64 bytes rejected

  • WHEN blake2b(msg, { dkLen: 32, key: 65-byte-buffer }) is called
  • THEN it throws SuaveplanError with code CRYPTO_HASH.BLAKE2B_KEY_TOO_LONG and context.keyLength === 65

Verified by: packages/crypto/crypto-hash/src/primitives/blake2b/blake2b.test.ts::"matches RFC 7693 known-answer test for input \"abc\"" AND packages/crypto/crypto-hash/src/primitives/blake2b/blake2b.spec-conformance.test.ts::"throws BLAKE2B_OUTPUT_TOO_LONG when dkLen exceeds 64" AND packages/crypto/crypto-hash/src/primitives/blake2b/blake2b.spec-conformance.test.ts::"throws BLAKE2B_KEY_TOO_LONG when key exceeds 64 bytes"


Requirement: blake2b password hashing and verification

The package SHALL expose hashPasswordBlake2b(password, options) returning a stored hash in the format $blake2b$v=1$cf=<costFactor>$<saltHex>$<hashHex>, where costFactor controls iteration count (default 14, doubling each level) and a 16-byte random salt is generated when not supplied. It SHALL also expose verifyPasswordBlake2b(storedHash, password) returning true when the password verifies and false otherwise. Comparison SHALL be constant-time over the hash bytes.

Scenario: hash and verify round-trip

  • WHEN hashPasswordBlake2b("correct horse battery staple") produces stored and verifyPasswordBlake2b(stored, "correct horse battery staple") is called
  • THEN the result is true

Scenario: wrong password verifies as false

  • WHEN verifyPasswordBlake2b(stored, "wrong password") is called
  • THEN the result is false (no exception thrown)

Scenario: salt is fresh per call

  • WHEN hashPasswordBlake2b("same password") is called twice
  • THEN the two stored hashes are not byte-equal (different salts → different outputs)

Scenario: malformed stored hash rejected

  • WHEN verifyPasswordBlake2b("not-a-valid-format", pw) is called
  • THEN it throws SuaveplanError with code CRYPTO_HASH.BLAKE2B_PASSWORD_FORMAT_INVALID

Verified by: packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.test.ts::"hash and verify round-trip with same password" AND packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.test.ts::"verify returns false for wrong password" AND packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.test.ts::"salt is fresh per call so identical inputs hash differently" AND packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.spec-conformance.test.ts::"throws BLAKE2B_PASSWORD_FORMAT_INVALID for malformed stored hash"