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
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# leviathan-crypto — AI Assistant Guide
|
|
2
|
+
|
|
3
|
+
This file ships with the package to help AI assistants use this library correctly.
|
|
4
|
+
Full API documentation is in the `docs/` directory alongside this file.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What This Library Is
|
|
9
|
+
|
|
10
|
+
`leviathan-crypto` is a zero-dependency WebAssembly cryptography library for
|
|
11
|
+
TypeScript and JavaScript. All cryptographic computation runs in WASM, outside
|
|
12
|
+
the JavaScript JIT. The TypeScript layer provides the public API — input
|
|
13
|
+
validation, type safety, and ergonomics. It never implements cryptographic
|
|
14
|
+
algorithms itself.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Critical: `init()` is required
|
|
19
|
+
|
|
20
|
+
**No class works before `init()` is called.** Calling any class before its
|
|
21
|
+
module is loaded throws immediately with a clear error. Call `init()` once at
|
|
22
|
+
startup, before any cryptographic operations.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { init, SerpentSeal } from 'leviathan-crypto'
|
|
26
|
+
|
|
27
|
+
await init(['serpent', 'sha2']) // load only the modules you need
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Available modules: `'serpent'`, `'chacha20'`, `'sha2'`, `'sha3'`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Critical: call `dispose()` after use
|
|
35
|
+
|
|
36
|
+
Every class holds WASM memory containing key material. Call `dispose()` when
|
|
37
|
+
done — it zeroes that memory. Not calling `dispose()` leaks key material.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const cipher = new SerpentSeal()
|
|
41
|
+
try {
|
|
42
|
+
return cipher.encrypt(key, plaintext)
|
|
43
|
+
} finally {
|
|
44
|
+
cipher.dispose()
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Critical: `decrypt()` throws on authentication failure — never returns null
|
|
51
|
+
|
|
52
|
+
All AEAD `decrypt()` methods throw if authentication fails. Do not check for a
|
|
53
|
+
null return — catch the exception.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
try {
|
|
57
|
+
const plaintext = seal.decrypt(key, ciphertext)
|
|
58
|
+
} catch {
|
|
59
|
+
// wrong key or tampered data
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Critical: subpath init function names
|
|
66
|
+
|
|
67
|
+
Each subpath export has its own module-specific init function — not `init()`.
|
|
68
|
+
These are only needed for tree-shakeable imports. The root barrel `init()` is
|
|
69
|
+
the normal path.
|
|
70
|
+
|
|
71
|
+
| Subpath | Init function |
|
|
72
|
+
|---------|---------------|
|
|
73
|
+
| `leviathan-crypto/serpent` | `serpentInit()` |
|
|
74
|
+
| `leviathan-crypto/chacha20` | `chacha20Init()` |
|
|
75
|
+
| `leviathan-crypto/sha2` | `sha2Init()` |
|
|
76
|
+
| `leviathan-crypto/sha3` | `sha3Init()` |
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Tree-shakeable — loads only serpent WASM
|
|
80
|
+
import { serpentInit, SerpentSeal } from 'leviathan-crypto/serpent'
|
|
81
|
+
await serpentInit()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Which module does each class require?
|
|
87
|
+
|
|
88
|
+
| Classes | `init()` call |
|
|
89
|
+
|---------|--------------|
|
|
90
|
+
| `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `SerpentStreamEncoder`, `SerpentStreamDecoder`, `Serpent`, `SerpentCtr`, `SerpentCbc` | `init(['serpent', 'sha2'])` |
|
|
91
|
+
| `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool` | `init(['chacha20'])` |
|
|
92
|
+
| `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HKDF_SHA256`, `HKDF_SHA512` | `init(['sha2'])` |
|
|
93
|
+
| `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` | `init(['sha3'])` |
|
|
94
|
+
| `Fortuna` | `init(['serpent', 'sha2'])` |
|
|
95
|
+
|
|
96
|
+
`Argon2id` is a separate subpath: `import { Argon2id } from 'leviathan-crypto/argon2id'`
|
|
97
|
+
It does **not** require `init()` — it uses its own WASM loader.
|
|
98
|
+
`'argon2id'` is **not** a valid module string for `init()`.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Recommended patterns
|
|
103
|
+
|
|
104
|
+
### Authenticated encryption (recommended default)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { init, SerpentSeal, randomBytes } from 'leviathan-crypto'
|
|
108
|
+
|
|
109
|
+
await init(['serpent', 'sha2'])
|
|
110
|
+
|
|
111
|
+
const key = randomBytes(64) // 64-byte key (encKey + macKey)
|
|
112
|
+
const seal = new SerpentSeal()
|
|
113
|
+
const ciphertext = seal.encrypt(key, plaintext) // Serpent-CBC + HMAC-SHA256
|
|
114
|
+
const decrypted = seal.decrypt(key, ciphertext) // throws on tamper
|
|
115
|
+
seal.dispose()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Incremental streaming AEAD
|
|
119
|
+
|
|
120
|
+
Use when you cannot buffer the full message before encrypting.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { init, SerpentStreamSealer, SerpentStreamOpener, randomBytes } from 'leviathan-crypto'
|
|
124
|
+
|
|
125
|
+
await init(['serpent', 'sha2'])
|
|
126
|
+
|
|
127
|
+
const key = randomBytes(64)
|
|
128
|
+
const sealer = new SerpentStreamSealer(key, 65536)
|
|
129
|
+
const header = sealer.header() // send to opener before any chunks
|
|
130
|
+
|
|
131
|
+
const chunk0 = sealer.seal(data0) // exactly chunkSize bytes
|
|
132
|
+
const last = sealer.final(tail) // any size up to chunkSize; wipes key
|
|
133
|
+
|
|
134
|
+
const opener = new SerpentStreamOpener(key, header)
|
|
135
|
+
const pt0 = opener.open(chunk0) // throws on auth failure
|
|
136
|
+
const ptLast = opener.open(last)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Length-prefixed streaming (for files and buffered transports)
|
|
140
|
+
|
|
141
|
+
`SerpentStreamEncoder`/`SerpentStreamDecoder` wrap the sealer/opener with
|
|
142
|
+
`u32be` length-prefixed framing so chunk boundaries are self-delimiting.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { init, SerpentStreamEncoder, SerpentStreamDecoder, randomBytes } from 'leviathan-crypto'
|
|
146
|
+
|
|
147
|
+
await init(['serpent', 'sha2'])
|
|
148
|
+
|
|
149
|
+
const key = randomBytes(64)
|
|
150
|
+
const encoder = new SerpentStreamEncoder(key, 65536)
|
|
151
|
+
const header = encoder.header()
|
|
152
|
+
|
|
153
|
+
const frame0 = encoder.encode(data0) // u32be(len) || sealed chunk
|
|
154
|
+
const last = encoder.encodeFinal(tail)
|
|
155
|
+
|
|
156
|
+
const decoder = new SerpentStreamDecoder(key, header)
|
|
157
|
+
const chunks = decoder.feed(frame0) // returns Uint8Array[], throws on auth failure
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### XChaCha20-Poly1305
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { init, XChaCha20Poly1305, randomBytes } from 'leviathan-crypto'
|
|
164
|
+
|
|
165
|
+
await init(['chacha20'])
|
|
166
|
+
|
|
167
|
+
const aead = new XChaCha20Poly1305()
|
|
168
|
+
const key = randomBytes(32)
|
|
169
|
+
const nonce = randomBytes(24)
|
|
170
|
+
const sealed = aead.encrypt(key, nonce, plaintext, aad?) // ciphertext || tag
|
|
171
|
+
const plaintext = aead.decrypt(key, nonce, sealed, aad?) // throws on tamper
|
|
172
|
+
aead.dispose()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Note: `encrypt()` returns ciphertext with the 16-byte Poly1305 tag appended.
|
|
176
|
+
`decrypt()` expects the same concatenated format — not separate ciphertext and tag.
|
|
177
|
+
|
|
178
|
+
### Hashing
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { init, SHA256, HMAC_SHA256 } from 'leviathan-crypto'
|
|
182
|
+
|
|
183
|
+
await init(['sha2'])
|
|
184
|
+
|
|
185
|
+
const hasher = new SHA256()
|
|
186
|
+
const digest = hasher.hash(data) // returns Uint8Array
|
|
187
|
+
hasher.dispose()
|
|
188
|
+
|
|
189
|
+
const mac = new HMAC_SHA256()
|
|
190
|
+
const tag = mac.hash(key, data)
|
|
191
|
+
mac.dispose()
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### SHAKE (XOF — variable-length output)
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { init, SHAKE128 } from 'leviathan-crypto'
|
|
198
|
+
|
|
199
|
+
await init(['sha3'])
|
|
200
|
+
|
|
201
|
+
const xof = new SHAKE128()
|
|
202
|
+
xof.absorb(data)
|
|
203
|
+
const out1 = xof.squeeze(32) // first 32 bytes of output stream
|
|
204
|
+
const out2 = xof.squeeze(32) // next 32 bytes — contiguous XOF stream
|
|
205
|
+
xof.dispose()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Fortuna CSPRNG
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { init, Fortuna } from 'leviathan-crypto'
|
|
212
|
+
|
|
213
|
+
await init(['serpent', 'sha2'])
|
|
214
|
+
|
|
215
|
+
const fortuna = await Fortuna.create() // static factory — not new Fortuna()
|
|
216
|
+
const bytes = fortuna.get(32)
|
|
217
|
+
fortuna.stop()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## `SerpentCbc` arg order
|
|
223
|
+
|
|
224
|
+
IV is the **second** argument, not the third:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
cipher.encrypt(key, iv, plaintext) // correct
|
|
228
|
+
cipher.decrypt(key, iv, ciphertext) // correct
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`SerpentCbc` is unauthenticated. Always pair with `HMAC_SHA256`
|
|
232
|
+
(Encrypt-then-MAC) or use `SerpentSeal` instead.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Utilities (no `init()` required)
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { hexToBytes, bytesToHex, randomBytes, constantTimeEqual, wipe } from 'leviathan-crypto'
|
|
240
|
+
|
|
241
|
+
// available immediately — no await init() needed
|
|
242
|
+
const key = randomBytes(32)
|
|
243
|
+
const hex = bytesToHex(key)
|
|
244
|
+
const back = hexToBytes(hex)
|
|
245
|
+
const safe = constantTimeEqual(a, b) // constant-time equality — never use ===
|
|
246
|
+
wipe(key) // zero a Uint8Array in place
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Full documentation
|
|
252
|
+
|
|
253
|
+
The complete API reference ships in `docs/` alongside this file:
|
|
254
|
+
|
|
255
|
+
| File | Contents |
|
|
256
|
+
|------|----------|
|
|
257
|
+
| `docs/serpent.md` | `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `SerpentStreamEncoder`, `SerpentStreamDecoder`, `Serpent`, `SerpentCtr`, `SerpentCbc` |
|
|
258
|
+
| `docs/chacha20.md` | `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool` |
|
|
259
|
+
| `docs/sha2.md` | `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HKDF_SHA256`, `HKDF_SHA512` |
|
|
260
|
+
| `docs/sha3.md` | `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` |
|
|
261
|
+
| `docs/fortuna.md` | `Fortuna` CSPRNG |
|
|
262
|
+
| `docs/init.md` | `init()` API, loading modes, subpath imports |
|
|
263
|
+
| `docs/utils.md` | Encoding helpers, `constantTimeEqual`, `wipe`, `randomBytes` |
|
|
264
|
+
| `docs/types.md` | `Hash`, `KeyedHash`, `Blockcipher`, `Streamcipher`, `AEAD` interfaces |
|
|
265
|
+
| `docs/architecture.md` | Module structure, WASM layer, three-tier design |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leviathan Crypto Library
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
[](https://github.com/xero/text0wnz/blob/main/LICENSE)
|
|
2
|
+
[](https://github.com/xero/leviathan-crypto/releases/latest)
|
|
3
|
+
[](https://github.com/xero/leviathan-crypto/)
|
|
4
|
+
[](https://github.com/xero/leviathan-crypto/actions/workflows/test-suite.yml)
|
|
5
|
+
[](https://github.com/xero/leviathan-crypto/wiki)
|
|
6
|
+
|
|
7
|
+
<img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="Leviathan logo" width="400">
|
|
8
|
+
|
|
9
|
+
# Leviathan-Crypto: Serpent-256 & XChaCha20-Poly1305 Cryptography for the Web
|
|
10
|
+
|
|
11
|
+
Serpent-256, the most conservative AES finalist, employs 32 rounds and a
|
|
12
|
+
maximum security margin, built to withstand future cryptanalytic advancements.
|
|
13
|
+
Paired with the streamlined brilliance of ChaCha20-Poly1305, and complemented
|
|
14
|
+
by SHA-2 and SHA-3. Two design philosophies, four cryptographic primitives,
|
|
15
|
+
integrated into one coherent API.
|
|
16
|
+
|
|
17
|
+
**WebAssembly (WASM) serves as the correctness layer.** It features spec-driven and
|
|
18
|
+
vector-verified AssemblyScript implementations of Serpent-256, ChaCha20/Poly1305,
|
|
19
|
+
SHA-2, and SHA-3. Each cryptographic primitive is compiled into its own isolated
|
|
20
|
+
binary, executing outside the JavaScript JIT. This ensures no speculative
|
|
21
|
+
optimization affects key material and eliminates data-dependent timing
|
|
22
|
+
vulnerabilities from table lookups.
|
|
23
|
+
|
|
24
|
+
**TypeScript acts as the ergonomics layer.** Fully typed classes, explicit
|
|
25
|
+
`init()` gates, input validation, and authenticated compositions
|
|
26
|
+
([`SerpentSeal`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentseal),
|
|
27
|
+
[`SerpentStream`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentstream),
|
|
28
|
+
[`SerpentStreamSealer`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentstreamsealer--serpentstreamopener))
|
|
29
|
+
ensure primitives are connected correctly, simplifying development and ensuring
|
|
30
|
+
correctness. Advanced users retain the ability to directly access the raw block
|
|
31
|
+
cipher classes.
|
|
32
|
+
|
|
33
|
+
## Why Serpent-256
|
|
34
|
+
|
|
35
|
+
Serpent-256, an AES finalist, received more first-place security votes than
|
|
36
|
+
Rijndael from the NIST evaluation committee. It was designed with a larger
|
|
37
|
+
security margin: 32 rounds compared to AES's 10, 12, or 14.
|
|
38
|
+
|
|
39
|
+
While Serpent won on security margin, AES (Rijndael) ultimately won the
|
|
40
|
+
competition due to its performance. Rijndael was selected because speed
|
|
41
|
+
was paramount for the hardware and embedded targets NIST was optimizing for
|
|
42
|
+
in 2001. However, for software running on modern hardware where milliseconds
|
|
43
|
+
of encryption latency are acceptable, this tradeoff is no longer as relevant.
|
|
44
|
+
|
|
45
|
+
**Security Margin.** Serpent has been a target of cryptanalytic research
|
|
46
|
+
since the AES competition. The current state-of-the-art is as follows:
|
|
47
|
+
|
|
48
|
+
- **Best known reduced-round attack:**
|
|
49
|
+
- multidimensional linear cryptanalysis reaching 12 of 32 rounds (Nguyen,
|
|
50
|
+
Wu & Wang, ACISP 2011), less than half the full cipher, requiring 2¹¹⁸
|
|
51
|
+
known plaintexts and 2²²⁸·⁸ time.
|
|
52
|
+
- Multidimensional linear cryptanalysis reaches 12 of 32 rounds (Nguyen,
|
|
53
|
+
Wu & Wang, ACISP 2011), less than half the full cipher. This requires
|
|
54
|
+
2¹¹⁸ known plaintexts and 2²²⁸·⁸ time. [source](https://personal.ntu.edu.sg/wuhj/research/publications/2011_ACISP_MLC.pdf) & [mirror](https://archive.is/6pwMM)
|
|
55
|
+
- **Best known full-round attack:**
|
|
56
|
+
- biclique cryptanalysis of full 32-round Serpent-256 (de Carvalho & Kowada,
|
|
57
|
+
SBSeg 2020), time complexity 2²⁵⁵·²¹, only 0.79 bits below the 256-bit
|
|
58
|
+
brute-force ceiling of 2²⁵⁶, and requires 2⁸⁸ chosen ciphertexts, making
|
|
59
|
+
it strictly less practical than brute force. For comparison, the analogous
|
|
60
|
+
biclique attack on full-round AES-256 (Bogdanov et al., 2011) reaches
|
|
61
|
+
2²⁵⁴·⁴. Serpent-256 is marginally harder to attack by this method than AES-256.
|
|
62
|
+
- Biclique cryptanalysis of full 32-round Serpent-256 (de Carvalho & Kowada,
|
|
63
|
+
SBSeg 2020) has a time complexity of 2²⁵⁵·²¹, only 0.79 bits below the 256-bit
|
|
64
|
+
brute-force ceiling of 2²⁵⁶. It requires 2⁸⁸ chosen ciphertexts, making it
|
|
65
|
+
strictly less practical than brute force. For comparison, the analogous biclique
|
|
66
|
+
attack on full-round AES-256 (Bogdanov et al., 2011) reaches 2²⁵⁴·⁴.
|
|
67
|
+
Serpent-256 is marginally harder to attack by this method than AES-256. [source](https://sol.sbc.org.br/index.php/sbseg/article/view/19225/19054) & [mirror](https://archive.is/ZZjrT)
|
|
68
|
+
- Our independent research improved the published result by
|
|
69
|
+
−0.20 bits through systematic search over v position, biclique nibble
|
|
70
|
+
selection, and nabla pair. the best configuration (K31/K17, delta nibble 0,
|
|
71
|
+
nabla nibble 10, v = state 66 nibbles 8+9) achieves 2²⁵⁵·¹⁹ with only 2⁴
|
|
72
|
+
chosen ciphertexts. The K17 nabla result is a new finding not present in
|
|
73
|
+
the published papers.
|
|
74
|
+
- Our independent research improved the published result by −0.20 bits through
|
|
75
|
+
systematic search over v position, biclique nibble selection, and nabla pair.
|
|
76
|
+
The best configuration (K31/K17, delta nibble 0, nabla nibble 10, v = state
|
|
77
|
+
66 nibbles 8+9) achieves 2²⁵⁵·¹⁹ with only 2⁴ chosen ciphertexts. The K17 nabla
|
|
78
|
+
result is a new finding not present in the published papers. [`biclique_research`](https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md)
|
|
79
|
+
|
|
80
|
+
See: [`serpent_audit.md`](https://github.com/xero/leviathan-crypto/wiki/serpent_audit)
|
|
81
|
+
for the full analysis.
|
|
82
|
+
|
|
83
|
+
**Implementation.** Implementation: Serpent's S-boxes are implemented as Boolean gate
|
|
84
|
+
circuits, meaning there are no table lookups, data-dependent memory access, or
|
|
85
|
+
data-dependent branches. Every bit is processed unconditionally on every block.
|
|
86
|
+
This approach provides the most timing-safe cipher implementation available in a
|
|
87
|
+
JavaScript runtime, where JIT optimization can otherwise introduce observable
|
|
88
|
+
timing variations.
|
|
89
|
+
|
|
90
|
+
**Key Size:** The default API only supports 256-bit keys. The absence of 128 or
|
|
91
|
+
192-bit variants mitigates the risk of key-size downgrade attacks.
|
|
92
|
+
|
|
93
|
+
## Primitives
|
|
94
|
+
|
|
95
|
+
| Classes | Module | Auth | Notes |
|
|
96
|
+
|---------|--------|------|-------|
|
|
97
|
+
| `SerpentSeal` | `serpent`, `sha2` | **Yes** | Authenticated encryption: Serpent-CBC + HMAC-SHA256. Recommended for most use cases. |
|
|
98
|
+
| `SerpentStream`, `SerpentStreamPool` | `serpent`, `sha2` | **Yes** | Chunked one-shot AEAD for large payloads. Pool variant parallelises across workers. |
|
|
99
|
+
| `SerpentStreamSealer`, `SerpentStreamOpener` | `serpent`, `sha2` | **Yes** | Incremental streaming AEAD: seal and open one chunk at a time without buffering the full message. |
|
|
100
|
+
| `SerpentStreamEncoder`, `SerpentStreamDecoder` | `serpent`, `sha2` | **Yes** | Length-prefixed framing over SerpentStreamSealer/Opener for flat byte streams (files, buffered TCP). |
|
|
101
|
+
| `Serpent`, `SerpentCtr`, `SerpentCbc` | `serpent` | **No** | Raw ECB, CTR, CBC modes. Pair with HMAC-SHA256 for authentication. |
|
|
102
|
+
| `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305` | `chacha20` | Yes (AEAD) | RFC 8439 |
|
|
103
|
+
| `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512` | `sha2` | -- | FIPS 180-4, RFC 2104 |
|
|
104
|
+
| `HKDF_SHA256`, `HKDF_SHA512` | `sha2` | -- | Key derivation: RFC 5869. Extract-and-expand over HMAC. |
|
|
105
|
+
| `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` | `sha3` | -- | FIPS 202 |
|
|
106
|
+
| `Fortuna` | `fortuna` | -- | Fortuna CSPRNG (Ferguson & Schneier). Requires `Fortuna.create()`. |
|
|
107
|
+
|
|
108
|
+
>[!IMPORTANT]
|
|
109
|
+
> All cryptographic computation runs in WASM (AssemblyScript), isolated outside the JavaScript JIT.
|
|
110
|
+
> The TypeScript layer provides the public API with input validation, type safety, and developer ergonomics.
|
|
111
|
+
|
|
112
|
+
## Quick Start
|
|
113
|
+
|
|
114
|
+
### Authenticated encryption with Serpent (recommended)
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { init, SerpentSeal, randomBytes } from 'leviathan-crypto'
|
|
118
|
+
|
|
119
|
+
await init(['serpent', 'sha2'])
|
|
120
|
+
|
|
121
|
+
const key = randomBytes(64) // 64-byte key (encKey + macKey)
|
|
122
|
+
|
|
123
|
+
const seal = new SerpentSeal()
|
|
124
|
+
|
|
125
|
+
// Encrypt and authenticate
|
|
126
|
+
const ciphertext = seal.encrypt(key, plaintext)
|
|
127
|
+
|
|
128
|
+
// Decrypt and verify (throws on tamper)
|
|
129
|
+
const decrypted = seal.decrypt(key, ciphertext)
|
|
130
|
+
|
|
131
|
+
seal.dispose()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Incremental streaming AEAD
|
|
135
|
+
|
|
136
|
+
Use `SerpentStreamSealer` when data arrives chunk by chunk and you cannot
|
|
137
|
+
buffer the full message before encrypting.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { init, SerpentStreamSealer, SerpentStreamOpener, randomBytes } from 'leviathan-crypto'
|
|
141
|
+
|
|
142
|
+
await init(['serpent', 'sha2'])
|
|
143
|
+
|
|
144
|
+
const key = randomBytes(64)
|
|
145
|
+
|
|
146
|
+
// Seal side
|
|
147
|
+
const sealer = new SerpentStreamSealer(key, 65536)
|
|
148
|
+
const header = sealer.header() // transmit to opener before any chunks
|
|
149
|
+
|
|
150
|
+
const chunk0 = sealer.seal(data0) // exactly chunkSize bytes
|
|
151
|
+
const chunk1 = sealer.seal(data1)
|
|
152
|
+
const last = sealer.final(tail) // any size up to chunkSize; wipes key on return
|
|
153
|
+
|
|
154
|
+
// Open side
|
|
155
|
+
const opener = new SerpentStreamOpener(key, header)
|
|
156
|
+
|
|
157
|
+
const pt0 = opener.open(chunk0)
|
|
158
|
+
const pt1 = opener.open(chunk1)
|
|
159
|
+
const ptN = opener.open(last) // detects final chunk; wipes key on return
|
|
160
|
+
|
|
161
|
+
// Reordering, truncation, and cross-stream splicing all throw on open()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Large payload chunking
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { init, SerpentStream, randomBytes } from 'leviathan-crypto'
|
|
168
|
+
|
|
169
|
+
await init(['serpent', 'sha2'])
|
|
170
|
+
|
|
171
|
+
const key = randomBytes(32) // 32-byte key (HKDF handles expansion internally)
|
|
172
|
+
|
|
173
|
+
const stream = new SerpentStream()
|
|
174
|
+
const ciphertext = stream.seal(key, largePlaintext) // default 64KB chunks
|
|
175
|
+
const decrypted = stream.open(key, ciphertext)
|
|
176
|
+
|
|
177
|
+
stream.dispose()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Fortuna CSPRNG
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { init, Fortuna } from 'leviathan-crypto'
|
|
184
|
+
|
|
185
|
+
await init(['serpent', 'sha2'])
|
|
186
|
+
|
|
187
|
+
const fortuna = await Fortuna.create()
|
|
188
|
+
const random = fortuna.get(32) // 32 random bytes
|
|
189
|
+
|
|
190
|
+
fortuna.stop()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Hashing with SHA-3
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { init, SHA3_256 } from 'leviathan-crypto'
|
|
197
|
+
|
|
198
|
+
await init(['sha3'])
|
|
199
|
+
|
|
200
|
+
const hasher = new SHA3_256()
|
|
201
|
+
const digest = hasher.hash(new TextEncoder().encode('hello'))
|
|
202
|
+
// digest is a 32-byte Uint8Array
|
|
203
|
+
|
|
204
|
+
hasher.dispose()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Utilities
|
|
208
|
+
|
|
209
|
+
These helpers are available immediately on import: no `init()` required.
|
|
210
|
+
|
|
211
|
+
| Function | Description |
|
|
212
|
+
|----------|-------------|
|
|
213
|
+
| `hexToBytes(hex)` | Hex string to `Uint8Array` (accepts uppercase, `0x` prefix) |
|
|
214
|
+
| `bytesToHex(bytes)` | `Uint8Array` to lowercase hex string |
|
|
215
|
+
| `utf8ToBytes(str)` | UTF-8 string to `Uint8Array` |
|
|
216
|
+
| `bytesToUtf8(bytes)` | `Uint8Array` to UTF-8 string |
|
|
217
|
+
| `base64ToBytes(b64)` | Base64/base64url string to `Uint8Array` (undefined on invalid) |
|
|
218
|
+
| `bytesToBase64(bytes, url?)` | `Uint8Array` to base64 string (url=true for base64url) |
|
|
219
|
+
| `constantTimeEqual(a, b)` | Constant-time byte comparison (XOR-accumulate) |
|
|
220
|
+
| `wipe(data)` | Zero a typed array in place |
|
|
221
|
+
| `xor(a, b)` | XOR two equal-length `Uint8Array`s |
|
|
222
|
+
| `concat(a, b)` | Concatenate two `Uint8Array`s |
|
|
223
|
+
| `randomBytes(n)` | Cryptographically secure random bytes via Web Crypto |
|
|
224
|
+
|
|
225
|
+
## Authentication Warning
|
|
226
|
+
|
|
227
|
+
`SerpentCtr` and `SerpentCbc` are **unauthenticated** cipher modes. They provide
|
|
228
|
+
confidentiality but not integrity or authenticity. An attacker can modify
|
|
229
|
+
ciphertext without detection.
|
|
230
|
+
|
|
231
|
+
>[!TIP]
|
|
232
|
+
> **For authenticated Serpent encryption:** use [`SerpentSeal`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentseal) or [`SerpentStreamSealer`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentstreamsealer--serpentstreamopener)
|
|
233
|
+
>
|
|
234
|
+
> **Using Serpent CBC/CTR directly:** pair with `HMAC_SHA256` using the Encrypt-then-MAC pattern
|
|
235
|
+
|
|
236
|
+
>[!NOTE]
|
|
237
|
+
> **[`SerpentStream`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentstream) and [`SerpentStreamSealer`](https://github.com/xero/leviathan-crypto/wiki/serpent#serpentstreamsealer--serpentstreamopener)
|
|
238
|
+
> inherently satisfy the Cryptographic Doom Principle.** Message Authentication Code (MAC)
|
|
239
|
+
> verification is the mandatory check on every `open()` call; decryption is impossible until
|
|
240
|
+
> this verification succeeds. Per-chunk HKDF key derivation, using position-bound info,
|
|
241
|
+
> extends this protection to stream integrity. Reordering, truncation, and cross-stream
|
|
242
|
+
> substitution are all detected at the MAC layer, preventing any plaintext from being
|
|
243
|
+
> produced in such cases. [Full analysis.](https://github.com/xero/leviathan-crypto/wiki/serpent_audit#24-serpentstream-encrypt-then-mac-and-the-cryptographic-doom-principle)
|
|
244
|
+
|
|
245
|
+
## Installation
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# use bun
|
|
249
|
+
bun i leviathan-crypto
|
|
250
|
+
# or npm
|
|
251
|
+
npm install leviathan-crypto
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Loading Modes
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Embedded (default): zero-config, base64-encoded WASM inline
|
|
258
|
+
await init(['serpent', 'sha3'])
|
|
259
|
+
|
|
260
|
+
// Streaming: uses instantiateStreaming for performance
|
|
261
|
+
await init(['serpent'], 'streaming', { wasmUrl: '/assets/wasm/' })
|
|
262
|
+
|
|
263
|
+
// Manual: provide your own binary
|
|
264
|
+
await init(['serpent'], 'manual', { wasmBinary: { serpent: myBuffer } })
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Documentation
|
|
268
|
+
|
|
269
|
+
**Full API documentation:** [./docs](./docs/README.md)
|
|
270
|
+
|
|
271
|
+
| Module | Description |
|
|
272
|
+
|--------|-------------|
|
|
273
|
+
| [serpent.md](./docs/serpent.md) | Serpent-256 TypeScript API (`SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc`) |
|
|
274
|
+
| [asm_serpent.md](./docs/asm_serpent.md) | Serpent-256 WASM implementation (bitslice S-boxes, key schedule, CTR/CBC) |
|
|
275
|
+
| [chacha20.md](./docs/chacha20.md) | ChaCha20/Poly1305 TypeScript API (`ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`) |
|
|
276
|
+
| [asm_chacha.md](./docs/asm_chacha.md) | ChaCha20/Poly1305 WASM implementation (quarter-round, HChaCha20) |
|
|
277
|
+
| [sha2.md](./docs/sha2.md) | SHA-2 TypeScript API (`SHA256`, `SHA512`, `SHA384`, `HMAC_SHA256`, `HMAC_SHA512`, `HMAC_SHA384`) |
|
|
278
|
+
| [asm_sha2.md](./docs/asm_sha2.md) | SHA-2 WASM implementation (compression functions, HMAC) |
|
|
279
|
+
| [sha3.md](./docs/sha3.md) | SHA-3 TypeScript API (`SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256`) |
|
|
280
|
+
| [asm_sha3.md](./docs/asm_sha3.md) | SHA-3 WASM implementation (Keccak-f[1600], sponge construction) |
|
|
281
|
+
| [fortuna.md](./docs/fortuna.md) | Fortuna CSPRNG (forward secrecy, 32 entropy pools) |
|
|
282
|
+
| [init.md](./docs/init.md) | `init()` API and WASM loading modes |
|
|
283
|
+
| [utils.md](./docs/utils.md) | Encoding helpers, `constantTimeEqual`, `wipe`, `randomBytes` |
|
|
284
|
+
| [types.md](./docs/types.md) | TypeScript interfaces (`Hash`, `KeyedHash`, `Blockcipher`, `Streamcipher`, `AEAD`) |
|
|
285
|
+
| [architecture.md](./docs/architecture.md.md) | Architecture overview, build pipeline, module relationships |
|
|
286
|
+
| [test-suite.md](./test-suite.md) | Test suite structure, vector corpus, gate discipline |
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
leviathan is written under the [MIT license](http://www.opensource.org/licenses/MIT).
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
██ ▐█████ ██ ▐█▌ ▄█▌ ███▌ ▀███████▀▄██▌ ▐█▌ ███▌ ██▌ ▓▓
|
|
293
|
+
▐█▌ ▐█▌ ▓█ ▐█▌ ▓██ ▐█▌██ ▐█▌ ███ ██▌ ▐█▌██ ▓██ ██
|
|
294
|
+
██▌ ░███ ▐█▌ ██ ▀▀ ██ ▐█▌ ██ ▐██▌ █▓ ▓█ ▐█▌ ▐███▌ █▓
|
|
295
|
+
██ ██ ▐█▌ █▓ ▐██ ▐█▌ █▓ ██ ▐██▄▄ ▐█▌ ▐█▌ ██ ▐█▌██ ▐█▌
|
|
296
|
+
▐█▌ ▐█▌ ██ ▐█▌ ██ ██ ██ ▐█▌ ██▀▀████▌ ██ ██ ██ ▐█▌▐█▌
|
|
297
|
+
▐▒▌ ▐▒▌ ▐▒▌ ██ ▒█ ██▀▀▀██▌ ▐▒▌ ▒█ █▓░ ▒█▀▀▀██▌ ▒█ ██▐█
|
|
298
|
+
█▓ ▄▄▓█ █▓ ▄▄▓█ ▓▓ ▐▓▌ ▐▓▌ ▐█▌ ▐▒▌ █▓ ▐▓▌ ▐▓█ ▐▓▌ ▐▒▌▐▓▌ ▐███
|
|
299
|
+
▓██▀▀ ▓██▀▀ ▓█▓█ ▐█▌ ▐█▌ ▐▓▌ ▓█ ▐█▌ ▐█▓ ▐█▌ ▐▓▌▐█▌ ██▓
|
|
300
|
+
▓█ ▄▄▄▄▄▄▄▄▄▄ ▀▀ ▐█▌▌▌
|
|
301
|
+
▄████████████████████▄▄
|
|
302
|
+
▄██████████████████████ ▀████▄
|
|
303
|
+
▄█████████▀▀▀ ▀███████▄▄███████▌
|
|
304
|
+
▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
|
|
305
|
+
████████ ███▀▀ ████▀ █▀ █▀
|
|
306
|
+
███████▌ ▀██▀ ██
|
|
307
|
+
███████ ▀███ ▀██ ▀█▄
|
|
308
|
+
▀██████ ▄▄██ ▀▀ ██▄
|
|
309
|
+
▀█████▄ ▄██▄ ▄▀▄▀
|
|
310
|
+
▀████▄ ▄██▄
|
|
311
|
+
▐████ ▐███
|
|
312
|
+
▄▄██████████ ▐███ ▄▄
|
|
313
|
+
▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
|
|
314
|
+
▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███
|
|
315
|
+
▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀
|
|
316
|
+
████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄
|
|
317
|
+
█████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄███▀
|
|
318
|
+
▀██████▀ ▀████▄▄▄████▀
|
|
319
|
+
▀█████▀
|
|
320
|
+
|
|
321
|
+
Serpent256 & Xchacha20-Poly1305 Cryptography for the Web
|
|
322
|
+
```
|