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.jsonis inspected - THEN
nameis@suaveplan/crypto-hash - AND no other workspace
package.jsonlists@suaveplan/hashas 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 bothsrc/primitives/<id>/impl/<id>.noble.tsandsrc/primitives/<id>/impl/<id>.native.ts - AND the
native.tsfile is either a working implementation or a stub that throwsPrimitiveNotImplemented('<id>')
Scenario: Selection driven by crypto.config.ts
- WHEN the root
crypto.config.tsmaps<id>to'noble'or'native' - THEN
src/primitives/<id>/<id>.select.tsis regenerated to re-export from the chosen impl - AND the build fails if any
*.select.tswas 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-hashruns - THEN each primitive's vector suite runs against its
nobleandnativeimpls - AND native impls that throw
PrimitiveNotImplementedare reported aspending, not failing
Scenario: Cross-impl equivalence
- WHEN a primitive has both a working
nobleandnativeimpl - 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:
- Full KAT suite passes on the native impl.
- Nightly differential fuzz (≥ 1M inputs over 7 days) has zero mismatches.
- Constant-time lint passes and
dudectstatistical test shows no measurable secret-dependent timing. - For
hmacand any primitive in the dangerous tier (defined incrypto-curveandcrypto-cipherspecs), 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.tsto change acrypto-hashentry 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
SuaveplanErrorwith codeCRYPTO_HASH.SCRYPT_INVALID_Nandcontext.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
SuaveplanErrorwith codeCRYPTO_HASH.BLAKE2B_OUTPUT_TOO_LONGandcontext.requestedLength === 65
Scenario: key longer than 64 bytes rejected
- WHEN
blake2b(msg, { dkLen: 32, key: 65-byte-buffer })is called - THEN it throws
SuaveplanErrorwith codeCRYPTO_HASH.BLAKE2B_KEY_TOO_LONGandcontext.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")producesstoredandverifyPasswordBlake2b(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
SuaveplanErrorwith codeCRYPTO_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"