leviathan-crypto 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CLAUDE.md +265 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -0
  4. package/SECURITY.md +174 -0
  5. package/dist/chacha.wasm +0 -0
  6. package/dist/chacha20/index.d.ts +49 -0
  7. package/dist/chacha20/index.js +177 -0
  8. package/dist/chacha20/ops.d.ts +16 -0
  9. package/dist/chacha20/ops.js +146 -0
  10. package/dist/chacha20/pool.d.ts +52 -0
  11. package/dist/chacha20/pool.js +188 -0
  12. package/dist/chacha20/pool.worker.d.ts +1 -0
  13. package/dist/chacha20/pool.worker.js +37 -0
  14. package/dist/chacha20/types.d.ts +30 -0
  15. package/dist/chacha20/types.js +1 -0
  16. package/dist/docs/architecture.md +795 -0
  17. package/dist/docs/argon2id.md +290 -0
  18. package/dist/docs/chacha20.md +602 -0
  19. package/dist/docs/chacha20_pool.md +306 -0
  20. package/dist/docs/fortuna.md +322 -0
  21. package/dist/docs/init.md +308 -0
  22. package/dist/docs/loader.md +206 -0
  23. package/dist/docs/serpent.md +914 -0
  24. package/dist/docs/sha2.md +620 -0
  25. package/dist/docs/sha3.md +509 -0
  26. package/dist/docs/types.md +198 -0
  27. package/dist/docs/utils.md +273 -0
  28. package/dist/docs/wasm.md +193 -0
  29. package/dist/embedded/chacha.d.ts +1 -0
  30. package/dist/embedded/chacha.js +2 -0
  31. package/dist/embedded/serpent.d.ts +1 -0
  32. package/dist/embedded/serpent.js +2 -0
  33. package/dist/embedded/sha2.d.ts +1 -0
  34. package/dist/embedded/sha2.js +2 -0
  35. package/dist/embedded/sha3.d.ts +1 -0
  36. package/dist/embedded/sha3.js +2 -0
  37. package/dist/fortuna.d.ts +72 -0
  38. package/dist/fortuna.js +445 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.js +44 -0
  41. package/dist/init.d.ts +11 -0
  42. package/dist/init.js +49 -0
  43. package/dist/loader.d.ts +4 -0
  44. package/dist/loader.js +30 -0
  45. package/dist/serpent/index.d.ts +65 -0
  46. package/dist/serpent/index.js +242 -0
  47. package/dist/serpent/seal.d.ts +8 -0
  48. package/dist/serpent/seal.js +70 -0
  49. package/dist/serpent/stream-encoder.d.ts +20 -0
  50. package/dist/serpent/stream-encoder.js +167 -0
  51. package/dist/serpent/stream-pool.d.ts +48 -0
  52. package/dist/serpent/stream-pool.js +285 -0
  53. package/dist/serpent/stream-sealer.d.ts +34 -0
  54. package/dist/serpent/stream-sealer.js +223 -0
  55. package/dist/serpent/stream.d.ts +28 -0
  56. package/dist/serpent/stream.js +205 -0
  57. package/dist/serpent/stream.worker.d.ts +32 -0
  58. package/dist/serpent/stream.worker.js +117 -0
  59. package/dist/serpent/types.d.ts +5 -0
  60. package/dist/serpent/types.js +1 -0
  61. package/dist/serpent.wasm +0 -0
  62. package/dist/sha2/hkdf.d.ts +16 -0
  63. package/dist/sha2/hkdf.js +108 -0
  64. package/dist/sha2/index.d.ts +40 -0
  65. package/dist/sha2/index.js +190 -0
  66. package/dist/sha2/types.d.ts +5 -0
  67. package/dist/sha2/types.js +1 -0
  68. package/dist/sha2.wasm +0 -0
  69. package/dist/sha3/index.d.ts +55 -0
  70. package/dist/sha3/index.js +246 -0
  71. package/dist/sha3/types.d.ts +5 -0
  72. package/dist/sha3/types.js +1 -0
  73. package/dist/sha3.wasm +0 -0
  74. package/dist/types.d.ts +24 -0
  75. package/dist/types.js +26 -0
  76. package/dist/utils.d.ts +26 -0
  77. package/dist/utils.js +169 -0
  78. package/package.json +90 -0
