leviathan-crypto 1.1.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/SECURITY.md CHANGED
@@ -1,18 +1,30 @@
1
1
  # Leviathan Crypto Library Security Policy
2
2
 
3
+ <img src="https://github.com/xero/leviathan-crypto/raw/main/docs/logo.svg" alt="Leviathan logo" width="100" align="left">
4
+
5
+ - **[Version Support](#supported-versions)**
6
+ - **[Security Posture](#security-posture)**
7
+ - **[Cryptanalytic Audits](#cryptanalytic-reviews)**
8
+ - **[Vulnerability Reporting](#reporting-a-vulnerability)**
9
+
10
+ ---
11
+
3
12
  ## Supported Versions
4
13
 
5
14
  | Version | Supported |
6
15
  |---------|-----------|
7
- | v1.x | |
16
+ | v1.3.x | ︎✓ |
17
+ | v1.2.x | ✓ |
18
+ | v1.1.x | ✗ |
19
+ | v1.0.x | ✗ |
8
20
 
9
- > [!NOTE]
10
- > v1.0.0 is the current stable release.
11
- > This table will be updated as new versions ship.
21
+ > [!WARNING]
22
+ > v1.0.x does not zero intermediate key material in HMAC and HKDF operations.
23
+ > Upgrading to v1.1.0 or later is strongly recommended.
12
24
 
13
25
  ## Security Posture
14
26
 
15
- leviathan-crypto is a cryptography library. Security is not an afterthought,
27
+ [`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library. Security is not an afterthought,
16
28
  it is the primary design constraint at every layer of the stack.
17
29
 
18
30
  ### Algorithm Correctness
@@ -23,69 +35,84 @@ against the authoritative specification for that algorithm:
23
35
  [RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
24
36
  [RFC 5869][rfc5869] (HKDF), and the original
25
37
  [Serpent-256 specification][serpent] and S-box reference. No algorithm was
26
- ported from an existing implementation the specs are the source of truth.
38
+ ported from an existing implementation. The specs are always the source of truth.
27
39
 
28
40
  All implementations are verified against published known-answer test vectors
29
41
  from NIST, RFC appendices, NESSIE, and the Argon2 reference suite. Vectors
30
42
  are immutable: if an implementation produces incorrect output, the
31
- implementation is fixed vectors are never adjusted to match code.
43
+ implementation is fixed and vectors are never adjusted to match code.
32
44
 
33
45
  ### Side-Channel Resistance
34
46
 
35
- Serpent's S-boxes are implemented as Boolean gate circuits no table
36
- lookups, no data-dependent memory access, no data-dependent branches. Every
47
+ Serpent's S-boxes are implemented as Boolean gate circuits designed with no table
48
+ lookups, no data-dependent memory access, and no data-dependent branches. Every
37
49
  bit is processed unconditionally on every block. This is the most
38
50
  timing-safe cipher implementation approach available in a WASM runtime,
39
51
  where JIT optimisation can otherwise introduce observable timing variation.
40
52
 
41
- All security-sensitive comparisons (MAC verification, padding validation)
53
+ All security-sensitive comparisons (e.g. MAC verification, padding validation)
42
54
  use XOR-accumulate patterns with no early return on mismatch.
43
55
  [`constantTimeEqual`][utils] is the mandated comparison function throughout
44
- the library and its demos.
56
+ the library and its [demos][demos].
45
57
 
46
58
  ### WASM Execution Model
47
59
 
48
60
  All cryptographic computation runs in WebAssembly, isolated outside the
49
61
  JavaScript JIT. WASM execution is deterministic and not subject to JIT
50
62
  speculation or optimisation. Each primitive family compiles to its own
51
- isolated binary with its own linear memory key material in the Serpent
52
- module cannot interact with memory in the SHA-3 module even in principle.
63
+ isolated binary with its own linear memory. For example, key material in
64
+ the Serpent module cannot interact with memory in the SHA-3 module,
65
+ even in principle.
53
66
 
54
- ### Cryptanalytic Review
67
+ ### Cryptanalytic Reviews
68
+
69
+ All of our primitives undergo periodic cryptographic implementation reviews.
70
+
71
+ | Primitive | Audit Description |
72
+ |-----------|-------------------|
73
+ | [serpent_audit][serpent_audit] | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
74
+ | [chacha_audit][chacha_audit] | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension |
75
+ | [sha2_audit][sha2_audit] | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
76
+ | [sha3_audit][sha3_audit] | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
77
+ | [hmac_audit][hmac_audit] | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
78
+ | [hkdf_audit][hkdf_audit] | HKDF extract-then-expand, info field domain separation, SerpentStream key derivation |
79
+
80
+ #### Additional Serpent-256 research
55
81
 
56
82
  The security margin of Serpent-256 has been independently researched and
57
- documented. The best known attack on the full 32-round cipher biclique
58
- cryptanalysis achieves a complexity of 2²⁵⁵·¹⁹ with 2⁴ chosen
83
+ documented. The best known attack on the full 32-round cipher, _"biclique
84
+ cryptanalysis"_, achieves a complexity of 2²⁵⁵·¹⁹ with 2⁴ chosen
59
85
  ciphertexts. This provides less than one bit of advantage over exhaustive
60
86
  key search and has zero practical impact. Independent research conducted
61
87
  against this implementation improved on the published result by −0.20 bits
62
88
  through systematic parameter search, confirming no structural weakness
63
89
  beyond what the published literature describes.
64
90
 
65
- See: [`xero/BicliqueFinder/biclique_research.md`][biclique] and
66
- [`leviathan-crypto/wiki/serpent_audit`][serpent-audit] for the full
67
- analysis.
91
+ See: [`xero/BicliqueFinder/biclique_research.md`][biclique]
68
92
 
69
93
  ### Authenticated Encryption by Default
70
94
 
71
95
  Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`) are exposed
72
96
  for power users but are not the recommended entry point. The primary API
73
97
  surfaces — `SerpentSeal`, `SerpentStream`, `SerpentStreamSealer` — are
74
- authenticated by construction. `SerpentStreamSealer` satisfies the
75
- Cryptographic Doom Principle: MAC verification is the unconditional gate on
76
- the open path, decryption is unreachable until that gate clears, and
77
- per-chunk HKDF key derivation with position-bound info extends this
98
+ authenticated by construction.
99
+
100
+ **`SerpentStreamSealer` satisfies the _Cryptographic Doom Principle_:**
101
+
102
+ MAC verification is the unconditional gate on the open path,
103
+ decryption is unreachable until that gate clears, and per-chunk
104
+ HKDF key derivation with position-bound info extends this
78
105
  guarantee to full stream integrity.
79
106
 
80
107
  ### Dependency Management
81
108
 
82
- The library has _zero_ runtime JavaScript dependencies by design.
109
+ The library has **zero** runtime dependencies by design.
83
110
  `sideEffects: false` is enforced in `package.json`. Argon2id integration
84
- is documented as an optional external dependency.
85
- See:" [`leviathan-crypto/wiki/argon2id`][argon2id-wiki].
111
+ is documented as an _optional_ external dependency.
112
+ See: [`leviathan-crypto/wiki/argon2id`][argon2id-wiki].
86
113
 
87
114
  Build toolchain dependencies are pinned with exact version locks in
88
- `bun.lock`. GitHub Actions workflows use SHA-pinned action references
115
+ `bun.lock`. GitHub Actions workflows use [SHA-pinned action references][workflows]
89
116
  throughout with no floating tags. Supply chain integrity is treated as a
90
117
  first-class concern for a cryptography library.
91
118
 
@@ -124,10 +151,13 @@ as the single source of authority.
124
151
  Use GitHub's private vulnerability reporting form:
125
152
  [https://github.com/xero/leviathan-crypto/security/advisories/new][advisory]
126
153
 
127
- This opens a private channel between you and the maintainer. You will
128
- receive a response ASAP. If the vulnerability is confirmed, a fix will be
129
- prioritised and a coordinated disclosure timeline agreed upon before any
130
- public advisory is published.
154
+ This opens a private channel between you and the maintainer, and you will
155
+ receive a response promptly. If the vulnerability is confirmed,
156
+ we will collaborate to fully understand the issue, including a review of
157
+ proposed fixes, so you can track and validate firsthand. Before any public
158
+ advisory is published, we will agree on a coordinated disclosure timeline.
159
+ After disclosure, you are encouraged to publish your own write-up, blog post,
160
+ or research notes, for full hacker scene credit.
131
161
 
132
162
  ### Direct Contact
133
163
 
@@ -141,34 +171,49 @@ If you prefer to contact the maintainer directly:
141
171
 
142
172
  ### Scope
143
173
 
144
- Reports are in scope for:
174
+ **Reports are in scope for:**
145
175
 
146
- - Correctness bugs in cryptographic implementations (wrong output against
147
- test vectors)
148
- - Side-channel vulnerabilities (timing, memory access patterns)
149
176
  - Authentication bypass in AEAD constructions
150
177
  - Key material exposure or improper zeroing
178
+ - Incorrect entropy or CSPRNG weaknesses in Fortuna
179
+ - Side-channel vulnerabilities (timing, memory access patterns)
180
+ - Correctness bugs in cryptographic implementations (wrong output against
181
+ test vectors)
182
+ - Platform-specific behavioral differences (WASM execution, binary output,
183
+ or timing characteristics that differ across operating systems or CPU
184
+ architectures)
151
185
  - Supply chain issues (dependency tampering, workflow compromise)
186
+ - Improper scope of exported symbols
152
187
 
153
- Out of scope:
188
+ **Out of scope:**
154
189
 
155
- - Unpatched vulnerabilities in third-party packages not maintained by this
156
- project
190
+ - Vulnerabilities in third-party packages not maintained by this project.
191
+ This includes optional peer dependencies such as argon2id.
192
+ Please report those directly to their maintainers.
157
193
  - Issues requiring physical access to the user's device
158
-
159
- ---
160
-
161
- [fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
162
- [fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
163
- [rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
164
- [rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
165
- [rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
166
- [serpent]: https://www.cl.cam.ac.uk/~rja14/Papers/serpent.pdf
167
- [utils]: https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal
168
- [biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
169
- [serpent-audit]: https://github.com/xero/leviathan-crypto/wiki/serpent_audit
170
- [argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
171
- [init]: https://github.com/xero/leviathan-crypto/wiki/init
172
- [agents]: https://github.com/xero/leviathan-crypto/blob/main/AGENTS.md
173
- [advisory]: https://github.com/xero/leviathan-crypto/security/advisories/new
174
- [pgp]: https://0w.nz/pgp.pub
194
+ - Theoretical attacks with no practical exploit path (e.g. complexity
195
+ improvements that remain computationally infeasible)
196
+ - Issues in the demo applications that do not affect the core library.
197
+ Please open an issue in the [`leviathan-demos`][demos] repository instead.
198
+
199
+ [fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
200
+ [fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
201
+ [rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
202
+ [rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
203
+ [rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
204
+ [serpent]: https://www.cl.cam.ac.uk/~rja14/Papers/serpent.pdf
205
+ [utils]: https://github.com/xero/leviathan-crypto/wiki/utils#constanttimeequal
206
+ [demos]: https://github.com/xero/leviathan-demos/
207
+ [serpent_audit]: https://github.com/xero/leviathan-crypto/wiki/serpent_audit
208
+ [chacha_audit]: https://github.com/xero/leviathan-crypto/wiki/chacha_audit
209
+ [sha2_audit]: https://github.com/xero/leviathan-crypto/wiki/sha2_audit
210
+ [sha3_audit]: https://github.com/xero/leviathan-crypto/wiki/sha3_audit
211
+ [hmac_audit]: https://github.com/xero/leviathan-crypto/wiki/hmac_audit
212
+ [hkdf_audit]: https://github.com/xero/leviathan-crypto/wiki/hkdf_audit
213
+ [biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
214
+ [argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
215
+ [workflows]: https://github.com/xero/leviathan-crypto/blob/main/scripts/pin-actions.ts
216
+ [init]: https://github.com/xero/leviathan-crypto/wiki/init
217
+ [agents]: https://github.com/xero/leviathan-crypto/blob/main/AGENTS.md
218
+ [advisory]: https://github.com/xero/leviathan-crypto/security/advisories/new
219
+ [pgp]: https://0w.nz/pgp.pub
package/dist/chacha.wasm CHANGED
Binary file
@@ -25,6 +25,7 @@
25
25
  // Uses the init() module cache — call init('chacha20') before constructing.
26
26
  import { getInstance, initModule } from '../init.js';
27
27
  import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
28
+ import { hasSIMD } from '../utils.js';
28
29
  const _embedded = () => import('../embedded/chacha.js').then(m => m.WASM_BASE64);
29
30
  export async function chacha20Init(mode = 'embedded', opts) {
30
31
  return initModule('chacha20', _embedded, mode, opts);
@@ -57,7 +58,8 @@ export class ChaCha20 {
57
58
  const ptOff = this.x.getChunkPtOffset();
58
59
  const ctOff = this.x.getChunkCtOffset();
59
60
  mem.set(chunk, ptOff);
60
- this.x.chachaEncryptChunk(chunk.length);
61
+ const fn = hasSIMD() ? this.x.chachaEncryptChunk_simd : this.x.chachaEncryptChunk;
62
+ fn(chunk.length);
61
63
  return mem.slice(ctOff, ctOff + chunk.length);
62
64
  }
63
65
  beginDecrypt(key, nonce) {
@@ -21,6 +21,8 @@ export interface ChaChaExports {
21
21
  chachaResetCounter(): void;
22
22
  chachaEncryptChunk(n: number): number;
23
23
  chachaDecryptChunk(n: number): number;
24
+ chachaEncryptChunk_simd(n: number): number;
25
+ chachaDecryptChunk_simd(n: number): number;
24
26
  chachaGenPolyKey(): void;
25
27
  hchacha20(): void;
26
28
  polyInit(): void;
@@ -676,9 +676,11 @@ Source: `src/asm/chacha/buffers.ts`
676
676
  | 131492 | 16 | `POLY_S_BUFFER` — s pad: 4 × u32 |
677
677
  | 131508 | 24 | `XCHACHA_NONCE_BUFFER` — full 24-byte XChaCha20 nonce |
678
678
  | 131532 | 32 | `XCHACHA_SUBKEY_BUFFER` — HChaCha20 output (key material) |
679
- | 131564 | | END |
679
+ | 131564 | 4 | *(padding for 16-byte SIMD alignment)* |
680
+ | 131568 | 256 | `CHACHA_SIMD_WORK_BUFFER` — 4-wide inter-block keystream (4 × 64 bytes) |
681
+ | 131824 | — | END |
680
682
 
681
- `wipeBuffers()` zeroes all 14 buffer regions (key, chacha nonce/ctr/block/state, chunk pt/ct, poly key/msg/buf/tag/h/r/rs/s, xchacha nonce/subkey).
683
+ `wipeBuffers()` zeroes all 15 buffer regions (key, chacha nonce/ctr/block/state, chunk pt/ct, poly key/msg/buf/tag/h/r/rs/s, xchacha nonce/subkey, SIMD work).
682
684
 
683
685
  ### SHA-2 module — 3 pages (192 KB)
684
686
 
@@ -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
@@ -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