sha3 - xero/leviathan-crypto GitHub Wiki
Covers the SHA-3 hash functions (SHA3-224 through SHA3-512) and the SHAKE extendable-output functions (SHAKE128, SHAKE256). See SHA-3 implementation audit for algorithm correctness verifications.
The SHA-3 family provides six hash functions standardized in FIPS 202: four fixed-output hash functions (SHA3-224, SHA3-256, SHA3-384, SHA3-512) and two extendable-output functions, or XOFs (SHAKE128, SHAKE256). All six are built on the Keccak sponge construction, a fundamentally different design from the Merkle-Damgard structure used by SHA-2.
SHA-3 is not a replacement for SHA-2. Both are considered secure, and both are standardized by NIST. SHA-3 exists to provide defense-in-depth: if a flaw is ever discovered in SHA-2, SHA-3 is completely unaffected because it uses a different mathematical foundation. You may never need that insurance, but if you do, you will be very glad it is there.
The SHAKE XOFs are particularly flexible. Unlike SHA3-256, which always produces exactly 32 bytes, SHAKE128 and SHAKE256 can produce variable-length output. You tell them how many bytes you want, making them useful for key derivation, generating nonces, or any situation where you need more (or fewer) bytes than a standard hash provides.
One key advantage of SHA-3 over SHA-2: SHA-3 is immune to length extension
attacks. With SHA-2, if you know SHA256(secret + message) but not the secret,
you can compute SHA256(secret + message + padding + extra) without knowing the
secret. SHA-3's sponge construction makes this impossible.
Important
Read these before using the API. Misusing hash functions is one of the most common sources of security vulnerabilities.
-
Length extension immunity. Unlike SHA-2, the SHA-3 sponge construction does not leak enough internal state for length extension attacks. Computing
SHA3(secret + message)does not let an attacker forgeSHA3(secret + message + extra). That said, HMAC is still the correct way to build a MAC — do not use rawSHA3(key + message)as a MAC construction, even though it is not vulnerable to length extension. HMAC provides a formally proven security reduction. -
SHAKE output is unbounded. SHAKE128 and SHAKE256 are full XOFs — output length is unbounded. Request any number of bytes via
hash(), or drive the sponge directly withabsorb()/squeeze(). The only constraint isoutputLength >= 1. -
Not for password hashing. SHA-3 is a fast hash, which is the opposite of what you want for password storage. Passwords must be hashed with a slow, memory-hardened algorithm like Argon2id. See argon2id.md for usage patterns including passphrase-based encryption with leviathan primitives.
-
Call
dispose()when finished. Every SHA-3 class wraps a WASM module that stores Keccak state in linear memory. Callingdispose()zeroes all internal state (the 200-byte lane matrix, input buffer, output buffer, and metadata). If you skipdispose(), key material or intermediate hash state may persist in memory.
Each module subpath exports its own init function for consumers who want tree-shakeable imports.
Initializes only the sha3 WASM binary. Equivalent to calling the
root init({ sha3: source }) but without pulling the other three
modules into the bundle.
Signature:
async function sha3Init(source: WasmSource): Promise<void>Usage:
import { sha3Init, SHA3_256 } from 'leviathan-crypto/sha3'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await sha3Init(sha3Wasm)
const sha3 = new SHA3_256()'keccak' is an alias for 'sha3'. Same WASM binary, same instance slot.
keccakInit() and sha3Init() are interchangeable.
import { keccakInit, SHAKE256, SHA3_256 } from 'leviathan-crypto/keccak'
import { keccakWasm } from 'leviathan-crypto/keccak/embedded'
await keccakInit(keccakWasm)
// isInitialized('sha3') === true — same slotUse the keccak subpath when the consuming context (such as ML-KEM) makes the
Keccak primitive name semantically clearer. See init.md
for full details.
All SHA-3 classes require initialization before use. Either the root init():
import { init } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })Or the subpath sha3Init():
import { sha3Init } from 'leviathan-crypto/sha3'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await sha3Init(sha3Wasm)If you use SHA-3 classes without calling init() first, the constructor
will throw an error.
Fixed-output hash function. Produces a 28-byte (224-bit) digest.
class SHA3_224 {
constructor()
hash(msg: Uint8Array): Uint8Array // returns 28 bytes
dispose(): void
}Fixed-output hash function. Produces a 32-byte (256-bit) digest. This is the most commonly used SHA-3 variant; 256-bit security is suitable for most applications.
class SHA3_256 {
constructor()
hash(msg: Uint8Array): Uint8Array // returns 32 bytes
dispose(): void
}Fixed-output hash function. Produces a 48-byte (384-bit) digest.
class SHA3_384 {
constructor()
hash(msg: Uint8Array): Uint8Array // returns 48 bytes
dispose(): void
}Fixed-output hash function. Produces a 64-byte (512-bit) digest. Use this when you need the highest security margin.
class SHA3_512 {
constructor()
hash(msg: Uint8Array): Uint8Array // returns 64 bytes
dispose(): void
}Extendable-output function (XOF). Produces variable-length output — any number of bytes you request. 128-bit security level.
Caution
SHAKE128 is stateful and holds exclusive access to the sha3 WASM module
for its entire lifetime. Constructing a second SHAKE128/SHAKE256 or any
other sha3 class (SHA3_256, etc.) while this instance is live throws.
Call dispose() when done. Pool workers are unaffected.
class SHAKE128 {
constructor()
hash(msg: Uint8Array, outputLength: number): Uint8Array
absorb(msg: Uint8Array): this
squeeze(n: number): Uint8Array
reset(): this
dispose(): void
}| Method | Description |
|---|---|
hash(msg, outputLength) |
One-shot: reset, absorb, squeeze. Safe on a dirty instance. |
absorb(msg) |
Feed data into the sponge. Chainable. Throws if called after squeeze(). |
squeeze(n) |
Pull n bytes of XOF output. Output is contiguous — squeeze(a) followed by squeeze(b) yields bytes [0, a) and [a, a+b) of the XOF stream. |
reset() |
Return to a fresh, zeroed state. Chainable. Safe at any point. Does not release the sha3 exclusivity token. |
dispose() |
Zero all WASM state and the TS-side block buffer, release the sha3 exclusivity token. Idempotent. |
outputLength / n must be >= 1. Values below 1 throw a RangeError.
After dispose(), all instance methods (reset, absorb, squeeze, hash)
throw Error: SHAKE128: instance has been disposed. Disposal is permanent;
construct a new instance if you need to continue.
Extendable-output function (XOF). Produces variable-length output — any number of bytes you request. 256-bit security level.
Caution
SHAKE256 is stateful and holds exclusive access to the sha3 WASM module
for its entire lifetime. Constructing a second SHAKE128/SHAKE256 or any
other sha3 class while this instance is live throws. Call dispose() when
done.
class SHAKE256 {
constructor()
hash(msg: Uint8Array, outputLength: number): Uint8Array
absorb(msg: Uint8Array): this
squeeze(n: number): Uint8Array
reset(): this
dispose(): void
}| Method | Description |
|---|---|
hash(msg, outputLength) |
One-shot: reset, absorb, squeeze. Safe on a dirty instance. |
absorb(msg) |
Feed data into the sponge. Chainable. Throws if called after squeeze(). |
squeeze(n) |
Pull n bytes of XOF output. Output is contiguous — squeeze(a) followed by squeeze(b) yields bytes [0, a) and [a, a+b) of the XOF stream. |
reset() |
Return to a fresh, zeroed state. Chainable. Safe at any point. Does not release the sha3 exclusivity token. |
dispose() |
Zero all WASM state and the TS-side block buffer, release the sha3 exclusivity token. Idempotent. |
outputLength / n must be >= 1. Values below 1 throw a RangeError.
After dispose(), all instance methods (reset, absorb, squeeze, hash)
throw Error: SHAKE256: instance has been disposed. Disposal is permanent;
construct a new instance if you need to continue.
For use cases where you need to pull output in multiple steps — key derivation,
mask generation, protocol-specific domain separation — the SHAKE classes expose
a streaming interface alongside the one-shot hash().
| State | Valid calls |
|---|---|
| fresh |
absorb(), hash(), reset()
|
| absorbing |
absorb(), squeeze(), hash(), reset()
|
| squeezing |
squeeze(), hash(), reset()
|
Calling absorb() while squeezing throws:
"SHAKE128: cannot absorb after squeeze — call reset() first"
hash() always resets before running — safe to call on a dirty instance.
import { init, SHAKE256 } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })
const xof = new SHAKE256()
xof.absorb(ikm) // input key material
xof.absorb(salt) // additional context
const encKey = xof.squeeze(32) // 256-bit encryption key
const macKey = xof.squeeze(32) // 256-bit MAC key
const nonce = xof.squeeze(12) // 96-bit nonce
xof.dispose()Stateless SHA3-256 HashFn for Fortuna's accumulator and reseed slots. Plain
const object — no instantiation, no dispose().
Requires init({ sha3: sha3Wasm }) (or the keccak alias). See
fortuna.md for full usage with Fortuna.create().
| Property | Value |
|---|---|
outputSize |
32 |
wasmModules |
['sha3'] |
Hashes msg and returns a 32-byte SHA3-256 digest. Wipes WASM input/output/sponge
state scratch before returning.
| Parameter | Type | Description |
|---|---|---|
msg |
Uint8Array |
Message to hash (any length) |
Returns a new Uint8Array of 32 bytes.
Throws Error if another stateful instance currently owns the sha3 WASM module.
import { init, Fortuna } from 'leviathan-crypto'
import { SHA3_256Hash } from 'leviathan-crypto/sha3'
import { ChaCha20Generator } from 'leviathan-crypto/chacha20'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
import { chacha20Wasm } from 'leviathan-crypto/chacha20/embedded'
await init({ sha3: sha3Wasm, chacha20: chacha20Wasm })
const rng = await Fortuna.create({ generator: ChaCha20Generator, hash: SHA3_256Hash })
const bytes = rng.get(32)
rng.stop()The most common use case: hash some data and get a hex digest.
import { init, SHA3_256, bytesToHex, utf8ToBytes } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
// Initialize the SHA-3 WASM module (once, at startup)
await init({ sha3: sha3Wasm })
// Create a hasher
const sha3 = new SHA3_256()
// Hash a UTF-8 string
const message = utf8ToBytes('Hello, world!')
const digest = sha3.hash(message)
console.log(bytesToHex(digest))
// 32 bytes (64 hex characters) of SHA3-256 output
// Clean up -- zeroes all WASM state
sha3.dispose()import { init, SHA3_512, bytesToHex } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })
const sha3 = new SHA3_512()
// Hash raw bytes (e.g., a file, a key, a nonce)
const data = new Uint8Array([0x01, 0x02, 0x03, 0x04])
const digest = sha3.hash(data)
console.log(bytesToHex(digest))
// 64 bytes (128 hex characters) of SHA3-512 output
sha3.dispose()Each call to hash() is independent; the internal state resets automatically.
You can reuse the same class instance for multiple hashes.
import { init, SHA3_256, bytesToHex, utf8ToBytes } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })
const sha3 = new SHA3_256()
const hash1 = sha3.hash(utf8ToBytes('first message'))
const hash2 = sha3.hash(utf8ToBytes('second message'))
const hash3 = sha3.hash(utf8ToBytes('first message'))
// hash1 and hash3 are identical -- same input, same output
console.log(bytesToHex(hash1) === bytesToHex(hash3)) // true
// hash2 is different -- different input
console.log(bytesToHex(hash1) === bytesToHex(hash2)) // false
sha3.dispose()SHAKE lets you choose exactly how many bytes of output you need. This is useful for key derivation or generating fixed-size tokens.
import { init, SHAKE128, bytesToHex, utf8ToBytes } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })
const shake = new SHAKE128()
const seed = utf8ToBytes('my-application-seed')
// Derive a 16-byte key (128 bits)
const key128 = shake.hash(seed, 16)
console.log('16-byte key:', bytesToHex(key128))
// Derive a 32-byte key (256 bits) from the same seed
const key256 = shake.hash(seed, 32)
console.log('32-byte key:', bytesToHex(key256))
// The 16-byte output is NOT a prefix of the 32-byte output --
// each call resets state, re-absorbs, and squeezes independently.
// However, for SHAKE, the first 16 bytes of the 32-byte output
// ARE identical to the 16-byte output (this is how XOFs work).
shake.dispose()import { init, SHAKE256, bytesToHex } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })
const shake = new SHAKE256()
// Derive a 48-byte key from raw entropy
const entropy = crypto.getRandomValues(new Uint8Array(32))
const derivedKey = shake.hash(entropy, 48)
console.log('Derived key:', bytesToHex(derivedKey))
// 48 bytes (96 hex characters) of SHAKE256 output
shake.dispose()SHA-256 (from the SHA-2 family) and SHA3-256 are completely different algorithms. They produce different output for the same input. Both are secure; SHA3-256 adds defense-in-depth.
import { init, SHA256, SHA3_256, bytesToHex, utf8ToBytes } from 'leviathan-crypto'
import { sha2Wasm } from 'leviathan-crypto/sha2/embedded'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
// Initialize both modules
await init({ sha2: sha2Wasm, sha3: sha3Wasm })
const sha2 = new SHA256()
const sha3 = new SHA3_256()
const message = utf8ToBytes('abc')
const sha2Digest = sha2.hash(message)
const sha3Digest = sha3.hash(message)
console.log('SHA-256: ', bytesToHex(sha2Digest))
console.log('SHA3-256: ', bytesToHex(sha3Digest))
// These are completely different values -- different algorithms
sha2.dispose()
sha3.dispose()All hash functions accept empty input. This is well-defined and produces a deterministic output.
import { init, SHA3_256, bytesToHex } from 'leviathan-crypto'
import { sha3Wasm } from 'leviathan-crypto/sha3/embedded'
await init({ sha3: sha3Wasm })
const sha3 = new SHA3_256()
const digest = sha3.hash(new Uint8Array(0))
console.log(bytesToHex(digest))
// The SHA3-256 hash of empty input -- a fixed, known value
sha3.dispose()If you construct a SHA-3 class before initializing the module, the constructor throws immediately:
Error: leviathan-crypto: call init({ sha3: ... }) before using this class
Fix: Call await init({ sha3: sha3Wasm }) once at application startup, before creating
any SHA-3 class instances.
SHAKE128 and SHAKE256 require outputLength >= 1. Passing 0 or a negative number
throws a RangeError:
RangeError: outputLength must be >= 1 (got 0)
Fix: Request at least 1 byte.
Calling absorb() after squeeze() has been called throws an Error. The sponge
has been padded and finalized — further absorption is not meaningful.
Error: SHAKE128: cannot absorb after squeeze — call reset() first
Fix: Call reset() to return the instance to a fresh state before absorbing
new data.
Passing an empty Uint8Array (length 0) is not an error. All SHA-3 and SHAKE
functions produce valid, deterministic output for empty input. The sponge simply
absorbs zero bytes and then squeezes.
| Document | Description |
|---|---|
| index | Project Documentation index |
| architecture | architecture overview, module relationships, buffer layouts, and build pipeline |
| sha3_audit.md | SHA-3 / Keccak implementation audit |