leviathan-crypto 1.2.0 → 1.3.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 +19 -17
- package/README.md +128 -63
- package/SECURITY.md +2 -1
- package/dist/docs/serpent.md +38 -4
- package/dist/docs/utils.md +21 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/serpent/index.d.ts +0 -1
- package/dist/serpent/index.js +0 -2
- package/dist/serpent/stream-sealer.d.ts +18 -2
- package/dist/serpent/stream-sealer.js +122 -4
- package/package.json +1 -1
- package/dist/serpent/stream-encoder.d.ts +0 -20
- package/dist/serpent/stream-encoder.js +0 -167
package/CLAUDE.md
CHANGED
|
@@ -87,16 +87,12 @@ await serpentInit()
|
|
|
87
87
|
|
|
88
88
|
| Classes | `init()` call |
|
|
89
89
|
|---------|--------------|
|
|
90
|
-
| `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `
|
|
90
|
+
| `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc` | `init(['serpent', 'sha2'])` |
|
|
91
91
|
| `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool` | `init(['chacha20'])` |
|
|
92
92
|
| `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HKDF_SHA256`, `HKDF_SHA512` | `init(['sha2'])` |
|
|
93
93
|
| `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` | `init(['sha3'])` |
|
|
94
94
|
| `Fortuna` | `init(['serpent', 'sha2'])` |
|
|
95
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
96
|
---
|
|
101
97
|
|
|
102
98
|
## Recommended patterns
|
|
@@ -138,23 +134,24 @@ const ptLast = opener.open(last)
|
|
|
138
134
|
|
|
139
135
|
### Length-prefixed streaming (for files and buffered transports)
|
|
140
136
|
|
|
141
|
-
`
|
|
142
|
-
`u32be` length-prefixed framing
|
|
137
|
+
Pass `{ framed: true }` to `SerpentStreamSealer`/`SerpentStreamOpener` for self-delimiting
|
|
138
|
+
`u32be` length-prefixed framing. Use when chunks will be concatenated into a flat byte
|
|
139
|
+
stream. Omit when the transport frames messages itself (WebSocket, IPC).
|
|
143
140
|
|
|
144
141
|
```typescript
|
|
145
|
-
import { init,
|
|
142
|
+
import { init, SerpentStreamSealer, SerpentStreamOpener, randomBytes } from 'leviathan-crypto'
|
|
146
143
|
|
|
147
144
|
await init(['serpent', 'sha2'])
|
|
148
145
|
|
|
149
|
-
const key
|
|
150
|
-
const
|
|
151
|
-
const header
|
|
146
|
+
const key = randomBytes(64)
|
|
147
|
+
const sealer = new SerpentStreamSealer(key, 65536, { framed: true })
|
|
148
|
+
const header = sealer.header()
|
|
152
149
|
|
|
153
|
-
const frame0
|
|
154
|
-
const last
|
|
150
|
+
const frame0 = sealer.seal(data0) // u32be(len) || sealed chunk
|
|
151
|
+
const last = sealer.final(tail)
|
|
155
152
|
|
|
156
|
-
const
|
|
157
|
-
const chunks
|
|
153
|
+
const opener = new SerpentStreamOpener(key, header, { framed: true })
|
|
154
|
+
const chunks = opener.feed(frame0) // Uint8Array[] — throws on auth failure
|
|
158
155
|
```
|
|
159
156
|
|
|
160
157
|
### XChaCha20-Poly1305
|
|
@@ -236,7 +233,7 @@ cipher.decrypt(key, iv, ciphertext) // correct
|
|
|
236
233
|
## Utilities (no `init()` required)
|
|
237
234
|
|
|
238
235
|
```typescript
|
|
239
|
-
import { hexToBytes, bytesToHex, randomBytes, constantTimeEqual, wipe } from 'leviathan-crypto'
|
|
236
|
+
import { hexToBytes, bytesToHex, randomBytes, constantTimeEqual, wipe, hasSIMD } from 'leviathan-crypto'
|
|
240
237
|
|
|
241
238
|
// available immediately — no await init() needed
|
|
242
239
|
const key = randomBytes(32)
|
|
@@ -246,6 +243,11 @@ const safe = constantTimeEqual(a, b) // constant-time equality — never use =
|
|
|
246
243
|
wipe(key) // zero a Uint8Array in place
|
|
247
244
|
```
|
|
248
245
|
|
|
246
|
+
`hasSIMD()` returns `true` if the runtime supports WebAssembly SIMD. It is used
|
|
247
|
+
internally — you do not need to call it. SIMD acceleration is fully transparent:
|
|
248
|
+
`SerpentCtr.encryptChunk`, `SerpentCbc.decrypt`, and `ChaCha20.encryptChunk` all
|
|
249
|
+
auto-dispatch to the faster 4-wide SIMD path when available, with no API change.
|
|
250
|
+
|
|
249
251
|
---
|
|
250
252
|
|
|
251
253
|
## Full documentation
|
|
@@ -254,7 +256,7 @@ The complete API reference ships in `docs/` alongside this file:
|
|
|
254
256
|
|
|
255
257
|
| File | Contents |
|
|
256
258
|
|------|----------|
|
|
257
|
-
| `docs/serpent.md` | `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `
|
|
259
|
+
| `docs/serpent.md` | `SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc` |
|
|
258
260
|
| `docs/chacha20.md` | `ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool` |
|
|
259
261
|
| `docs/sha2.md` | `SHA256`, `SHA384`, `SHA512`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HKDF_SHA256`, `HKDF_SHA512` |
|
|
260
262
|
| `docs/sha3.md` | `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256` |
|
package/README.md
CHANGED
|
@@ -8,26 +8,62 @@
|
|
|
8
8
|
|
|
9
9
|
> Web cryptography built on Serpent-256 paranoia and XChaCha20-Poly1305 elegance.
|
|
10
10
|
|
|
11
|
-
**Serpent-256 is the cipher for those who distrust consensus.** In 2001, when
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
**Serpent-256 is the cipher for those who distrust consensus.** In 2001, when
|
|
12
|
+
NIST selected AES, Serpent actually received more first-place security votes
|
|
13
|
+
from the evaluation committee. However, it lost because the competition also
|
|
14
|
+
considered performance on hardware embedded systems, which are no longer
|
|
15
|
+
representative of the environments for which we develop software. Serpent's
|
|
16
|
+
designers made no compromises: thirty-two rounds, S-boxes implemented using
|
|
17
|
+
pure Boolean logic gates without table lookups, and every bit processed for
|
|
18
|
+
each block. You use Serpent not because a committee recommended it, but because
|
|
19
|
+
you trust the cryptanalysis. The current best attack on the full
|
|
20
|
+
thirty-two-round Serpent-256 achieves 2²⁵⁵·¹⁹ — less than one bit below the
|
|
21
|
+
brute-force ceiling, and strictly impractical. This includes our own
|
|
22
|
+
independent research, which improved upon the published result. See
|
|
23
|
+
[`serpent_audit.md`](https://github.com/xero/leviathan-crypto/wiki/serpent_audit).
|
|
24
|
+
|
|
25
|
+
**XChaCha20-Poly1305 is the cipher for those who appreciate design that has
|
|
26
|
+
nothing to hide.** Daniel Bernstein built ChaCha20 as a twenty-round ARX
|
|
27
|
+
construction: add, rotate, and XOR, in a precise choreography that simply
|
|
28
|
+
doesn't have the attack surface that table-based ciphers do. It has no S-boxes,
|
|
29
|
+
no cache-timing leakage, and requires no hardware acceleration to be fast.
|
|
30
|
+
Poly1305 adds a final layer of security: a one-time authenticator with an
|
|
31
|
+
unconditional forgery bound, mathematically guaranteed regardless of attacker
|
|
32
|
+
compute power. XChaCha20-Poly1305 is the construction you reach for when you
|
|
33
|
+
want an AEAD whose security proof you can actually read without a PhD. See
|
|
34
|
+
[`chacha_audit.md`](https://github.com/xero/leviathan-crypto/wiki/chacha_audit).
|
|
35
|
+
|
|
36
|
+
The tension between these two approaches constitutes the library's core
|
|
37
|
+
identity. Serpent embodies defiance, ChaCha embodies elegance, yet both arrive
|
|
38
|
+
at the same place: constant-time, side-channel resistant implementations,
|
|
39
|
+
independently audited against their specifications. They represent two design
|
|
40
|
+
philosophies that do not agree on anything, except the answer.
|
|
41
|
+
|
|
42
|
+
**WebAssembly provides a correctness layer.** Each primitive compiles into its
|
|
43
|
+
own isolated binary, executing outside the JavaScript JIT. This prevents
|
|
44
|
+
speculative optimization from affecting key material and ensures that
|
|
45
|
+
data-dependent timing vulnerabilities do not cross the boundary.
|
|
46
|
+
|
|
47
|
+
**TypeScript acts as the ergonomics layer.** Fully typed classes, explicit
|
|
48
|
+
`init()` gates, input validation, and authenticated compositions ensure
|
|
49
|
+
primitives are connected correctly.
|
|
20
50
|
|
|
21
51
|
---
|
|
22
52
|
|
|
23
53
|
#### **Zero Dependencies.**
|
|
24
|
-
|
|
54
|
+
|
|
55
|
+
With no npm dependency graph to audit, the supply chain attack surface is
|
|
56
|
+
eliminated.
|
|
25
57
|
|
|
26
58
|
#### **Tree-shakeable.**
|
|
27
|
-
|
|
59
|
+
|
|
60
|
+
Import only the cipher(s) you intend to use. Subpath exports allow bundlers to
|
|
61
|
+
exclude everything else.
|
|
28
62
|
|
|
29
63
|
#### **Side-effect Free.**
|
|
30
|
-
|
|
64
|
+
|
|
65
|
+
Nothing runs upon import. Initialization via `init()` is explicit and
|
|
66
|
+
asynchronous.
|
|
31
67
|
|
|
32
68
|
|
|
33
69
|
## Installation
|
|
@@ -40,7 +76,10 @@ npm install leviathan-crypto
|
|
|
40
76
|
```
|
|
41
77
|
|
|
42
78
|
> [!NOTE]
|
|
43
|
-
> The Serpent and ChaCha20 modules require a runtime with WebAssembly SIMD
|
|
79
|
+
> The Serpent and ChaCha20 modules require a runtime with WebAssembly SIMD
|
|
80
|
+
> support. This has been a feature of all major browsers and runtimes since
|
|
81
|
+
> 2021. All other primitives (SHA-2, SHA-3, Poly1305) run on any WASM-capable
|
|
82
|
+
> runtime.
|
|
44
83
|
|
|
45
84
|
---
|
|
46
85
|
|
|
@@ -48,15 +87,30 @@ npm install leviathan-crypto
|
|
|
48
87
|
|
|
49
88
|
**`lvthn-web`** [ [demo](https://leviathan.3xi.club/web) · [source](https://github.com/xero/leviathan-demos/tree/main/lvthn-web) · [readme](https://github.com/xero/leviathan-demos/blob/main/lvthn-web/README.md) ]
|
|
50
89
|
|
|
51
|
-
A browser encryption tool in a single, self-contained HTML file. Encrypt text
|
|
90
|
+
A browser encryption tool in a single, self-contained HTML file. Encrypt text
|
|
91
|
+
or files using Serpent-256-CBC and Argon2id key derivation, then share the
|
|
92
|
+
armored output. No server, installation, or network connection required after
|
|
93
|
+
initial load. The code is written to be read. The Encrypt-then-MAC
|
|
94
|
+
construction, HMAC input (header with HMAC field zeroed + ciphertext), and
|
|
95
|
+
Argon2id parameters are all intentional examples worth reading.
|
|
52
96
|
|
|
53
97
|
**`lvthn-chat`** [ [demo](https://leviathan.3xi.club/chat) · [source](https://github.com/xero/leviathan-demos/tree/main/lvthn-chat) · [readme](https://github.com/xero/leviathan-demos/blob/main/lvthn-chat/README.md) ]
|
|
54
98
|
|
|
55
|
-
End-to-end encrypted chat featuring two-party messaging over X25519 key
|
|
99
|
+
End-to-end encrypted chat featuring two-party messaging over X25519 key
|
|
100
|
+
exchange and XChaCha20-Poly1305 message encryption. The relay server functions
|
|
101
|
+
as a dumb WebSocket pipe that never sees plaintext. Each message incorporates
|
|
102
|
+
sequence numbers, which allows the system to detect and reject replayed
|
|
103
|
+
messages from an attacker. The demo deconstructs the protocol step by step,
|
|
104
|
+
with visual feedback for both injection and replays.
|
|
56
105
|
|
|
57
106
|
**`lvthn-cli`** [ [npm](https://www.npmjs.com/package/lvthn) · [source](https://github.com/xero/leviathan-demos/tree/main/lvthn-cli) · [readme](https://github.com/xero/leviathan-demos/blob/main/lvthn-cli/README.md) ]
|
|
58
107
|
|
|
59
|
-
File encryption CLI. Supports both Serpent-256 and XChaCha20-Poly1305,
|
|
108
|
+
File encryption CLI. Supports both Serpent-256 and XChaCha20-Poly1305,
|
|
109
|
+
selectable via the `--cipher` flag. A single keyfile is compatible with both
|
|
110
|
+
ciphers; the header byte determines decryption automatically. Encryption and
|
|
111
|
+
decryption distribute 64KB chunks across a worker pool sized to
|
|
112
|
+
hardwareConcurrency. Each worker owns an isolated WASM instance with no shared
|
|
113
|
+
memory between workers.
|
|
60
114
|
|
|
61
115
|
```sh
|
|
62
116
|
bun i -g lvthn # or npm slow mode
|
|
@@ -69,27 +123,33 @@ cat secret.txt | lvthn encrypt -k my.key --armor > secret.enc
|
|
|
69
123
|
|
|
70
124
|
## Primitives
|
|
71
125
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
|
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
126
|
+
| Class | Module | Auth | Notes |
|
|
127
|
+
| ----------------------------------------------------------- | ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
128
|
+
| **Authenticated encryption** | | | |
|
|
129
|
+
| `SerpentSeal` | `serpent`, `sha2` | **Yes** | Serpent-CBC + HMAC-SHA256. Recommended default for most use cases. 64-byte key. |
|
|
130
|
+
| `SerpentStream`, `SerpentStreamPool` | `serpent`, `sha2` | **Yes** | Chunked one-shot AEAD for large payloads. Pool variant parallelises across workers. 32-byte key. |
|
|
131
|
+
| `SerpentStreamSealer`, `SerpentStreamOpener` | `serpent`, `sha2` | **Yes** | Incremental streaming AEAD: seal/open one chunk at a time. Pass `{ framed: true }` for self-delimiting `u32be` length-prefix framing. 64-byte key. |
|
|
132
|
+
| `XChaCha20Poly1305` | `chacha20` | **Yes** | XChaCha20-Poly1305 AEAD. Recommended when you want a simpler API or a 192-bit nonce safe for random generation. 32-byte key. |
|
|
133
|
+
| `XChaCha20Poly1305Pool` | `chacha20` | **Yes** | Worker-pool wrapper for `XChaCha20Poly1305`. Parallelises encryption across isolated WASM instances. |
|
|
134
|
+
| `ChaCha20Poly1305` | `chacha20` | **Yes** | ChaCha20-Poly1305 AEAD — RFC 8439. 12-byte nonce; prefer `XChaCha20Poly1305` unless you need RFC 8439 exact compliance. |
|
|
135
|
+
| **Unauthenticated primitives** _pair with HMAC or use AEAD_ | | | |
|
|
136
|
+
| `Serpent` | `serpent` | **No** | Serpent-256 ECB block cipher. Single-block encrypt/decrypt. |
|
|
137
|
+
| `SerpentCtr` | `serpent` | **No** | Serpent-256 CTR mode stream cipher. Requires `{ dangerUnauthenticated: true }`. |
|
|
138
|
+
| `SerpentCbc` | `serpent` | **No** | Serpent-256 CBC mode with PKCS7 padding. Requires `{ dangerUnauthenticated: true }`. |
|
|
139
|
+
| `ChaCha20` | `chacha20` | **No** | ChaCha20 stream cipher — RFC 8439. Unauthenticated; use `XChaCha20Poly1305` unless you need raw keystream. |
|
|
140
|
+
| `Poly1305` | `chacha20` | **No** | Poly1305 one-time MAC — RFC 8439. Use via the AEAD classes unless you have a specific reason not to. |
|
|
141
|
+
| **Hashing and key derivation** | | | |
|
|
142
|
+
| `SHA256`, `SHA384`, `SHA512` | `sha2` | — | SHA-2 family — FIPS 180-4. |
|
|
143
|
+
| `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512` | `sha2` | — | HMAC construction over SHA-2 — RFC 2104. |
|
|
144
|
+
| `HKDF_SHA256`, `HKDF_SHA512` | `sha2` | — | Extract-and-expand key derivation over HMAC — RFC 5869. |
|
|
145
|
+
| `SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512` | `sha3` | — | SHA-3 family — FIPS 202. Keccak-based, structurally independent of SHA-2. |
|
|
146
|
+
| `SHAKE128`, `SHAKE256` | `sha3` | — | Extendable output functions (XOF) — FIPS 202. Variable-length output; useful for key derivation and stream generation. |
|
|
147
|
+
| **CSPRNG** | | | |
|
|
148
|
+
| `Fortuna` | `serpent`, `sha2` | — | Fortuna CSPRNG (Ferguson & Schneier). 32 entropy pools, forward secrecy. Use `Fortuna.create()`. |
|
|
86
149
|
|
|
87
150
|
> [!IMPORTANT]
|
|
88
151
|
> All cryptographic computation runs in WASM (AssemblyScript), isolated outside the JavaScript JIT. The TypeScript layer provides the public API with input validation, type safety, and developer ergonomics.
|
|
89
152
|
|
|
90
|
-
> [!WARNING]
|
|
91
|
-
> `SerpentCtr` and `SerpentCbc` are **unauthenticated** cipher modes. They provide confidentiality but not integrity or authenticity. An attacker can modify ciphertext without detection. For authenticated Serpent encryption use `SerpentSeal` or `SerpentStreamSealer`. When using CBC/CTR directly, pair with `HMAC_SHA256` using the Encrypt-then-MAC pattern.
|
|
92
|
-
|
|
93
153
|
---
|
|
94
154
|
|
|
95
155
|
## Quick Start
|
|
@@ -135,7 +195,8 @@ const decrypted = chacha.decrypt(key, nonce, ciphertext)
|
|
|
135
195
|
chacha.dispose()
|
|
136
196
|
```
|
|
137
197
|
|
|
138
|
-
For more examples, including streaming, chunking, hashing, and key derivation,
|
|
198
|
+
For more examples, including streaming, chunking, hashing, and key derivation,
|
|
199
|
+
see the [examples page](https://github.com/xero/leviathan-crypto/wiki/examples).
|
|
139
200
|
|
|
140
201
|
---
|
|
141
202
|
|
|
@@ -154,7 +215,8 @@ await init(['serpent'], 'manual', { wasmBinary: { serpent: myBuffer } })
|
|
|
154
215
|
|
|
155
216
|
### Tree-shaking with subpath imports
|
|
156
217
|
|
|
157
|
-
Each cipher ships as its own subpath export. A bundler with tree-shaking
|
|
218
|
+
Each cipher ships as its own subpath export. A bundler with tree-shaking
|
|
219
|
+
support and `"sideEffects": false` will exclude every module you don't import:
|
|
158
220
|
|
|
159
221
|
```typescript
|
|
160
222
|
// Only serpent.wasm ends up in your bundle
|
|
@@ -186,37 +248,38 @@ await chacha20Init()
|
|
|
186
248
|
|
|
187
249
|
### API Surface
|
|
188
250
|
|
|
189
|
-
| Module
|
|
190
|
-
|
|
|
191
|
-
| serpent
|
|
192
|
-
| asm_serpent
|
|
193
|
-
| chacha20
|
|
194
|
-
| asm_chacha
|
|
195
|
-
| sha2
|
|
196
|
-
| asm_sha2
|
|
197
|
-
| sha3
|
|
198
|
-
| asm_sha3
|
|
199
|
-
| fortuna
|
|
200
|
-
| init
|
|
201
|
-
| utils
|
|
202
|
-
| types
|
|
251
|
+
| Module | MD/Wiki | Description |
|
|
252
|
+
| ----------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
253
|
+
| serpent | [▼](./docs/serpent.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/serpent) | Serpent-256 TypeScript API (`SerpentSeal`, `SerpentStream`, `SerpentStreamPool`, `SerpentStreamSealer`, `SerpentStreamOpener`, `Serpent`, `SerpentCtr`, `SerpentCbc`) |
|
|
254
|
+
| asm_serpent | [▼](./docs/asm_serpent.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_serpent) | Serpent-256 WASM implementation (bitslice S-boxes, key schedule, CTR/CBC) |
|
|
255
|
+
| chacha20 | [▼](./docs/chacha20.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/chacha20) | ChaCha20/Poly1305 TypeScript API (`ChaCha20`, `Poly1305`, `ChaCha20Poly1305`, `XChaCha20Poly1305`, `XChaCha20Poly1305Pool`) |
|
|
256
|
+
| asm_chacha | [▼](./docs/asm_chacha.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_chacha) | ChaCha20/Poly1305 WASM implementation (quarter-round, HChaCha20) |
|
|
257
|
+
| sha2 | [▼](./docs/sha2.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/sha2) | SHA-2 TypeScript API (`SHA256`, `SHA512`, `SHA384`, `HMAC_SHA256`, `HMAC_SHA512`, `HMAC_SHA384`) |
|
|
258
|
+
| asm_sha2 | [▼](./docs/asm_sha2.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_sha2) | SHA-2 WASM implementation (compression functions, HMAC) |
|
|
259
|
+
| sha3 | [▼](./docs/sha3.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/sha3) | SHA-3 TypeScript API (`SHA3_224`, `SHA3_256`, `SHA3_384`, `SHA3_512`, `SHAKE128`, `SHAKE256`) |
|
|
260
|
+
| asm_sha3 | [▼](./docs/asm_sha3.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/asm_sha3) | SHA-3 WASM implementation (Keccak-f[1600], sponge construction) |
|
|
261
|
+
| fortuna | [▼](./docs/fortuna.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/fortuna) | Fortuna CSPRNG (forward secrecy, 32 entropy pools) |
|
|
262
|
+
| init | [▼](./docs/init.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/init) | `init()` API and WASM loading modes |
|
|
263
|
+
| utils | [▼](./docs/utils.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils) | Encoding helpers, `constantTimeEqual`, `wipe`, `randomBytes` |
|
|
264
|
+
| types | [▼](./docs/types.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/types) | TypeScript interfaces (`Hash`, `KeyedHash`, `Blockcipher`, `Streamcipher`, `AEAD`) |
|
|
203
265
|
|
|
204
266
|
### Utilities
|
|
205
267
|
|
|
206
268
|
These helpers are available immediately on import with no `init()` required.
|
|
207
269
|
|
|
208
|
-
| Function | MD/Wiki | Description
|
|
209
|
-
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
210
|
-
| `hexToBytes(hex)` | [▼](./docs/utils.md#hextobytes) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#hextobytes) | Hex string to `Uint8Array` (accepts uppercase, `0x` prefix)
|
|
211
|
-
| `bytesToHex(bytes)` | [▼](./docs/utils.md#bytestohex) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#bytestohex) | `Uint8Array` to lowercase hex string
|
|
212
|
-
| `utf8ToBytes(str)` | [▼](./docs/utils.md#utf8tobytes) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#utf8tobytes) | UTF-8 string to `Uint8Array`
|
|
213
|
-
| `bytesToUtf8(bytes)` | [▼](./docs/utils.md#bytestoutf8) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#bytestoutf8) | `Uint8Array` to UTF-8 string
|
|
214
|
-
| `base64ToBytes(b64)` | [▼](./docs/utils.md#base64tobytes) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#base64tobytes) | Base64/base64url string to `Uint8Array` (undefined on invalid)
|
|
215
|
-
| `bytesToBase64(bytes, url?)` | [▼](./docs/utils.md#bytestobase64) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#bytestobase64) | `Uint8Array` to base64 string (url=true for base64url)
|
|
216
|
-
| `constantTimeEqual(a, b)` | [▼](./docs/utils.md#constanttimeequal) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal) | Constant-time byte comparison (XOR-accumulate)
|
|
217
|
-
| `wipe(data)` | [▼](./docs/utils.md#wipe) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#wipe) | Zero a typed array in place
|
|
218
|
-
| `xor(a, b)` | [▼](./docs/utils.md#xor) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#xor) | XOR two equal-length `Uint8Array`s
|
|
219
|
-
| `concat(a, b)` | [▼](./docs/utils.md#concat) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#concat) | Concatenate two `Uint8Array`s
|
|
270
|
+
| Function | MD/Wiki | Description |
|
|
271
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
272
|
+
| `hexToBytes(hex)` | [▼](./docs/utils.md#hextobytes) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#hextobytes) | Hex string to `Uint8Array` (accepts uppercase, `0x` prefix) |
|
|
273
|
+
| `bytesToHex(bytes)` | [▼](./docs/utils.md#bytestohex) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#bytestohex) | `Uint8Array` to lowercase hex string |
|
|
274
|
+
| `utf8ToBytes(str)` | [▼](./docs/utils.md#utf8tobytes) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#utf8tobytes) | UTF-8 string to `Uint8Array` |
|
|
275
|
+
| `bytesToUtf8(bytes)` | [▼](./docs/utils.md#bytestoutf8) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#bytestoutf8) | `Uint8Array` to UTF-8 string |
|
|
276
|
+
| `base64ToBytes(b64)` | [▼](./docs/utils.md#base64tobytes) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#base64tobytes) | Base64/base64url string to `Uint8Array` (undefined on invalid) |
|
|
277
|
+
| `bytesToBase64(bytes, url?)` | [▼](./docs/utils.md#bytestobase64) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#bytestobase64) | `Uint8Array` to base64 string (url=true for base64url) |
|
|
278
|
+
| `constantTimeEqual(a, b)` | [▼](./docs/utils.md#constanttimeequal) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal) | Constant-time byte comparison (XOR-accumulate) |
|
|
279
|
+
| `wipe(data)` | [▼](./docs/utils.md#wipe) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#wipe) | Zero a typed array in place |
|
|
280
|
+
| `xor(a, b)` | [▼](./docs/utils.md#xor) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#xor) | XOR two equal-length `Uint8Array`s |
|
|
281
|
+
| `concat(a, b)` | [▼](./docs/utils.md#concat) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#concat) | Concatenate two `Uint8Array`s |
|
|
282
|
+
| `hasSIMD()` | [▼](./docs/utils.md#hassimd) · [¶](https://github.com/xero/leviathan-crypto/wiki/utils#hassimd) | Detects WebAssembly SIMD support. Cached after first call. Used internally for CTR/CBC/ChaCha20 dispatch. |
|
|
220
283
|
|
|
221
284
|
### Algorithm correctness and verifications
|
|
222
285
|
|
|
@@ -230,13 +293,14 @@ These helpers are available immediately on import with no `init()` required.
|
|
|
230
293
|
| hkdf_audit | [▼](./docs/hkdf_audit.md) · [¶](https://github.com/xero/leviathan-crypto/wiki/hkdf_audit) | HKDF extract-then-expand, info field domain separation, SerpentStream key derivation |
|
|
231
294
|
|
|
232
295
|
>[!NOTE]
|
|
233
|
-
> Additional documentation available in [./docs](./docs/README.md) and on the
|
|
296
|
+
> Additional documentation available in [./docs](./docs/README.md) and on the
|
|
297
|
+
> [project wiki](https://github.com/xero/leviathan-crypto/wiki/).
|
|
234
298
|
|
|
235
299
|
---
|
|
236
300
|
|
|
237
301
|
## License
|
|
238
302
|
|
|
239
|
-
leviathan is
|
|
303
|
+
leviathan-crypto is released under the [MIT license](./LICENSE).
|
|
240
304
|
|
|
241
305
|
```
|
|
242
306
|
▄▄▄▄▄▄▄▄▄▄
|
|
@@ -260,3 +324,4 @@ leviathan is written under the [MIT license](http://www.opensource.org/licenses/
|
|
|
260
324
|
▀██████▀ ▀████▄▄▄████▀
|
|
261
325
|
▀█████▀
|
|
262
326
|
```
|
|
327
|
+
|
package/SECURITY.md
CHANGED
package/dist/docs/serpent.md
CHANGED
|
@@ -265,6 +265,12 @@ automatically.
|
|
|
265
265
|
- **chunk** -- any length up to the module's internal chunk buffer size. Throws
|
|
266
266
|
`RangeError` if the chunk exceeds the maximum size.
|
|
267
267
|
|
|
268
|
+
> [!NOTE]
|
|
269
|
+
> Automatically dispatches to the 4-wide SIMD path (`encryptChunk_simd`) when
|
|
270
|
+
> the runtime supports WebAssembly SIMD (`hasSIMD()` returns `true`), otherwise
|
|
271
|
+
> falls back to the scalar unrolled path. The dispatch is transparent — no API
|
|
272
|
+
> change required.
|
|
273
|
+
|
|
268
274
|
---
|
|
269
275
|
|
|
270
276
|
#### `beginDecrypt(key: Uint8Array, nonce: Uint8Array): void`
|
|
@@ -353,6 +359,12 @@ Decrypts Serpent CBC ciphertext and strips PKCS7 padding.
|
|
|
353
359
|
|
|
354
360
|
Returns the decrypted plaintext as a new `Uint8Array`.
|
|
355
361
|
|
|
362
|
+
> [!NOTE]
|
|
363
|
+
> Automatically dispatches to the 4-wide SIMD path (`cbcDecryptChunk_simd`) when
|
|
364
|
+
> the runtime supports WebAssembly SIMD (`hasSIMD()` returns `true`), otherwise
|
|
365
|
+
> falls back to the scalar unrolled path. CBC encryption has no SIMD variant —
|
|
366
|
+
> each ciphertext block depends on the previous one.
|
|
367
|
+
|
|
356
368
|
---
|
|
357
369
|
|
|
358
370
|
#### `dispose(): void`
|
|
@@ -535,7 +547,7 @@ and cross-stream splicing are all detected.
|
|
|
535
547
|
|
|
536
548
|
```typescript
|
|
537
549
|
class SerpentStreamSealer {
|
|
538
|
-
constructor(key: Uint8Array, chunkSize?: number)
|
|
550
|
+
constructor(key: Uint8Array, chunkSize?: number, opts?: { framed?: boolean })
|
|
539
551
|
header(): Uint8Array // call once before seal() — returns 20 bytes
|
|
540
552
|
seal(plaintext: Uint8Array): Uint8Array // exactly chunkSize bytes
|
|
541
553
|
final(plaintext: Uint8Array): Uint8Array // <= chunkSize bytes; wipes on return
|
|
@@ -543,8 +555,9 @@ class SerpentStreamSealer {
|
|
|
543
555
|
}
|
|
544
556
|
|
|
545
557
|
class SerpentStreamOpener {
|
|
546
|
-
constructor(key: Uint8Array, header: Uint8Array)
|
|
558
|
+
constructor(key: Uint8Array, header: Uint8Array, opts?: { framed?: boolean })
|
|
547
559
|
open(chunk: Uint8Array): Uint8Array // throws on auth failure or post-final
|
|
560
|
+
feed(bytes: Uint8Array): Uint8Array[] // framed mode only — accumulates and parses frames
|
|
548
561
|
dispose(): void
|
|
549
562
|
}
|
|
550
563
|
```
|
|
@@ -576,12 +589,18 @@ wipes its key material and transitions to `dead`. Subsequent `open()` calls thro
|
|
|
576
589
|
|
|
577
590
|
---
|
|
578
591
|
|
|
579
|
-
#### `constructor(key, chunkSize?)`
|
|
592
|
+
#### `constructor(key, chunkSize?, opts?)`
|
|
580
593
|
|
|
581
594
|
- **key** — 64-byte key. Throws `RangeError` if wrong length.
|
|
582
595
|
- **chunkSize** — bytes per chunk. Must be 1024–65536. Default: 65536. Throws
|
|
583
596
|
`RangeError` if out of range.
|
|
584
597
|
|
|
598
|
+
##### Options (`opts`)
|
|
599
|
+
|
|
600
|
+
| Option | Type | Default | Description |
|
|
601
|
+
|--------|------|---------|-------------|
|
|
602
|
+
| `framed` | `boolean` | `false` | Prepend `u32be(sealedLen)` to each `seal()`/`final()` output. Use for flat byte streams (files, pipes, TCP). Omit when the transport already frames messages (WebSocket, IPC). |
|
|
603
|
+
|
|
585
604
|
---
|
|
586
605
|
|
|
587
606
|
#### `header()`
|
|
@@ -615,12 +634,18 @@ Safe to call after `final()` — no-op if already dead.
|
|
|
615
634
|
|
|
616
635
|
---
|
|
617
636
|
|
|
618
|
-
#### `constructor(key, header)` (opener)
|
|
637
|
+
#### `constructor(key, header, opts?)` (opener)
|
|
619
638
|
|
|
620
639
|
- **key** — 64-byte key. Throws `RangeError` if wrong length.
|
|
621
640
|
- **header** — 20-byte stream header from `sealer.header()`. Throws `RangeError`
|
|
622
641
|
if wrong length.
|
|
623
642
|
|
|
643
|
+
##### Options (`opts`)
|
|
644
|
+
|
|
645
|
+
| Option | Type | Default | Description |
|
|
646
|
+
|--------|------|---------|-------------|
|
|
647
|
+
| `framed` | `boolean` | `false` | Enable byte-accumulation mode. Parses `u32be` length prefixes and dispatches complete frames to `open()` internally. Required to use `feed()`. |
|
|
648
|
+
|
|
624
649
|
---
|
|
625
650
|
|
|
626
651
|
#### `open(chunk)`
|
|
@@ -631,6 +656,15 @@ plaintext bytes (PKCS7 padding stripped).
|
|
|
631
656
|
|
|
632
657
|
---
|
|
633
658
|
|
|
659
|
+
#### `feed(bytes: Uint8Array): Uint8Array[]`
|
|
660
|
+
|
|
661
|
+
Only callable when constructed with `{ framed: true }`. Accumulates incoming bytes,
|
|
662
|
+
parses `u32be` length prefixes, dispatches complete frames to `open()` internally.
|
|
663
|
+
Returns an array of decrypted chunks — zero, one, or more per call depending on how
|
|
664
|
+
many complete frames were buffered. Throws if called on an unframed opener.
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
634
668
|
#### `dispose()` (opener)
|
|
635
669
|
|
|
636
670
|
Wipes key material. Safe to call at any point — use to abort opening a stream
|
package/dist/docs/utils.md
CHANGED
|
@@ -143,6 +143,26 @@ Returns `n` cryptographically secure random bytes via the Web Crypto API (`crypt
|
|
|
143
143
|
|
|
144
144
|
---
|
|
145
145
|
|
|
146
|
+
### hasSIMD
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
hasSIMD(): boolean
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Returns `true` if the current runtime supports WebAssembly SIMD (the `v128`
|
|
153
|
+
type and associated operations). The result is computed once on first call by
|
|
154
|
+
validating a minimal v128 WASM module, then cached for subsequent calls.
|
|
155
|
+
|
|
156
|
+
This function is called internally by `SerpentCtr.encryptChunk`,
|
|
157
|
+
`SerpentCbc.decrypt`, and `ChaCha20.encryptChunk` to select the fast SIMD path
|
|
158
|
+
at runtime. It is exported for informational purposes — you do not need to call
|
|
159
|
+
it yourself. SIMD dispatch is fully automatic.
|
|
160
|
+
|
|
161
|
+
Supported in all modern browsers and Node.js 16+. Returns `false` in older
|
|
162
|
+
environments, which fall back silently to the scalar path.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
146
166
|
## Usage Examples
|
|
147
167
|
|
|
148
168
|
### Converting between formats
|
|
@@ -258,6 +278,7 @@ console.log(combined.length) // 32
|
|
|
258
278
|
| `constantTimeEqual` | Arrays differ in length | Returns `false` immediately |
|
|
259
279
|
| `xor` | Arrays differ in length | Throws `RangeError` |
|
|
260
280
|
| `randomBytes` | `crypto` not available | Throws (runtime-dependent) |
|
|
281
|
+
| `hasSIMD` | `WebAssembly` not available | Returns `false` |
|
|
261
282
|
|
|
262
283
|
---
|
|
263
284
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Module, Mode, InitOpts } from './init.js';
|
|
2
2
|
export declare function init(modules: Module | Module[], mode?: Mode, opts?: InitOpts): Promise<void>;
|
|
3
3
|
export { type Module, type Mode, type InitOpts, isInitialized, _resetForTesting } from './init.js';
|
|
4
|
-
export { serpentInit, SerpentSeal, Serpent, SerpentCtr, SerpentCbc, SerpentStream, SerpentStreamPool, SerpentStreamSealer, SerpentStreamOpener,
|
|
4
|
+
export { serpentInit, SerpentSeal, Serpent, SerpentCtr, SerpentCbc, SerpentStream, SerpentStreamPool, SerpentStreamSealer, SerpentStreamOpener, _serpentReady } from './serpent/index.js';
|
|
5
5
|
export type { StreamPoolOpts } from './serpent/index.js';
|
|
6
6
|
export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, _chachaReady } from './chacha20/index.js';
|
|
7
7
|
export { XChaCha20Poly1305Pool } from './chacha20/pool.js';
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function init(modules, mode = 'embedded', opts) {
|
|
|
35
35
|
await Promise.all(list.map(mod => _dispatchers[mod](mode, opts)));
|
|
36
36
|
}
|
|
37
37
|
export { isInitialized, _resetForTesting } from './init.js';
|
|
38
|
-
export { serpentInit, SerpentSeal, Serpent, SerpentCtr, SerpentCbc, SerpentStream, SerpentStreamPool, SerpentStreamSealer, SerpentStreamOpener,
|
|
38
|
+
export { serpentInit, SerpentSeal, Serpent, SerpentCtr, SerpentCbc, SerpentStream, SerpentStreamPool, SerpentStreamSealer, SerpentStreamOpener, _serpentReady } from './serpent/index.js';
|
|
39
39
|
export { chacha20Init, ChaCha20, Poly1305, ChaCha20Poly1305, XChaCha20Poly1305, _chachaReady } from './chacha20/index.js';
|
|
40
40
|
export { XChaCha20Poly1305Pool } from './chacha20/pool.js';
|
|
41
41
|
export { sha2Init, SHA256, SHA512, SHA384, HMAC_SHA256, HMAC_SHA512, HMAC_SHA384, HKDF_SHA256, HKDF_SHA512, _sha2Ready } from './sha2/index.js';
|
package/dist/serpent/index.d.ts
CHANGED
|
@@ -61,5 +61,4 @@ export { SerpentStream, sealChunk, openChunk } from './stream.js';
|
|
|
61
61
|
export { SerpentStreamPool } from './stream-pool.js';
|
|
62
62
|
export type { StreamPoolOpts } from './stream-pool.js';
|
|
63
63
|
export { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
|
|
64
|
-
export { SerpentStreamEncoder, SerpentStreamDecoder } from './stream-encoder.js';
|
|
65
64
|
export declare function _serpentReady(): boolean;
|
package/dist/serpent/index.js
CHANGED
|
@@ -231,8 +231,6 @@ export { SerpentStream, sealChunk, openChunk } from './stream.js';
|
|
|
231
231
|
export { SerpentStreamPool } from './stream-pool.js';
|
|
232
232
|
// ── SerpentStreamSealer / SerpentStreamOpener re-export ───────────────────────
|
|
233
233
|
export { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
|
|
234
|
-
// ── SerpentStreamEncoder / SerpentStreamDecoder re-export ─────────────────────
|
|
235
|
-
export { SerpentStreamEncoder, SerpentStreamDecoder } from './stream-encoder.js';
|
|
236
234
|
// ── Ready check ──────────────────────────────────────────────────────────────
|
|
237
235
|
export function _serpentReady() {
|
|
238
236
|
try {
|
|
@@ -5,11 +5,19 @@ export declare class SerpentStreamSealer {
|
|
|
5
5
|
private readonly _cbc;
|
|
6
6
|
private readonly _hmac;
|
|
7
7
|
private readonly _hkdf;
|
|
8
|
+
private readonly _framed;
|
|
8
9
|
private readonly _ivs;
|
|
9
10
|
private _ivIdx;
|
|
10
11
|
private _index;
|
|
11
12
|
private _state;
|
|
12
|
-
|
|
13
|
+
/** Public: consumers use this 3-param form. */
|
|
14
|
+
constructor(key: Uint8Array, chunkSize?: number, opts?: {
|
|
15
|
+
framed?: boolean;
|
|
16
|
+
});
|
|
17
|
+
/** @internal Test-only overload to inject fixed nonce/IVs for deterministic KAT vectors. */
|
|
18
|
+
constructor(key: Uint8Array, chunkSize: number | undefined, opts: {
|
|
19
|
+
framed?: boolean;
|
|
20
|
+
} | undefined, _nonce: Uint8Array, _ivs?: Uint8Array[]);
|
|
13
21
|
header(): Uint8Array;
|
|
14
22
|
seal(plaintext: Uint8Array): Uint8Array;
|
|
15
23
|
final(plaintext: Uint8Array): Uint8Array;
|
|
@@ -24,11 +32,19 @@ export declare class SerpentStreamOpener {
|
|
|
24
32
|
private readonly _cbc;
|
|
25
33
|
private readonly _hmac;
|
|
26
34
|
private readonly _hkdf;
|
|
35
|
+
private readonly _framed;
|
|
36
|
+
private readonly _buf;
|
|
37
|
+
private readonly _maxFrame;
|
|
38
|
+
private _bufLen;
|
|
27
39
|
private _index;
|
|
28
40
|
private _dead;
|
|
29
|
-
constructor(key: Uint8Array, header: Uint8Array
|
|
41
|
+
constructor(key: Uint8Array, header: Uint8Array, opts?: {
|
|
42
|
+
framed?: boolean;
|
|
43
|
+
});
|
|
30
44
|
get closed(): boolean;
|
|
31
45
|
open(chunk: Uint8Array): Uint8Array;
|
|
46
|
+
private _openRaw;
|
|
47
|
+
feed(bytes: Uint8Array): Uint8Array[];
|
|
32
48
|
private _wipe;
|
|
33
49
|
dispose(): void;
|
|
34
50
|
}
|
|
@@ -63,12 +63,12 @@ export class SerpentStreamSealer {
|
|
|
63
63
|
_cbc;
|
|
64
64
|
_hmac;
|
|
65
65
|
_hkdf;
|
|
66
|
+
_framed;
|
|
66
67
|
_ivs; // test seam: fixed IVs
|
|
67
68
|
_ivIdx;
|
|
68
69
|
_index;
|
|
69
70
|
_state;
|
|
70
|
-
|
|
71
|
-
constructor(key, chunkSize, _nonce, _ivs) {
|
|
71
|
+
constructor(key, chunkSize, opts, _nonce, _ivs) {
|
|
72
72
|
if (!_serpentReady())
|
|
73
73
|
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamSealer');
|
|
74
74
|
if (!_sha2Ready())
|
|
@@ -80,6 +80,7 @@ export class SerpentStreamSealer {
|
|
|
80
80
|
throw new RangeError(`SerpentStreamSealer chunkSize must be ${CHUNK_MIN}..${CHUNK_MAX} (got ${cs})`);
|
|
81
81
|
this._key = key.slice();
|
|
82
82
|
this._cs = cs;
|
|
83
|
+
this._framed = opts?.framed ?? false;
|
|
83
84
|
this._nonce = new Uint8Array(16);
|
|
84
85
|
if (_nonce && _nonce.length === 16) {
|
|
85
86
|
this._nonce.set(_nonce);
|
|
@@ -138,7 +139,13 @@ export class SerpentStreamSealer {
|
|
|
138
139
|
const ciphertext = this._cbc.encrypt(encKey, iv, plaintext);
|
|
139
140
|
const tag = this._hmac.hash(macKey, concat(iv, ciphertext));
|
|
140
141
|
this._index++;
|
|
141
|
-
|
|
142
|
+
const sealed = concat(concat(iv, ciphertext), tag);
|
|
143
|
+
if (!this._framed)
|
|
144
|
+
return sealed;
|
|
145
|
+
const out = new Uint8Array(4 + sealed.length);
|
|
146
|
+
out.set(u32be(sealed.length), 0);
|
|
147
|
+
out.set(sealed, 4);
|
|
148
|
+
return out;
|
|
142
149
|
}
|
|
143
150
|
_wipe() {
|
|
144
151
|
wipe(this._key);
|
|
@@ -160,9 +167,13 @@ export class SerpentStreamOpener {
|
|
|
160
167
|
_cbc;
|
|
161
168
|
_hmac;
|
|
162
169
|
_hkdf;
|
|
170
|
+
_framed;
|
|
171
|
+
_buf;
|
|
172
|
+
_maxFrame;
|
|
173
|
+
_bufLen;
|
|
163
174
|
_index;
|
|
164
175
|
_dead;
|
|
165
|
-
constructor(key, header) {
|
|
176
|
+
constructor(key, header, opts) {
|
|
166
177
|
if (!_serpentReady())
|
|
167
178
|
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamOpener');
|
|
168
179
|
if (!_sha2Ready())
|
|
@@ -174,11 +185,21 @@ export class SerpentStreamOpener {
|
|
|
174
185
|
this._key = key.slice();
|
|
175
186
|
this._nonce = header.slice(0, 16);
|
|
176
187
|
this._cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
|
|
188
|
+
if (this._cs < CHUNK_MIN || this._cs > CHUNK_MAX)
|
|
189
|
+
throw new RangeError(`SerpentStreamOpener: header contains invalid chunkSize ${this._cs} (expected ${CHUNK_MIN}..${CHUNK_MAX})`);
|
|
190
|
+
this._framed = opts?.framed ?? false;
|
|
177
191
|
this._cbc = new SerpentCbc({ dangerUnauthenticated: true });
|
|
178
192
|
this._hmac = new HMAC_SHA256();
|
|
179
193
|
this._hkdf = new HKDF_SHA256();
|
|
180
194
|
this._index = 0;
|
|
181
195
|
this._dead = false;
|
|
196
|
+
this._bufLen = 0;
|
|
197
|
+
if (this._framed) {
|
|
198
|
+
const cs = this._cs;
|
|
199
|
+
const maxSealed = 16 + (cs + (16 - (cs % 16))) + 32;
|
|
200
|
+
this._maxFrame = 4 + maxSealed;
|
|
201
|
+
this._buf = new Uint8Array(this._maxFrame);
|
|
202
|
+
}
|
|
182
203
|
}
|
|
183
204
|
get closed() {
|
|
184
205
|
return this._dead;
|
|
@@ -186,6 +207,11 @@ export class SerpentStreamOpener {
|
|
|
186
207
|
open(chunk) {
|
|
187
208
|
if (this._dead)
|
|
188
209
|
throw new Error('SerpentStreamOpener: stream is closed');
|
|
210
|
+
if (this._framed)
|
|
211
|
+
throw new Error('SerpentStreamOpener: call feed() on framed openers — open() expects raw sealed chunks without length prefix');
|
|
212
|
+
return this._openRaw(chunk);
|
|
213
|
+
}
|
|
214
|
+
_openRaw(chunk) {
|
|
189
215
|
// Try isLast = true first, then false.
|
|
190
216
|
// Whichever passes auth is the correct interpretation.
|
|
191
217
|
for (const isLast of [true, false]) {
|
|
@@ -208,12 +234,104 @@ export class SerpentStreamOpener {
|
|
|
208
234
|
}
|
|
209
235
|
throw new Error('SerpentStreamOpener: authentication failed');
|
|
210
236
|
}
|
|
237
|
+
feed(bytes) {
|
|
238
|
+
if (!this._framed)
|
|
239
|
+
throw new Error('SerpentStreamOpener: feed() requires { framed: true }');
|
|
240
|
+
if (this._dead)
|
|
241
|
+
throw new Error('SerpentStreamOpener: stream is closed');
|
|
242
|
+
const buf = this._buf;
|
|
243
|
+
const maxFrame = this._maxFrame;
|
|
244
|
+
const results = [];
|
|
245
|
+
let consumed = 0;
|
|
246
|
+
// ── Phase 1: drain carry-over ─────────────────────────────────────
|
|
247
|
+
if (this._bufLen > 0) {
|
|
248
|
+
// Sub-case A: partial prefix — we have < 4 bytes buffered
|
|
249
|
+
if (this._bufLen < 4) {
|
|
250
|
+
const need = 4 - this._bufLen;
|
|
251
|
+
const take = Math.min(need, bytes.length - consumed);
|
|
252
|
+
buf.set(bytes.subarray(consumed, consumed + take), this._bufLen);
|
|
253
|
+
this._bufLen += take;
|
|
254
|
+
consumed += take;
|
|
255
|
+
if (this._bufLen < 4)
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
const sealedLen = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
259
|
+
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
260
|
+
this._wipe();
|
|
261
|
+
throw new Error('SerpentStreamOpener: invalid sealed chunk length');
|
|
262
|
+
}
|
|
263
|
+
const frameLen = 4 + sealedLen;
|
|
264
|
+
// Sub-case B: partial frame body
|
|
265
|
+
const haveInBuf = this._bufLen;
|
|
266
|
+
const needMore = frameLen - haveInBuf;
|
|
267
|
+
if (needMore > 0) {
|
|
268
|
+
const take = Math.min(needMore, bytes.length - consumed);
|
|
269
|
+
buf.set(bytes.subarray(consumed, consumed + take), haveInBuf);
|
|
270
|
+
this._bufLen += take;
|
|
271
|
+
consumed += take;
|
|
272
|
+
if (this._bufLen < frameLen)
|
|
273
|
+
return results;
|
|
274
|
+
}
|
|
275
|
+
const plaintext = this._openRaw(buf.subarray(4, frameLen));
|
|
276
|
+
results.push(plaintext);
|
|
277
|
+
this._bufLen = 0;
|
|
278
|
+
if (this._dead) {
|
|
279
|
+
if (consumed < bytes.length) {
|
|
280
|
+
this._wipe();
|
|
281
|
+
throw new Error('SerpentStreamOpener: unexpected bytes after final chunk');
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ── Phase 2: parse complete frames directly from bytes ────────────
|
|
287
|
+
let pos = consumed;
|
|
288
|
+
while (true) {
|
|
289
|
+
if (bytes.length - pos < 4)
|
|
290
|
+
break;
|
|
291
|
+
const sealedLen = (bytes[pos] << 24 | bytes[pos + 1] << 16 | bytes[pos + 2] << 8 | bytes[pos + 3]) >>> 0;
|
|
292
|
+
if (sealedLen === 0 || sealedLen > (maxFrame - 4)) {
|
|
293
|
+
this._wipe();
|
|
294
|
+
throw new Error('SerpentStreamOpener: invalid sealed chunk length');
|
|
295
|
+
}
|
|
296
|
+
const frameLen = 4 + sealedLen;
|
|
297
|
+
if (bytes.length - pos < frameLen)
|
|
298
|
+
break;
|
|
299
|
+
const plaintext = this._openRaw(bytes.subarray(pos + 4, pos + frameLen));
|
|
300
|
+
results.push(plaintext);
|
|
301
|
+
if (this._dead) {
|
|
302
|
+
const remaining = bytes.length - pos - frameLen;
|
|
303
|
+
if (remaining > 0) {
|
|
304
|
+
this._wipe();
|
|
305
|
+
throw new Error('SerpentStreamOpener: unexpected bytes after final chunk');
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
pos += frameLen;
|
|
310
|
+
}
|
|
311
|
+
// ── Carry over any incomplete trailing bytes into _buf ────────────
|
|
312
|
+
const leftover = bytes.length - pos;
|
|
313
|
+
if (leftover > 0) {
|
|
314
|
+
if (leftover > maxFrame) {
|
|
315
|
+
this._wipe();
|
|
316
|
+
throw new Error('SerpentStreamOpener: input exceeds maximum frame size');
|
|
317
|
+
}
|
|
318
|
+
buf.set(bytes.subarray(pos), 0);
|
|
319
|
+
this._bufLen = leftover;
|
|
320
|
+
}
|
|
321
|
+
return results;
|
|
322
|
+
}
|
|
211
323
|
_wipe() {
|
|
324
|
+
if (this._dead)
|
|
325
|
+
return;
|
|
212
326
|
wipe(this._key);
|
|
213
327
|
wipe(this._nonce);
|
|
214
328
|
this._cbc.dispose();
|
|
215
329
|
this._hmac.dispose();
|
|
216
330
|
this._hkdf.dispose();
|
|
331
|
+
if (this._framed) {
|
|
332
|
+
wipe(this._buf);
|
|
333
|
+
this._bufLen = 0;
|
|
334
|
+
}
|
|
217
335
|
this._dead = true;
|
|
218
336
|
}
|
|
219
337
|
dispose() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leviathan-crypto",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "xero (https://x-e.ro)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Zero-dependency WebAssembly cryptography library for TypeScript: Serpent-256, XChaCha20-Poly1305, SHA-2/3, HMAC, HKDF, and Fortuna CSPRNG, with a strictly typed API built on vector-verified primitives.",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export declare class SerpentStreamEncoder {
|
|
2
|
-
private readonly _sealer;
|
|
3
|
-
private _state;
|
|
4
|
-
constructor(key: Uint8Array, chunkSize?: number, _nonce?: Uint8Array, _ivs?: Uint8Array[]);
|
|
5
|
-
header(): Uint8Array;
|
|
6
|
-
encode(plaintext: Uint8Array): Uint8Array;
|
|
7
|
-
encodeFinal(plaintext: Uint8Array): Uint8Array;
|
|
8
|
-
dispose(): void;
|
|
9
|
-
}
|
|
10
|
-
export declare class SerpentStreamDecoder {
|
|
11
|
-
private readonly _opener;
|
|
12
|
-
private readonly _buf;
|
|
13
|
-
private readonly _maxFrame;
|
|
14
|
-
private _bufLen;
|
|
15
|
-
private _dead;
|
|
16
|
-
constructor(key: Uint8Array, header: Uint8Array);
|
|
17
|
-
feed(bytes: Uint8Array): Uint8Array[];
|
|
18
|
-
private _wipe;
|
|
19
|
-
dispose(): void;
|
|
20
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
// ▄▄▄▄▄▄▄▄▄▄
|
|
2
|
-
// ▄████████████████████▄▄ ▒ ▄▀▀ ▒ ▒ █ ▄▀▄ ▀█▀ █ ▒ ▄▀▄ █▀▄
|
|
3
|
-
// ▄██████████████████████ ▀████▄ ▓ ▓▀ ▓ ▓ ▓ ▓▄▓ ▓ ▓▀▓ ▓▄▓ ▓ ▓
|
|
4
|
-
// ▄█████████▀▀▀ ▀███████▄▄███████▌ ▀▄ ▀▄▄ ▀▄▀ ▒ ▒ ▒ ▒ ▒ █ ▒ ▒ ▒ █
|
|
5
|
-
// ▐████████▀ ▄▄▄▄ ▀████████▀██▀█▌
|
|
6
|
-
// ████████ ███▀▀ ████▀ █▀ █▀ Leviathan Crypto Library
|
|
7
|
-
// ███████▌ ▀██▀ ███
|
|
8
|
-
// ███████ ▀███ ▀██ ▀█▄ Repository & Mirror:
|
|
9
|
-
// ▀██████ ▄▄██ ▀▀ ██▄ github.com/xero/leviathan-crypto
|
|
10
|
-
// ▀█████▄ ▄██▄ ▄▀▄▀ unpkg.com/leviathan-crypto
|
|
11
|
-
// ▀████▄ ▄██▄
|
|
12
|
-
// ▐████ ▐███ Author: xero (https://x-e.ro)
|
|
13
|
-
// ▄▄██████████ ▐███ ▄▄ License: MIT
|
|
14
|
-
// ▄██▀▀▀▀▀▀▀▀▀▀ ▄████ ▄██▀
|
|
15
|
-
// ▄▀ ▄▄█████████▄▄ ▀▀▀▀▀ ▄███ This file is provided completely
|
|
16
|
-
// ▄██████▀▀▀▀▀▀██████▄ ▀▄▄▄▄████▀ free, "as is", and without
|
|
17
|
-
// ████▀ ▄▄▄▄▄▄▄ ▀████▄ ▀█████▀ ▄▄▄▄ warranty of any kind. The author
|
|
18
|
-
// █████▄▄█████▀▀▀▀▀▀▄ ▀███▄ ▄████ assumes absolutely no liability
|
|
19
|
-
// ▀██████▀ ▀████▄▄▄████▀ for its {ab,mis,}use.
|
|
20
|
-
// ▀█████▀▀
|
|
21
|
-
//
|
|
22
|
-
// src/ts/serpent/stream-encoder.ts
|
|
23
|
-
//
|
|
24
|
-
// Tier 2 pure-TS composition: SerpentStreamSealer / SerpentStreamOpener
|
|
25
|
-
// with u32be length-prefixed framing.
|
|
26
|
-
//
|
|
27
|
-
// SerpentStreamEncoder: wraps SerpentStreamSealer, prepends u32be(sealedLen)
|
|
28
|
-
// SerpentStreamDecoder: wraps SerpentStreamOpener, buffers input and assembles
|
|
29
|
-
// complete frames before dispatching to opener.open()
|
|
30
|
-
//
|
|
31
|
-
// Wire format per chunk:
|
|
32
|
-
// u32be(sealedLen) || IV(16) || CBC_ciphertext(padded) || HMAC(32)
|
|
33
|
-
//
|
|
34
|
-
// Use these classes when chunks will be concatenated into a flat byte array
|
|
35
|
-
// (files, buffered TCP, etc). Use SerpentStreamSealer/Opener directly when
|
|
36
|
-
// the transport already frames messages (WebSocket, IPC, etc).
|
|
37
|
-
import { SerpentStreamSealer, SerpentStreamOpener } from './stream-sealer.js';
|
|
38
|
-
import { _serpentReady } from './index.js';
|
|
39
|
-
import { _sha2Ready } from '../sha2/index.js';
|
|
40
|
-
import { wipe } from '../utils.js';
|
|
41
|
-
import { u32be } from './stream.js';
|
|
42
|
-
export class SerpentStreamEncoder {
|
|
43
|
-
_sealer;
|
|
44
|
-
_state;
|
|
45
|
-
// _nonce, _ivs: test seams — passed through to SerpentStreamSealer
|
|
46
|
-
constructor(key, chunkSize, _nonce, _ivs) {
|
|
47
|
-
if (!_serpentReady())
|
|
48
|
-
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamEncoder');
|
|
49
|
-
if (!_sha2Ready())
|
|
50
|
-
throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamEncoder');
|
|
51
|
-
this._sealer = new SerpentStreamSealer(key, chunkSize, _nonce, _ivs);
|
|
52
|
-
this._state = 'fresh';
|
|
53
|
-
}
|
|
54
|
-
header() {
|
|
55
|
-
if (this._state === 'encoding')
|
|
56
|
-
throw new Error('SerpentStreamEncoder: header() already called');
|
|
57
|
-
if (this._state === 'dead')
|
|
58
|
-
throw new Error('SerpentStreamEncoder: stream is closed');
|
|
59
|
-
this._state = 'encoding';
|
|
60
|
-
return this._sealer.header();
|
|
61
|
-
}
|
|
62
|
-
encode(plaintext) {
|
|
63
|
-
if (this._state === 'fresh')
|
|
64
|
-
throw new Error('SerpentStreamEncoder: call header() first');
|
|
65
|
-
if (this._state === 'dead')
|
|
66
|
-
throw new Error('SerpentStreamEncoder: stream is closed');
|
|
67
|
-
const sealed = this._sealer.seal(plaintext);
|
|
68
|
-
return _prependLen(sealed);
|
|
69
|
-
}
|
|
70
|
-
encodeFinal(plaintext) {
|
|
71
|
-
if (this._state === 'fresh')
|
|
72
|
-
throw new Error('SerpentStreamEncoder: call header() first');
|
|
73
|
-
if (this._state === 'dead')
|
|
74
|
-
throw new Error('SerpentStreamEncoder: stream is closed');
|
|
75
|
-
const sealed = this._sealer.final(plaintext);
|
|
76
|
-
this._state = 'dead';
|
|
77
|
-
return _prependLen(sealed);
|
|
78
|
-
}
|
|
79
|
-
dispose() {
|
|
80
|
-
if (this._state !== 'dead') {
|
|
81
|
-
this._sealer.dispose();
|
|
82
|
-
this._state = 'dead';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// ── SerpentStreamDecoder ─────────────────────────────────────────────────────
|
|
87
|
-
export class SerpentStreamDecoder {
|
|
88
|
-
_opener;
|
|
89
|
-
_buf; // fixed-size accumulation buffer
|
|
90
|
-
_maxFrame; // 4 + max sealed chunk size
|
|
91
|
-
_bufLen; // valid bytes currently in _buf
|
|
92
|
-
_dead;
|
|
93
|
-
constructor(key, header) {
|
|
94
|
-
if (!_serpentReady())
|
|
95
|
-
throw new Error('leviathan-crypto: call init([\'serpent\']) before using SerpentStreamDecoder');
|
|
96
|
-
if (!_sha2Ready())
|
|
97
|
-
throw new Error('leviathan-crypto: call init([\'sha2\']) before using SerpentStreamDecoder');
|
|
98
|
-
this._opener = new SerpentStreamOpener(key, header);
|
|
99
|
-
// Parse chunkSize from stream header (bytes 16..20, u32be)
|
|
100
|
-
const cs = (header[16] << 24 | header[17] << 16 | header[18] << 8 | header[19]) >>> 0;
|
|
101
|
-
// Max sealed chunk size: IV(16) + PKCS7-padded ciphertext + HMAC(32)
|
|
102
|
-
// PKCS7: plaintext always padded to next multiple of 16 (minimum 1 pad byte)
|
|
103
|
-
const maxSealed = 16 + (cs + (16 - (cs % 16))) + 32;
|
|
104
|
-
this._maxFrame = 4 + maxSealed;
|
|
105
|
-
this._buf = new Uint8Array(this._maxFrame);
|
|
106
|
-
this._bufLen = 0;
|
|
107
|
-
this._dead = false;
|
|
108
|
-
}
|
|
109
|
-
feed(bytes) {
|
|
110
|
-
if (this._dead)
|
|
111
|
-
throw new Error('SerpentStreamDecoder: stream is closed');
|
|
112
|
-
// Append incoming bytes to accumulation buffer
|
|
113
|
-
if (this._bufLen + bytes.length > this._maxFrame) {
|
|
114
|
-
throw new Error('SerpentStreamDecoder: input exceeds maximum frame size');
|
|
115
|
-
}
|
|
116
|
-
this._buf.set(bytes, this._bufLen);
|
|
117
|
-
this._bufLen += bytes.length;
|
|
118
|
-
const results = [];
|
|
119
|
-
while (true) {
|
|
120
|
-
// Need at least 4 bytes for the length prefix
|
|
121
|
-
if (this._bufLen < 4)
|
|
122
|
-
break;
|
|
123
|
-
const sealedLen = ((this._buf[0] << 24 | this._buf[1] << 16 |
|
|
124
|
-
this._buf[2] << 8 | this._buf[3]) >>> 0);
|
|
125
|
-
const frameLen = 4 + sealedLen;
|
|
126
|
-
// Need the full frame
|
|
127
|
-
if (this._bufLen < frameLen)
|
|
128
|
-
break;
|
|
129
|
-
// Complete frame — dispatch to opener
|
|
130
|
-
const sealedChunk = this._buf.subarray(4, frameLen);
|
|
131
|
-
const plaintext = this._opener.open(sealedChunk);
|
|
132
|
-
results.push(plaintext);
|
|
133
|
-
if (this._opener.closed) {
|
|
134
|
-
// After final chunk: any leftover bytes are a protocol error
|
|
135
|
-
const remaining = this._bufLen - frameLen;
|
|
136
|
-
if (remaining > 0) {
|
|
137
|
-
this._wipe();
|
|
138
|
-
throw new Error('SerpentStreamDecoder: unexpected bytes after final chunk');
|
|
139
|
-
}
|
|
140
|
-
this._wipe();
|
|
141
|
-
return results;
|
|
142
|
-
}
|
|
143
|
-
// Shift remaining bytes to front of buffer — no allocation
|
|
144
|
-
const remaining = this._bufLen - frameLen;
|
|
145
|
-
this._buf.copyWithin(0, frameLen, frameLen + remaining);
|
|
146
|
-
this._bufLen = remaining;
|
|
147
|
-
}
|
|
148
|
-
return results;
|
|
149
|
-
}
|
|
150
|
-
_wipe() {
|
|
151
|
-
wipe(this._buf);
|
|
152
|
-
this._bufLen = 0;
|
|
153
|
-
this._opener.dispose();
|
|
154
|
-
this._dead = true;
|
|
155
|
-
}
|
|
156
|
-
dispose() {
|
|
157
|
-
if (!this._dead)
|
|
158
|
-
this._wipe();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
162
|
-
function _prependLen(chunk) {
|
|
163
|
-
const out = new Uint8Array(4 + chunk.length);
|
|
164
|
-
out.set(u32be(chunk.length), 0);
|
|
165
|
-
out.set(chunk, 4);
|
|
166
|
-
return out;
|
|
167
|
-
}
|