cryptofence
v0.1.1
Published
hashing and key derivation in pure typescript, sync, zero deps, runs everywhere
Readme
cryptofence
sha-2, hmac, pbkdf2 and hkdf in pure typescript. synchronous, zero deps, runs anywhere.
npm i cryptofencewhy
when browser code touches node's crypto, bundlers reach for
crypto-browserify and pull in a tree that has quietly rotted:
| package | weekly | last published |
|---|---|---|
| crypto-browserify | 11.8M | oct 2024 |
| create-hash | 13.9M | april 2018 |
| create-ecdh | 11.3M | august 2020 |
| public-encrypt | 11.2M | october 2018 |
| diffie-hellman | 11.2M | april 2018 |
| browserify-cipher | — | april 2018 |
eleven million installs a week each, on packages that have not shipped in six to eight years. nobody is going to fix them.
this is the hashing and key derivation part, written once, with no dependencies at all.
use
import { sha256, hmac, pbkdf2Async, hkdf, toHex } from 'cryptofence'
toHex(sha256('hello')) // synchronous
toHex(hmac('sha256', key, message))
await pbkdf2Async('sha256', password, salt, 600000, 32)
hkdf('sha256', ikm, salt, info, 42)streaming works the way you expect:
import { createHash } from 'cryptofence'
const h = createHash('sha512')
for await (const chunk of stream) h.update(chunk)
h.digest()algorithms: sha224, sha256, sha384, sha512.
speed, honestly
pure javascript hashing is not native hashing. measured on an m-series mac against node's openssl:
sha256, 4MB ours 104ms node 2ms 38 MB/s
sha512, 4MB ours 123ms node 3ms
pbkdf2, 100k ours 819ms node 13msso:
- on node, for large data, use
node:crypto. it is right there and it is forty times quicker. this package is for where that is not an option. - for pbkdf2, use
pbkdf2Async. it hands the work to webcrypto, which is native on node, workers, deno, bun and browsers — the same 100k rounds drop from 819ms to 13ms. it falls back to the sync code when webcrypto is missing or the algorithm is sha224, and the two are asserted to agree.
for the sizes this is usually reaching for — a token, a session id, a signature over a few kilobytes — the difference does not show up.
it says no to a few things
pbkdf2 under 1000 iterations throws. a low count is the usual way this gets misused, and quietly deriving a weak key is worse than failing.
timingSafeEqual reads every byte. comparing secrets with === stops at
the first wrong byte, and the timing tells an attacker how much of a token
they have guessed. there is a test asserting a difference in the first byte
takes the same time as one in the last.
randomInt uses rejection sampling. taking a modulus of a random number
skews the low end of the range.
a hash cannot be reused after digest(). it throws rather than silently
returning a stale value.
correctness
38 tests, on two legs.
published vectors — fips 180-4 for sha-224/256/384/512, rfc 4231 for hmac,
rfc 5869 test case 1 for hkdf including the intermediate PRK.
agreement with node's own openssl — every algorithm, against every input,
must produce the identical digest to node:crypto. that covers hmac at key
lengths either side of the block size, pbkdf2, hkdf at seven output lengths,
a 1,000,000 byte input, streaming in chunks of ten different sizes so they
straddle block boundaries, and every message length around the 55/56 and
111/112 padding edges where the length field forces an extra block.
api
createHash(alg)→{ update, digest, blockSize, digestLength }hash(alg, data),sha224/sha256/sha384/sha512hmac(alg, key, data)pbkdf2(alg, password, salt, iterations, length)— syncpbkdf2Async(...)— same, via webcryptohkdf(alg, ikm, salt, info, length),hkdfExtract,hkdfExpandtimingSafeEqual(a, b)randomBytes(n),randomInt(min, max?),randomUUID()toHex(bytes),fromHex(string)
strings are treated as utf-8. everything else takes and returns Uint8Array.
runtime
the hashing is pure javascript and would run anywhere, but randomBytes,
randomInt and randomUUID need crypto.getRandomValues, so node 20 or
newer — node 18 went end of life in april 2025 and never exposed webcrypto
as a global. workers, deno and bun all have it.
what it is not
no md5 or sha-1 — if you need them for a legacy checksum, they are not here on purpose. no ciphers, no public key operations. this is the hashing half.
license
MIT
