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.
- package/CLAUDE.md +265 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +174 -0
- package/dist/chacha.wasm +0 -0
- package/dist/chacha20/index.d.ts +49 -0
- package/dist/chacha20/index.js +177 -0
- package/dist/chacha20/ops.d.ts +16 -0
- package/dist/chacha20/ops.js +146 -0
- package/dist/chacha20/pool.d.ts +52 -0
- package/dist/chacha20/pool.js +188 -0
- package/dist/chacha20/pool.worker.d.ts +1 -0
- package/dist/chacha20/pool.worker.js +37 -0
- package/dist/chacha20/types.d.ts +30 -0
- package/dist/chacha20/types.js +1 -0
- package/dist/docs/architecture.md +795 -0
- package/dist/docs/argon2id.md +290 -0
- package/dist/docs/chacha20.md +602 -0
- package/dist/docs/chacha20_pool.md +306 -0
- package/dist/docs/fortuna.md +322 -0
- package/dist/docs/init.md +308 -0
- package/dist/docs/loader.md +206 -0
- package/dist/docs/serpent.md +914 -0
- package/dist/docs/sha2.md +620 -0
- package/dist/docs/sha3.md +509 -0
- package/dist/docs/types.md +198 -0
- package/dist/docs/utils.md +273 -0
- package/dist/docs/wasm.md +193 -0
- package/dist/embedded/chacha.d.ts +1 -0
- package/dist/embedded/chacha.js +2 -0
- package/dist/embedded/serpent.d.ts +1 -0
- package/dist/embedded/serpent.js +2 -0
- package/dist/embedded/sha2.d.ts +1 -0
- package/dist/embedded/sha2.js +2 -0
- package/dist/embedded/sha3.d.ts +1 -0
- package/dist/embedded/sha3.js +2 -0
- package/dist/fortuna.d.ts +72 -0
- package/dist/fortuna.js +445 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +44 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +49 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.js +30 -0
- package/dist/serpent/index.d.ts +65 -0
- package/dist/serpent/index.js +242 -0
- package/dist/serpent/seal.d.ts +8 -0
- package/dist/serpent/seal.js +70 -0
- package/dist/serpent/stream-encoder.d.ts +20 -0
- package/dist/serpent/stream-encoder.js +167 -0
- package/dist/serpent/stream-pool.d.ts +48 -0
- package/dist/serpent/stream-pool.js +285 -0
- package/dist/serpent/stream-sealer.d.ts +34 -0
- package/dist/serpent/stream-sealer.js +223 -0
- package/dist/serpent/stream.d.ts +28 -0
- package/dist/serpent/stream.js +205 -0
- package/dist/serpent/stream.worker.d.ts +32 -0
- package/dist/serpent/stream.worker.js +117 -0
- package/dist/serpent/types.d.ts +5 -0
- package/dist/serpent/types.js +1 -0
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.d.ts +16 -0
- package/dist/sha2/hkdf.js +108 -0
- package/dist/sha2/index.d.ts +40 -0
- package/dist/sha2/index.js +190 -0
- package/dist/sha2/types.d.ts +5 -0
- package/dist/sha2/types.js +1 -0
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +55 -0
- package/dist/sha3/index.js +246 -0
- package/dist/sha3/types.d.ts +5 -0
- package/dist/sha3/types.js +1 -0
- package/dist/sha3.wasm +0 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +26 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +169 -0
- 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`
|