@@ -0,0 +1,620 @@
1
+ # SHA-2 hash functions and HMAC TypeScript API
2
+
3
+ > [!NOTE]
4
+ > Cryptographic hashing and message authentication using SHA-256, SHA-384,
5
+ > SHA-512, HMAC-SHA256, HMAC-SHA384, and HMAC-SHA512.
6
+
7
+ ## Overview
8
+
9
+ SHA-2 is a family of cryptographic hash functions standardized in
10
+ [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final).
11
+ A hash function takes an input of any size -- a password, a file, a single
12
+ byte -- and produces a fixed-size output called a **digest** (sometimes called
13
+ a "fingerprint" or "hash"). Even the smallest change to the input produces a
14
+ completely different digest. This makes hash functions useful for verifying that
15
+ data has not been tampered with.
16
+
17
+ leviathan-crypto provides three SHA-2 variants:
18
+
19
+ - **SHA-256** -- 32-byte (256-bit) digest. The most widely used variant. Use
20
+ this unless you have a specific reason to choose another.
21
+ - **SHA-512** -- 64-byte (512-bit) digest. Higher security margin. Faster than
22
+ SHA-256 on 64-bit platforms.
23
+ - **SHA-384** -- 48-byte (384-bit) digest. A truncated variant of SHA-512.
24
+ Useful when you need a digest longer than 256 bits but shorter than 512 bits,
25
+ or when a protocol specifies it (e.g. TLS cipher suites).
26
+
27
+ **HMAC** (Hash-based Message Authentication Code, [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104))
28
+ combines a secret key with a hash function to produce a **tag** that proves both
29
+ the integrity and the authenticity of a message. Anyone can compute a plain SHA-256
30
+ hash of a message -- but only someone who holds the secret key can compute the
31
+ correct HMAC tag. This means the recipient can verify that the message was sent by
32
+ someone who knows the key, and that it was not modified in transit.
33
+
34
+ leviathan-crypto provides three HMAC variants corresponding to each hash:
35
+
36
+ - **HMAC_SHA256** -- 32-byte tag, using SHA-256
37
+ - **HMAC_SHA512** -- 64-byte tag, using SHA-512
38
+ - **HMAC_SHA384** -- 48-byte tag, using SHA-384
39
+
40
+ All computation runs in WebAssembly. The TypeScript classes handle input
41
+ validation and the JS/WASM boundary -- they never implement cryptographic
42
+ algorithms directly.
43
+
44
+ ---
45
+
46
+ ## Security Notes
47
+
48
+ > [!IMPORTANT]
49
+ > Read these before using the API. Misusing hash functions is one of the most
50
+ > common sources of security vulnerabilities.
51
+
52
+ ### Hashing is NOT encryption
53
+
54
+ A hash is a one-way function. You **cannot** recover the original input from a
55
+ hash digest. If you need to protect data so that it can be read later, you need
56
+ encryption (see [serpent.md](./serpent.md) or use `XChaCha20Poly1305`).
57
+
58
+ ### Do NOT use plain SHA-2 for passwords
59
+
60
+ SHA-2 is extremely fast by design. An attacker with a GPU can compute billions
61
+ of SHA-256 hashes per second, making brute-force attacks on passwords trivial.
62
+ For password hashing, use a memory-hardened function like **Argon2id**. See
63
+ [argon2id.md](./argon2id.md) for usage patterns including passphrase-based
64
+ encryption with leviathan primitives.
65
+
66
+ ### SHA-2 is vulnerable to length extension attacks
67
+
68
+ Never construct a MAC by concatenating a secret and a message and hashing them:
69
+
70
+ ```typescript
71
+ // DANGEROUS -- DO NOT DO THIS
72
+ const bad = sha256.hash(concat(secret, message))
73
+ ```
74
+
75
+ An attacker who sees `SHA256(secret || message)` can compute
76
+ `SHA256(secret || message || padding || attacker_data)` without knowing the
77
+ secret. This is called a **length extension attack**.
78
+
79
+ **Always use HMAC** when you need to authenticate a message with a secret key.
80
+ HMAC is specifically designed to be immune to this attack.
81
+
82
+ ### HMAC key length
83
+
84
+ HMAC keys should be **at least as long as the hash output**:
85
+
86
+ | HMAC variant | Minimum recommended key length |
87
+ |---------------|-------------------------------|
88
+ | HMAC_SHA256 | 32 bytes (256 bits) |
89
+ | HMAC_SHA384 | 48 bytes (384 bits) |
90
+ | HMAC_SHA512 | 64 bytes (512 bits) |
91
+
92
+ Keys shorter than this are technically valid (they will be zero-padded
93
+ internally) but provide less security than the hash function offers. Keys
94
+ longer than the hash block size (64 bytes for SHA-256, 128 bytes for
95
+ SHA-384/SHA-512) are pre-hashed automatically per RFC 2104 section 3 -- this is
96
+ handled for you, but there is no benefit to using very long keys.
97
+
98
+ ### Always use constant-time comparison for HMAC verification
99
+
100
+ When verifying an HMAC tag, **never** use `===` or any other comparison that
101
+ can return early on the first mismatched byte. An attacker can measure how long
102
+ the comparison takes and use that information to forge a valid tag one byte at a
103
+ time (this is called a **timing attack**).
104
+
105
+ Use `constantTimeEqual()` from leviathan-crypto instead. It always compares
106
+ every byte regardless of where the first difference is.
107
+
108
+ ### Call dispose() when you are done
109
+
110
+ `dispose()` calls `wipeBuffers()` in the WASM module, which zeroes out all
111
+ internal buffers including hash state and key material. This prevents sensitive
112
+ data from lingering in memory. Always call `dispose()` when you are finished
113
+ with a hash or HMAC instance.
114
+
115
+ ---
116
+
117
+ ## Module Init
118
+
119
+ Each module subpath exports its own init function for consumers who want
120
+ tree-shakeable imports.
121
+
122
+ ### `sha2Init(mode?, opts?)`
123
+
124
+ Initializes only the sha2 WASM binary. Equivalent to calling the
125
+ root `init(['sha2'], mode, opts)` but without pulling the other three
126
+ modules into the bundle.
127
+
128
+ **Signature:**
129
+
130
+ ```typescript
131
+ async function sha2Init(mode?: Mode, opts?: InitOpts): Promise<void>
132
+ ```
133
+
134
+ **Usage:**
135
+
136
+ ```typescript
137
+ import { sha2Init, SHA256 } from 'leviathan-crypto/sha2'
138
+
139
+ await sha2Init()
140
+ const sha = new SHA256()
141
+ ```
142
+
143
+ ---
144
+
145
+ ## API Reference
146
+
147
+ All classes require `init(['sha2'])` or the subpath `sha2Init()` to be called first.
148
+ Constructing any SHA-2 class before initialization throws an error.
149
+
150
+ ### SHA256
151
+
152
+ Computes a SHA-256 hash (32-byte digest).
153
+
154
+ ```typescript
155
+ class SHA256 {
156
+ constructor()
157
+ hash(msg: Uint8Array): Uint8Array
158
+ dispose(): void
159
+ }
160
+ ```
161
+
162
+ **`constructor()`** -- Creates a new SHA256 instance. Throws if `init(['sha2'])`
163
+ has not been called.
164
+
165
+ **`hash(msg: Uint8Array): Uint8Array`** -- Hashes the entire message and returns
166
+ a 32-byte `Uint8Array` digest. The message can be any length (including empty).
167
+ Large messages are internally chunked and streamed through the WASM hash function,
168
+ so memory usage stays constant regardless of input size.
169
+
170
+ **`dispose(): void`** -- Wipes all internal WASM buffers (hash state, input
171
+ buffer, output buffer). Call this when you are done with the instance.
172
+
173
+ ---
174
+
175
+ ### SHA512
176
+
177
+ Computes a SHA-512 hash (64-byte digest).
178
+
179
+ ```typescript
180
+ class SHA512 {
181
+ constructor()
182
+ hash(msg: Uint8Array): Uint8Array
183
+ dispose(): void
184
+ }
185
+ ```
186
+
187
+ **`constructor()`** -- Creates a new SHA512 instance. Throws if not initialized.
188
+
189
+ **`hash(msg: Uint8Array): Uint8Array`** -- Returns a 64-byte digest.
190
+
191
+ **`dispose(): void`** -- Wipes all internal WASM buffers.
192
+
193
+ ---
194
+
195
+ ### SHA384
196
+
197
+ Computes a SHA-384 hash (48-byte digest). SHA-384 is a truncated variant of
198
+ SHA-512 with different initial values.
199
+
200
+ ```typescript
201
+ class SHA384 {
202
+ constructor()
203
+ hash(msg: Uint8Array): Uint8Array
204
+ dispose(): void
205
+ }
206
+ ```
207
+
208
+ **`constructor()`** -- Creates a new SHA384 instance. Throws if not initialized.
209
+
210
+ **`hash(msg: Uint8Array): Uint8Array`** -- Returns a 48-byte digest.
211
+
212
+ **`dispose(): void`** -- Wipes all internal WASM buffers.
213
+
214
+ ---
215
+
216
+ ### HMAC_SHA256
217
+
218
+ Computes an HMAC-SHA256 authentication tag (32-byte output).
219
+
220
+ ```typescript
221
+ class HMAC_SHA256 {
222
+ constructor()
223
+ hash(key: Uint8Array, msg: Uint8Array): Uint8Array
224
+ dispose(): void
225
+ }
226
+ ```
227
+
228
+ **`constructor()`** -- Creates a new HMAC_SHA256 instance. Throws if not
229
+ initialized.
230
+
231
+ **`hash(key: Uint8Array, msg: Uint8Array): Uint8Array`** -- Computes the
232
+ HMAC-SHA256 tag for the given message using the given key. Returns a 32-byte
233
+ `Uint8Array`. Keys longer than 64 bytes are automatically pre-hashed with
234
+ SHA-256 per RFC 2104 section 3.
235
+
236
+ **`dispose(): void`** -- Wipes all internal WASM buffers, including key material.
237
+
238
+ ---
239
+
240
+ ### HMAC_SHA512
241
+
242
+ Computes an HMAC-SHA512 authentication tag (64-byte output).
243
+
244
+ ```typescript
245
+ class HMAC_SHA512 {
246
+ constructor()
247
+ hash(key: Uint8Array, msg: Uint8Array): Uint8Array
248
+ dispose(): void
249
+ }
250
+ ```
251
+
252
+ **`constructor()`** -- Creates a new HMAC_SHA512 instance. Throws if not
253
+ initialized.
254
+
255
+ **`hash(key: Uint8Array, msg: Uint8Array): Uint8Array`** -- Returns a 64-byte
256
+ HMAC tag. Keys longer than 128 bytes are pre-hashed with SHA-512.
257
+
258
+ **`dispose(): void`** -- Wipes all internal WASM buffers.
259
+
260
+ ---
261
+
262
+ ### HMAC_SHA384
263
+
264
+ Computes an HMAC-SHA384 authentication tag (48-byte output).
265
+
266
+ ```typescript
267
+ class HMAC_SHA384 {
268
+ constructor()
269
+ hash(key: Uint8Array, msg: Uint8Array): Uint8Array
270
+ dispose(): void
271
+ }
272
+ ```
273
+
274
+ **`constructor()`** -- Creates a new HMAC_SHA384 instance. Throws if not
275
+ initialized.
276
+
277
+ **`hash(key: Uint8Array, msg: Uint8Array): Uint8Array`** -- Returns a 48-byte
278
+ HMAC tag. Keys longer than 128 bytes are pre-hashed with SHA-384.
279
+
280
+ **`dispose(): void`** -- Wipes all internal WASM buffers.
281
+
282
+ ---
283
+
284
+ ### HKDF_SHA256
285
+
286
+ RFC 5869 HMAC-based Extract-and-Expand Key Derivation Function over
287
+ HMAC-SHA256. Use HKDF when you need to derive one or more keys from a shared
288
+ secret (e.g. after a Diffie-Hellman exchange) or to separate keys for different
289
+ purposes from a single source of keying material.
290
+
291
+ `HKDF_SHA256` should be the default choice. `HKDF_SHA384` does not exist.
292
+
293
+ ```typescript
294
+ class HKDF_SHA256 {
295
+ constructor()
296
+ extract(salt: Uint8Array | null, ikm: Uint8Array): Uint8Array
297
+ expand(prk: Uint8Array, info: Uint8Array, length: number): Uint8Array
298
+ derive(ikm: Uint8Array, salt: Uint8Array | null, info: Uint8Array, length: number): Uint8Array
299
+ dispose(): void
300
+ }
301
+ ```
302
+
303
+ **`constructor()`** -- Creates a new HKDF_SHA256 instance. Throws if
304
+ `init(['sha2'])` has not been called.
305
+
306
+ **`extract(salt, ikm): Uint8Array`** -- RFC 5869 section 2.2. Computes
307
+ `PRK = HMAC-SHA256(salt, IKM)`. Returns a 32-byte pseudorandom key. If `salt`
308
+ is `null` or empty, defaults to 32 zero bytes per RFC section 2.2.
309
+
310
+ **`expand(prk, info, length): Uint8Array`** -- RFC 5869 section 2.3. Derives
311
+ `length` bytes of output keying material from a 32-byte PRK. `info` provides
312
+ application-specific context (can be empty). `length` must be between 1 and
313
+ 8160 (255 x 32). Throws `RangeError` if `prk` is not exactly 32 bytes or if
314
+ `length` is out of range.
315
+
316
+ **`derive(ikm, salt, info, length): Uint8Array`** -- One-shot: calls
317
+ `extract(salt, ikm)` then `expand(prk, info, length)`. This is the correct
318
+ path for most callers. `extract()` and `expand()` are exposed separately for
319
+ advanced use cases such as key separation and ratchets -- callers who reach for
320
+ them should know why.
321
+
322
+ **`dispose(): void`** -- Releases the internal HMAC instance.
323
+
324
+ ---
325
+
326
+ ### HKDF_SHA512
327
+
328
+ Identical to `HKDF_SHA256` but uses HMAC-SHA512 internally. HashLen is 64, so
329
+ PRK must be exactly 64 bytes and maximum output length is 16320 (255 x 64).
330
+
331
+ ```typescript
332
+ class HKDF_SHA512 {
333
+ constructor()
334
+ extract(salt: Uint8Array | null, ikm: Uint8Array): Uint8Array
335
+ expand(prk: Uint8Array, info: Uint8Array, length: number): Uint8Array
336
+ derive(ikm: Uint8Array, salt: Uint8Array | null, info: Uint8Array, length: number): Uint8Array
337
+ dispose(): void
338
+ }
339
+ ```
340
+
341
+ **`extract(salt, ikm)`** -- If `salt` is `null` or empty, defaults to 64 zero
342
+ bytes.
343
+
344
+ **`expand(prk, info, length)`** -- PRK must be exactly 64 bytes. `length` must
345
+ be between 1 and 16320. Throws `RangeError` otherwise.
346
+
347
+ > [!NOTE]
348
+ > HKDF is a pure TypeScript composition over the WASM-backed HMAC classes.
349
+ > It does not introduce new WASM code or new `init()` modules. Initializing
350
+ > `sha2` is sufficient.
351
+
352
+ **Usage example:**
353
+
354
+ ```typescript
355
+ import { init, HKDF_SHA256, bytesToHex } from 'leviathan-crypto'
356
+
357
+ await init(['sha2'])
358
+
359
+ const hkdf = new HKDF_SHA256()
360
+ const ikm = new Uint8Array(32) // your input keying material
361
+ const salt = crypto.getRandomValues(new Uint8Array(32))
362
+ const info = new TextEncoder().encode('my-app-v1-encryption-key')
363
+
364
+ const key = hkdf.derive(ikm, salt, info, 32)
365
+ console.log('Derived key:', bytesToHex(key))
366
+
367
+ hkdf.dispose()
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Usage Examples
373
+
374
+ ### Example 1: Hash a message with SHA-256
375
+
376
+ The most common operation: hash a string and get a hex-encoded digest.
377
+
378
+ ```typescript
379
+ import { init, SHA256, bytesToHex, utf8ToBytes } from 'leviathan-crypto'
380
+
381
+ // Step 1: Initialize the SHA-2 WASM module (do this once at app startup)
382
+ await init(['sha2'])
383
+
384
+ // Step 2: Create a SHA256 instance
385
+ const sha = new SHA256()
386
+
387
+ // Step 3: Hash a message
388
+ // utf8ToBytes converts a string to a Uint8Array
389
+ const message = utf8ToBytes('Hello, world!')
390
+ const digest = sha.hash(message)
391
+
392
+ // Step 4: Convert the digest to a hex string for display or storage
393
+ console.log(bytesToHex(digest))
394
+ // => "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
395
+
396
+ // Step 5: Clean up -- wipes hash state from WASM memory
397
+ sha.dispose()
398
+ ```
399
+
400
+ ### Example 2: Hash binary data (e.g., a file)
401
+
402
+ SHA-256 works on raw bytes, not just strings. You can hash any `Uint8Array`,
403
+ including file contents read from a `<input type="file">` element or a fetch
404
+ response.
405
+
406
+ ```typescript
407
+ import { init, SHA256, bytesToHex } from 'leviathan-crypto'
408
+
409
+ await init(['sha2'])
410
+
411
+ // Suppose you have file contents as an ArrayBuffer (from FileReader, fetch, etc.)
412
+ const response = await fetch('https://example.com/file.bin')
413
+ const buffer = new Uint8Array(await response.arrayBuffer())
414
+
415
+ const sha = new SHA256()
416
+ const digest = sha.hash(buffer)
417
+ console.log('SHA-256:', bytesToHex(digest))
418
+
419
+ sha.dispose()
420
+ ```
421
+
422
+ The library handles large inputs automatically -- it streams the data through
423
+ the WASM hash function in chunks, so you do not need to worry about memory.
424
+
425
+ ### Example 3: Using SHA-512 or SHA-384
426
+
427
+ The API is identical for all three hash variants. Only the output size differs.
428
+
429
+ ```typescript
430
+ import { init, SHA256, SHA384, SHA512, bytesToHex, utf8ToBytes } from 'leviathan-crypto'
431
+
432
+ await init(['sha2'])
433
+
434
+ const msg = utf8ToBytes('Same message, different hashes')
435
+
436
+ const sha256 = new SHA256()
437
+ const sha384 = new SHA384()
438
+ const sha512 = new SHA512()
439
+
440
+ console.log('SHA-256 (32 bytes):', bytesToHex(sha256.hash(msg)))
441
+ console.log('SHA-384 (48 bytes):', bytesToHex(sha384.hash(msg)))
442
+ console.log('SHA-512 (64 bytes):', bytesToHex(sha512.hash(msg)))
443
+
444
+ sha256.dispose()
445
+ sha384.dispose()
446
+ sha512.dispose()
447
+ ```
448
+
449
+ ### Example 4: Generate and verify an HMAC
450
+
451
+ Use HMAC when you need to prove that a message was created by someone who holds
452
+ a secret key. A typical pattern: one side generates a tag, the other side
453
+ recomputes the tag with the same key and checks that they match.
454
+
455
+ ```typescript
456
+ import {
457
+ init, HMAC_SHA256, constantTimeEqual, randomBytes,
458
+ bytesToHex, utf8ToBytes
459
+ } from 'leviathan-crypto'
460
+
461
+ await init(['sha2'])
462
+
463
+ // Generate a random 32-byte key (do this once, store it securely)
464
+ const key = randomBytes(32)
465
+
466
+ const hmac = new HMAC_SHA256()
467
+ const message = utf8ToBytes('Transfer $100 to Alice')
468
+
469
+ // --- Sender side: generate the tag ---
470
+ const tag = hmac.hash(key, message)
471
+ console.log('HMAC tag:', bytesToHex(tag))
472
+
473
+ // Send both `message` and `tag` to the recipient...
474
+
475
+ // --- Recipient side: verify the tag ---
476
+ // The recipient has the same key and recomputes the tag
477
+ const recomputed = hmac.hash(key, message)
478
+
479
+ // Use constant-time comparison to check the tags
480
+ if (constantTimeEqual(tag, recomputed)) {
481
+ console.log('Message is authentic -- it was not tampered with')
482
+ } else {
483
+ console.log('WARNING: message has been modified or key is wrong')
484
+ }
485
+
486
+ hmac.dispose()
487
+ ```
488
+
489
+ ### Example 5: HMAC verification -- the wrong way vs. the right way
490
+
491
+ This is important enough to call out separately. The difference between these
492
+ two approaches is the difference between a secure system and a broken one.
493
+
494
+ ```typescript
495
+ import { init, HMAC_SHA256, constantTimeEqual, bytesToHex } from 'leviathan-crypto'
496
+
497
+ await init(['sha2'])
498
+ const hmac = new HMAC_SHA256()
499
+
500
+ // Suppose you received a message with a tag and you recomputed the expected tag:
501
+ const receivedTag = hmac.hash(key, message)
502
+ const expectedTag = hmac.hash(key, message)
503
+
504
+ // WRONG -- timing attack vulnerable!
505
+ // JavaScript's === operator compares byte-by-byte and returns false as soon as
506
+ // it finds a mismatch. An attacker can measure the response time to figure out
507
+ // how many leading bytes of the tag are correct, then forge a valid tag one
508
+ // byte at a time.
509
+ if (bytesToHex(receivedTag) === bytesToHex(expectedTag)) {
510
+ // This "works" but is insecure
511
+ }
512
+
513
+ // RIGHT -- constant-time comparison
514
+ // constantTimeEqual always examines every byte, regardless of where the first
515
+ // difference is. The comparison takes the same amount of time whether zero
516
+ // bytes match or all bytes match.
517
+ if (constantTimeEqual(receivedTag, expectedTag)) {
518
+ // This is secure
519
+ }
520
+
521
+ hmac.dispose()
522
+ ```
523
+
524
+ ### Example 6: Using HMAC-SHA512 for higher security
525
+
526
+ The pattern is identical to HMAC-SHA256. Use a 64-byte key for full security.
527
+
528
+ ```typescript
529
+ import { init, HMAC_SHA512, constantTimeEqual, randomBytes, utf8ToBytes } from 'leviathan-crypto'
530
+
531
+ await init(['sha2'])
532
+
533
+ // 64-byte key for HMAC-SHA512
534
+ const key = randomBytes(64)
535
+ const hmac = new HMAC_SHA512()
536
+
537
+ const tag = hmac.hash(key, utf8ToBytes('Important message'))
538
+ // tag is a 64-byte Uint8Array
539
+
540
+ // Verify
541
+ const recomputed = hmac.hash(key, utf8ToBytes('Important message'))
542
+ console.log('Valid:', constantTimeEqual(tag, recomputed)) // true
543
+
544
+ hmac.dispose()
545
+ ```
546
+
547
+ ### Example 7: Hashing an empty input
548
+
549
+ SHA-2 is well-defined for empty inputs. This can be useful as a sanity check.
550
+
551
+ ```typescript
552
+ import { init, SHA256, bytesToHex } from 'leviathan-crypto'
553
+
554
+ await init(['sha2'])
555
+
556
+ const sha = new SHA256()
557
+ const digest = sha.hash(new Uint8Array(0))
558
+ console.log(bytesToHex(digest))
559
+ // => "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
560
+ // (This is the well-known SHA-256 hash of the empty string)
561
+
562
+ sha.dispose()
563
+ ```
564
+
565
+ ---
566
+
567
+ ## Error Conditions
568
+
569
+ ### Module not initialized
570
+
571
+ If you construct any SHA-2 class before calling `init(['sha2'])`, the
572
+ constructor throws immediately:
573
+
574
+ ```
575
+ Error: leviathan-crypto: call init(['sha2']) before using this class
576
+ ```
577
+
578
+ **Fix:** Call `await init(['sha2'])` at application startup, before creating any
579
+ SHA-2 instances.
580
+
581
+ ```typescript
582
+ // This will throw:
583
+ const sha = new SHA256() // Error!
584
+
585
+ // Do this instead:
586
+ await init(['sha2'])
587
+ const sha = new SHA256() // OK
588
+ ```
589
+
590
+ ### HMAC key length
591
+
592
+ HMAC accepts keys of any length, including zero-length keys. However:
593
+
594
+ - **Keys shorter than the recommended minimum** (see the table in Security
595
+ Notes) are zero-padded internally. They will produce valid HMAC tags, but the
596
+ security of the MAC is limited by the key length, not the hash output size.
597
+ A 16-byte key with HMAC-SHA256 provides at most 128 bits of security, not 256.
598
+ - **Keys longer than the hash block size** (64 bytes for HMAC-SHA256, 128 bytes
599
+ for HMAC-SHA384 and HMAC-SHA512) are automatically pre-hashed to fit. This is
600
+ standard behavior defined in RFC 2104 section 3 and is handled transparently.
601
+ - **Zero-length keys** are technically valid per the HMAC spec, but provide no
602
+ authentication. Do not use a zero-length key in production.
603
+
604
+ ### Empty message input
605
+
606
+ All hash and HMAC functions accept empty `Uint8Array` inputs (`new Uint8Array(0)`).
607
+ SHA-2 is well-defined for zero-length messages and will return the correct digest.
608
+
609
+ ---
610
+
611
+ ## Cross-References
612
+
613
+ - [README.md](./README.md) — project overview and quick-start guide
614
+ - [architecture.md](./architecture.md) — library architecture and `init()` API
615
+ - [asm_sha2.md](./asm_sha2.md) — WASM implementation details (AssemblyScript buffer layout, compression functions)
616
+ - [sha3.md](./sha3.md) — alternative: SHA-3 family (immune to length extension attacks)
617
+ - [serpent.md](./serpent.md) — SerpentSeal and SerpentStream use HMAC-SHA256 and HKDF internally
618
+ - [argon2id.md](./argon2id.md) — Argon2id password hashing; HKDF expands Argon2id root keys
619
+ - [fortuna.md](./fortuna.md) — Fortuna CSPRNG uses SHA-256 for entropy accumulation
620
+ - [utils.md](./utils.md) — `constantTimeEqual`, `bytesToHex`, `utf8ToBytes`, `randomBytes`