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,290 @@
1
+ # Argon2id: Memory-Hardened Password Hashing and Key Derivation
2
+
3
+ > [!NOTE]
4
+ > leviathan-crypto does not wrap Argon2id. This document covers how to use the
5
+ > [`argon2id`](https://www.npmjs.com/package/argon2id) npm package directly and
6
+ > how to pair it with leviathan primitives for passphrase-based encryption.
7
+
8
+ ## Why Argon2id
9
+
10
+ Password hashing is the last line of defense when a database is breached. If an
11
+ attacker obtains hashed passwords, the hash function determines how expensive it
12
+ is to recover each plaintext. Traditional hash functions — even iterated ones
13
+ like PBKDF2 — are fast on GPUs and custom hardware. An attacker with a few
14
+ thousand dollars of GPU hardware can test billions of PBKDF2-SHA256 candidates
15
+ per second. bcrypt improves on this with a 4 KiB memory requirement that limits
16
+ GPU parallelism, but 4 KiB is trivial by modern standards.
17
+
18
+ Argon2 was the winner of the Password Hashing Competition (PHC, 2013–2015),
19
+ selected from 24 submissions after two years of public analysis. It was designed
20
+ specifically to be **memory-hard**, computing the hash requires not just CPU
21
+ time but a large block of RAM that cannot be traded away. An attacker who tries
22
+ to use less memory must perform exponentially more computation, making GPU and
23
+ ASIC attacks economically impractical.
24
+
25
+ Argon2id is the recommended variant (RFC 9106): it uses Argon2i for the first
26
+ pass (resisting side-channel attacks) and Argon2d for subsequent passes
27
+ (resisting GPU attacks). It is the only Argon2 variant you should use for new
28
+ applications.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ npm i argon2id
36
+ ```
37
+
38
+ The compiled WASM binaries are included. SIMD acceleration is used automatically
39
+ where available (all modern browsers and Node ≥ 18), with a scalar fallback for
40
+ environments that do not support it. Both produce identical output.
41
+
42
+ ---
43
+
44
+ ## Basic usage
45
+
46
+ With a bundler (Rollup, Webpack, Vite):
47
+
48
+ ```typescript
49
+ import loadArgon2idWasm from 'argon2id';
50
+
51
+ const argon2id = await loadArgon2idWasm();
52
+
53
+ const hash = argon2id({
54
+ password: new TextEncoder().encode('hunter2'),
55
+ salt: crypto.getRandomValues(new Uint8Array(16)),
56
+ passes: 2,
57
+ memorySize: 19456, // KiB
58
+ parallelism: 1,
59
+ tagLength: 32,
60
+ });
61
+ // hash is a Uint8Array
62
+ ```
63
+
64
+ Without a bundler (Node, or browsers using `setupWasm` directly):
65
+
66
+ ```typescript
67
+ import setupWasm from 'argon2id/lib/setup.js';
68
+ import { readFileSync } from 'fs';
69
+
70
+ const argon2id = await setupWasm(
71
+ importObj => WebAssembly.instantiate(readFileSync('node_modules/argon2id/dist/simd.wasm'), importObj),
72
+ importObj => WebAssembly.instantiate(readFileSync('node_modules/argon2id/dist/no-simd.wasm'), importObj),
73
+ );
74
+ ```
75
+
76
+ The hash function signature is the same either way.
77
+
78
+ ---
79
+
80
+ ## Parameter presets
81
+
82
+ These align with OWASP and RFC 9106 recommendations:
83
+
84
+ | Name | `memorySize` | `passes` | `parallelism` | `tagLength` | Use case |
85
+ |------|-------------|---------|---------------|-------------|----------|
86
+ | INTERACTIVE | 19456 KiB | 2 | 1 | 32 | Login forms, session tokens |
87
+ | SENSITIVE | 65536 KiB | 3 | 4 | 32 | Master passwords, high-value secrets |
88
+
89
+ **INTERACTIVE** (~200–500 ms on modern hardware) is the right default for user
90
+ login. **SENSITIVE** (~1–2 s, 64 MiB) is for situations where latency is
91
+ acceptable in exchange for significantly stronger resistance: master passwords,
92
+ recovery keys, encryption keys derived from a passphrase.
93
+
94
+ A function that requires 64 MiB of RAM per evaluation means a GPU with 8 GiB
95
+ of VRAM can only run ~128 evaluations in parallel regardless of shader core
96
+ count. PBKDF2 at any practical iteration count cannot approach this resistance.
97
+
98
+ ---
99
+
100
+ ## Password hashing and verification
101
+
102
+ ```typescript
103
+ import loadArgon2idWasm from 'argon2id';
104
+ import { constantTimeEqual } from 'leviathan-crypto';
105
+
106
+ const argon2id = await loadArgon2idWasm();
107
+
108
+ // Registration — hash and store
109
+ const salt = crypto.getRandomValues(new Uint8Array(16));
110
+ const hash = argon2id({
111
+ password: new TextEncoder().encode(password),
112
+ salt,
113
+ passes: 2,
114
+ memorySize: 19456,
115
+ parallelism: 1,
116
+ tagLength: 32,
117
+ });
118
+
119
+ // Store hash, salt, and params together. Salt is not secret.
120
+ db.store(userId, { hash, salt, passes: 2, memorySize: 19456, parallelism: 1 });
121
+
122
+ // Verification — recompute and compare
123
+ const stored = db.load(userId);
124
+ const candidate = argon2id({
125
+ password: new TextEncoder().encode(candidatePassword),
126
+ salt: stored.salt,
127
+ passes: stored.passes,
128
+ memorySize: stored.memorySize,
129
+ parallelism: stored.parallelism,
130
+ tagLength: 32,
131
+ });
132
+
133
+ // constantTimeEqual from leviathan-crypto prevents timing side-channels
134
+ const valid = constantTimeEqual(candidate, stored.hash);
135
+ ```
136
+
137
+ > [!IMPORTANT]
138
+ > The `salt` **must** be stored alongside the hash. It is not secret, but without
139
+ > the original salt the hash cannot be recomputed and verification will always
140
+ > fail. Store `salt`, `passes`, `memorySize`, and `parallelism` together as a
141
+ > unit.
142
+
143
+ ---
144
+
145
+ ## Passphrase-based encryption with leviathan-crypto
146
+
147
+ Argon2id produces a root key from a passphrase. HKDF-SHA256 from leviathan then
148
+ expands that root key into the separate encryption and MAC keys that
149
+ `SerpentSeal` and `XChaCha20Poly1305` expect. Keeping the two steps separate
150
+ means the expensive Argon2id call happens once per passphrase, and HKDF handles
151
+ any further key material needed.
152
+
153
+ ### With SerpentSeal
154
+
155
+ `SerpentSeal` takes a 64-byte key (32-byte enc key + 32-byte MAC key). HKDF
156
+ expands the 32-byte Argon2id output to 64 bytes:
157
+
158
+ ```typescript
159
+ import loadArgon2idWasm from 'argon2id';
160
+ import { init, SerpentSeal, HKDF_SHA256 } from 'leviathan-crypto';
161
+
162
+ await init(['serpent', 'sha2']);
163
+ const argon2id = await loadArgon2idWasm();
164
+
165
+ // ── Encrypt ──────────────────────────────────────────────────────────────────
166
+
167
+ const argonSalt = crypto.getRandomValues(new Uint8Array(16));
168
+ const rootKey = argon2id({
169
+ password: new TextEncoder().encode(passphrase),
170
+ salt: argonSalt,
171
+ passes: 2,
172
+ memorySize: 19456,
173
+ parallelism: 1,
174
+ tagLength: 32,
175
+ });
176
+
177
+ const hkdf = new HKDF_SHA256();
178
+ const fullKey = hkdf.derive(rootKey, argonSalt, new TextEncoder().encode('serpent-seal-v1'), 64);
179
+ hkdf.dispose();
180
+
181
+ const serpent = new SerpentSeal();
182
+ const ciphertext = serpent.encrypt(fullKey, plaintext);
183
+ serpent.dispose();
184
+
185
+ // Store with ciphertext — all required for decryption
186
+ const envelope = { ciphertext, argonSalt };
187
+
188
+ // ── Decrypt ──────────────────────────────────────────────────────────────────
189
+
190
+ const rootKey2 = argon2id({
191
+ password: new TextEncoder().encode(passphrase),
192
+ salt: envelope.argonSalt,
193
+ passes: 2,
194
+ memorySize: 19456,
195
+ parallelism: 1,
196
+ tagLength: 32,
197
+ });
198
+
199
+ const hkdf2 = new HKDF_SHA256();
200
+ const fullKey2 = hkdf2.derive(rootKey2, envelope.argonSalt, new TextEncoder().encode('serpent-seal-v1'), 64);
201
+ hkdf2.dispose();
202
+
203
+ const serpent2 = new SerpentSeal();
204
+ const decrypted = serpent2.decrypt(fullKey2, envelope.ciphertext);
205
+ serpent2.dispose();
206
+ ```
207
+
208
+ ### With XChaCha20Poly1305
209
+
210
+ `XChaCha20Poly1305` takes a 32-byte key and a 24-byte nonce. The nonce is
211
+ generated fresh per encryption; only the Argon2id salt needs to be stored:
212
+
213
+ ```typescript
214
+ import loadArgon2idWasm from 'argon2id';
215
+ import { init, XChaCha20Poly1305, HKDF_SHA256 } from 'leviathan-crypto';
216
+
217
+ await init(['chacha20', 'sha2']);
218
+ const argon2id = await loadArgon2idWasm();
219
+
220
+ // ── Encrypt ──────────────────────────────────────────────────────────────────
221
+
222
+ const argonSalt = crypto.getRandomValues(new Uint8Array(16));
223
+ const rootKey = argon2id({
224
+ password: new TextEncoder().encode(passphrase),
225
+ salt: argonSalt,
226
+ passes: 2,
227
+ memorySize: 19456,
228
+ parallelism: 1,
229
+ tagLength: 32,
230
+ });
231
+
232
+ // tagLength: 32 already matches XChaCha20Poly1305's expected key size
233
+ // HKDF is optional here but included for domain separation.
234
+ const hkdf = new HKDF_SHA256();
235
+ const key = hkdf.derive(rootKey, argonSalt, new TextEncoder().encode('xchacha-v1'), 32);
236
+ hkdf.dispose();
237
+
238
+ const nonce = crypto.getRandomValues(new Uint8Array(24));
239
+ const xc = new XChaCha20Poly1305();
240
+ const ct = xc.encrypt(key, nonce, plaintext);
241
+ xc.dispose();
242
+
243
+ const envelope = { ct, nonce, argonSalt };
244
+
245
+ // ── Decrypt ──────────────────────────────────────────────────────────────────
246
+
247
+ const rootKey2 = argon2id({
248
+ password: new TextEncoder().encode(passphrase),
249
+ salt: envelope.argonSalt,
250
+ passes: 2,
251
+ memorySize: 19456,
252
+ parallelism: 1,
253
+ tagLength: 32,
254
+ });
255
+
256
+ const hkdf2 = new HKDF_SHA256();
257
+ const key2 = hkdf2.derive(rootKey2, envelope.argonSalt, new TextEncoder().encode('xchacha-v1'), 32);
258
+ hkdf2.dispose();
259
+
260
+ const xc2 = new XChaCha20Poly1305();
261
+ const decrypted = xc2.decrypt(key2, envelope.nonce, envelope.ct);
262
+ xc2.dispose();
263
+ ```
264
+
265
+ > [!CAUTION]
266
+ > Never reuse an Argon2id salt across different passphrases or key derivation
267
+ > contexts. Generate a fresh random salt for each new encryption envelope. The
268
+ > salt is not secret — store it in plaintext alongside the ciphertext.
269
+
270
+ ---
271
+
272
+ ## Memory note
273
+
274
+ >[!IMPORTANT]
275
+ > Each call to `loadArgon2idWasm()` instantiates a separate WASM instance. The
276
+ > package's own documentation recommends reloading the module between hashes when
277
+ > the `memorySize` varies significantly, since WASM linear memory is not
278
+ > deallocated between calls. For a single `memorySize` used consistently (the
279
+ > common case), one `await loadArgon2idWasm()` call at startup is correct.
280
+
281
+ ---
282
+
283
+ ## Cross-References
284
+
285
+ - [README.md](./README.md) — project overview and quick-start guide
286
+ - [sha2.md](./sha2.md) — HKDF-SHA256 for key expansion from Argon2id root keys
287
+ - [serpent.md](./serpent.md) — SerpentSeal: Serpent-256 authenticated encryption (pairs with Argon2id-derived keys)
288
+ - [chacha20.md](./chacha20.md) — XChaCha20Poly1305: ChaCha20 authenticated encryption (pairs with Argon2id-derived keys)
289
+ - [utils.md](./utils.md) — `randomBytes` for generating salts, `constantTimeEqual` for hash verification
290
+ - [architecture.md](./architecture.md) — library architecture and design decisions