leviathan-crypto 1.3.1 → 2.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 +129 -76
- package/README.md +166 -221
- package/SECURITY.md +89 -37
- package/dist/chacha20/cipher-suite.d.ts +4 -0
- package/dist/chacha20/cipher-suite.js +78 -0
- package/dist/chacha20/embedded.d.ts +1 -0
- package/dist/chacha20/embedded.js +27 -0
- package/dist/chacha20/index.d.ts +20 -7
- package/dist/chacha20/index.js +41 -14
- package/dist/chacha20/ops.d.ts +1 -1
- package/dist/chacha20/ops.js +19 -18
- package/dist/chacha20/pool-worker.js +77 -0
- package/dist/ct-wasm.d.ts +1 -0
- package/dist/ct-wasm.js +3 -0
- package/dist/ct.wasm +0 -0
- package/dist/docs/aead.md +320 -0
- package/dist/docs/architecture.md +419 -285
- package/dist/docs/argon2id.md +42 -30
- package/dist/docs/chacha20.md +218 -150
- package/dist/docs/exports.md +241 -0
- package/dist/docs/fortuna.md +65 -74
- package/dist/docs/init.md +172 -178
- package/dist/docs/loader.md +87 -132
- package/dist/docs/serpent.md +134 -565
- package/dist/docs/sha2.md +91 -103
- package/dist/docs/sha3.md +70 -36
- package/dist/docs/types.md +93 -16
- package/dist/docs/utils.md +114 -41
- package/dist/embedded/chacha20.d.ts +1 -1
- package/dist/embedded/chacha20.js +2 -1
- package/dist/embedded/kyber.d.ts +1 -0
- package/dist/embedded/kyber.js +3 -0
- package/dist/embedded/serpent.d.ts +1 -1
- package/dist/embedded/serpent.js +2 -1
- package/dist/embedded/sha2.d.ts +1 -1
- package/dist/embedded/sha2.js +2 -1
- package/dist/embedded/sha3.d.ts +1 -1
- package/dist/embedded/sha3.js +2 -1
- package/dist/errors.d.ts +10 -0
- package/dist/{serpent/seal.js → errors.js} +14 -46
- package/dist/fortuna.d.ts +2 -8
- package/dist/fortuna.js +11 -9
- package/dist/index.d.ts +25 -9
- package/dist/index.js +36 -7
- package/dist/init.d.ts +3 -7
- package/dist/init.js +18 -35
- package/dist/keccak/embedded.d.ts +1 -0
- package/dist/keccak/embedded.js +27 -0
- package/dist/keccak/index.d.ts +4 -0
- package/dist/keccak/index.js +31 -0
- package/dist/kyber/embedded.d.ts +1 -0
- package/dist/kyber/embedded.js +27 -0
- package/dist/kyber/indcpa.d.ts +49 -0
- package/dist/kyber/indcpa.js +352 -0
- package/dist/kyber/index.d.ts +38 -0
- package/dist/kyber/index.js +150 -0
- package/dist/kyber/kem.d.ts +21 -0
- package/dist/kyber/kem.js +160 -0
- package/dist/kyber/params.d.ts +14 -0
- package/dist/kyber/params.js +37 -0
- package/dist/kyber/suite.d.ts +13 -0
- package/dist/kyber/suite.js +93 -0
- package/dist/kyber/types.d.ts +98 -0
- package/dist/kyber/types.js +25 -0
- package/dist/kyber/validate.d.ts +19 -0
- package/dist/kyber/validate.js +68 -0
- package/dist/kyber.wasm +0 -0
- package/dist/loader.d.ts +19 -4
- package/dist/loader.js +91 -25
- package/dist/serpent/cipher-suite.d.ts +4 -0
- package/dist/serpent/cipher-suite.js +121 -0
- package/dist/serpent/embedded.d.ts +1 -0
- package/dist/serpent/embedded.js +27 -0
- package/dist/serpent/index.d.ts +6 -37
- package/dist/serpent/index.js +9 -118
- package/dist/serpent/pool-worker.d.ts +1 -0
- package/dist/serpent/pool-worker.js +202 -0
- package/dist/serpent/serpent-cbc.d.ts +30 -0
- package/dist/serpent/serpent-cbc.js +136 -0
- package/dist/sha2/embedded.d.ts +1 -0
- package/dist/sha2/embedded.js +27 -0
- package/dist/sha2/hkdf.js +6 -2
- package/dist/sha2/index.d.ts +3 -2
- package/dist/sha2/index.js +3 -4
- package/dist/sha3/embedded.d.ts +1 -0
- package/dist/sha3/embedded.js +27 -0
- package/dist/sha3/index.d.ts +3 -2
- package/dist/sha3/index.js +3 -4
- package/dist/stream/constants.d.ts +6 -0
- package/dist/stream/constants.js +30 -0
- package/dist/stream/header.d.ts +9 -0
- package/dist/stream/header.js +77 -0
- package/dist/stream/index.d.ts +7 -0
- package/dist/stream/index.js +27 -0
- package/dist/stream/open-stream.d.ts +21 -0
- package/dist/stream/open-stream.js +146 -0
- package/dist/stream/seal-stream-pool.d.ts +38 -0
- package/dist/stream/seal-stream-pool.js +391 -0
- package/dist/stream/seal-stream.d.ts +20 -0
- package/dist/stream/seal-stream.js +142 -0
- package/dist/stream/seal.d.ts +9 -0
- package/dist/stream/seal.js +75 -0
- package/dist/stream/types.d.ts +24 -0
- package/dist/stream/types.js +26 -0
- package/dist/utils.d.ts +12 -7
- package/dist/utils.js +75 -19
- package/dist/wasm-source.d.ts +12 -0
- package/dist/wasm-source.js +26 -0
- package/package.json +13 -5
- package/dist/chacha20/pool.d.ts +0 -52
- package/dist/chacha20/pool.js +0 -188
- package/dist/chacha20/pool.worker.js +0 -37
- package/dist/docs/chacha20_pool.md +0 -309
- package/dist/docs/wasm.md +0 -194
- package/dist/serpent/seal.d.ts +0 -8
- package/dist/serpent/stream-pool.d.ts +0 -48
- package/dist/serpent/stream-pool.js +0 -285
- package/dist/serpent/stream-sealer.d.ts +0 -50
- package/dist/serpent/stream-sealer.js +0 -341
- package/dist/serpent/stream.d.ts +0 -28
- package/dist/serpent/stream.js +0 -205
- package/dist/serpent/stream.worker.d.ts +0 -32
- package/dist/serpent/stream.worker.js +0 -117
- /package/dist/chacha20/{pool.worker.d.ts → pool-worker.d.ts} +0 -0
package/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- **[Version Support](#supported-versions)**
|
|
6
6
|
- **[Security Posture](#security-posture)**
|
|
7
|
-
- **[Cryptanalytic Audits](#cryptanalytic-
|
|
7
|
+
- **[Cryptanalytic Audits](#cryptanalytic-audits)**
|
|
8
8
|
- **[Vulnerability Reporting](#reporting-a-vulnerability)**
|
|
9
9
|
|
|
10
10
|
---
|
|
@@ -12,26 +12,39 @@
|
|
|
12
12
|
## Supported Versions
|
|
13
13
|
|
|
14
14
|
| Version | Supported |
|
|
15
|
-
|
|
16
|
-
|
|
|
17
|
-
| v1.
|
|
15
|
+
| ------- | --------- |
|
|
16
|
+
| v2.0.x | ✓ |
|
|
17
|
+
| v1.4.x | ✗ |
|
|
18
|
+
| v1.3.x | ✗ |
|
|
19
|
+
| v1.2.x | ✗ |
|
|
18
20
|
| v1.1.x | ✗ |
|
|
19
21
|
| v1.0.x | ✗ |
|
|
20
22
|
|
|
23
|
+
> [!CAUTION]
|
|
24
|
+
> **All v1.x releases are deprecated.** Upgrading to v2 is strongly
|
|
25
|
+
> recommended. v1.x releases will not receive security patches.
|
|
26
|
+
|
|
21
27
|
> [!WARNING]
|
|
22
|
-
> v1.
|
|
23
|
-
>
|
|
28
|
+
> **v1.x known issues** (addressed in v2):
|
|
29
|
+
> - Partial WASM buffer wipe on AEAD and serpent auth failure.
|
|
30
|
+
> - HMAC tag and HKDF operations do not zero intermediate key material.
|
|
31
|
+
> - TransformStream error paths leak derived keys.
|
|
32
|
+
> - Pool workers copy result buffers.
|
|
33
|
+
> - Scalar JS `constantTimeEqual` was "best-effort only".
|
|
34
|
+
|
|
24
35
|
|
|
25
36
|
## Security Posture
|
|
26
37
|
|
|
27
|
-
[`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library.
|
|
28
|
-
it is the primary design constraint at every
|
|
38
|
+
[`leviathan-crypto`](https://leviathan.3xi.club) is a cryptography library.
|
|
39
|
+
Security is not an afterthought, it is the primary design constraint at every
|
|
40
|
+
layer of the stack.
|
|
29
41
|
|
|
30
42
|
### Algorithm Correctness
|
|
31
43
|
|
|
32
44
|
Every primitive in this library was implemented by hand in AssemblyScript
|
|
33
45
|
against the authoritative specification for that algorithm:
|
|
34
46
|
[FIPS 180-4][fips180] (SHA-2), [FIPS 202][fips202] (SHA-3),
|
|
47
|
+
[FIPS 203][fips203] (ML-KEM),
|
|
35
48
|
[RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
|
|
36
49
|
[RFC 5869][rfc5869] (HKDF), and the original
|
|
37
50
|
[Serpent-256 specification][serpent] and S-box reference. No algorithm was
|
|
@@ -51,9 +64,13 @@ timing-safe cipher implementation approach available in a WASM runtime,
|
|
|
51
64
|
where JIT optimisation can otherwise introduce observable timing variation.
|
|
52
65
|
|
|
53
66
|
All security-sensitive comparisons (e.g. MAC verification, padding validation)
|
|
54
|
-
use
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
use [`constantTimeEqual`][utils], which is backed by a dedicated WASM SIMD module
|
|
68
|
+
(v128 XOR accumulate + `any_true`) when WebAssembly SIMD is available. The WASM
|
|
69
|
+
execution path eliminates JIT short-circuiting and speculative optimization as
|
|
70
|
+
theoretical side-channel vectors. On runtimes without SIMD (sha2/sha3-only
|
|
71
|
+
consumers), the function falls back to a JS XOR-accumulate loop. This is best-effort
|
|
72
|
+
constant-time, not a hardware-level guarantee. WASM comparison memory is
|
|
73
|
+
wiped after every call.
|
|
57
74
|
|
|
58
75
|
### WASM Execution Model
|
|
59
76
|
|
|
@@ -62,20 +79,36 @@ JavaScript JIT. WASM execution is deterministic and not subject to JIT
|
|
|
62
79
|
speculation or optimisation. Each primitive family compiles to its own
|
|
63
80
|
isolated binary with its own linear memory. For example, key material in
|
|
64
81
|
the Serpent module cannot interact with memory in the SHA-3 module,
|
|
65
|
-
even in principle.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
even in principle. A dedicated WASM module handles constant-time comparison
|
|
83
|
+
with its own single-page memory that is wiped after every call.
|
|
84
|
+
|
|
85
|
+
Serpent and ChaCha20 modules require WebAssembly SIMD (`v128` instructions).
|
|
86
|
+
`init()` and `initModule()` perform a SIMD preflight check and throw a
|
|
87
|
+
clear error on runtimes without support. SIMD has been a baseline feature
|
|
88
|
+
of all major browsers and runtimes since 2021. SHA-2 and SHA-3 modules
|
|
89
|
+
run on any WASM-capable runtime.
|
|
90
|
+
|
|
91
|
+
The `kyber` module requires WebAssembly SIMD for NTT and polynomial
|
|
92
|
+
arithmetic (`v128` instructions). The SIMD preflight check is applied on
|
|
93
|
+
`init()` alongside serpent and chacha20. Its linear memory is independent
|
|
94
|
+
from all other modules. The kyber module's constant-time path (FO transform
|
|
95
|
+
decapsulation) uses dedicated `ct_verify` and `ct_cmov` functions implemented
|
|
96
|
+
in the kyber WASM binary. The comparison never passes through JavaScript.
|
|
97
|
+
|
|
98
|
+
### Cryptanalytic Audits
|
|
99
|
+
|
|
100
|
+
All primitives undergo periodic cryptographic implementation reviews. See the [audit index][audits] for a full summary.
|
|
101
|
+
|
|
102
|
+
| Primitive | Audit Description |
|
|
103
|
+
| ------------------------------ | -------------------------------------------------------------------------------------- |
|
|
104
|
+
| [serpent_audit][serpent_audit] | Correctness verification, side-channel analysis, cryptanalytic attack paper review |
|
|
105
|
+
| [chacha_audit][chacha_audit] | XChaCha20-Poly1305 correctness, Poly1305 field arithmetic, HChaCha20 nonce extension |
|
|
106
|
+
| [sha2_audit][sha2_audit] | SHA-256/512/384 correctness, HMAC and HKDF composition, constant verification |
|
|
107
|
+
| [sha3_audit][sha3_audit] | Keccak permutation correctness, θ/ρ/π/χ/ι step verification, round constant derivation |
|
|
108
|
+
| [hmac_audit][hmac_audit] | HMAC-SHA256/512/384 construction, key processing, RFC 4231 vector coverage |
|
|
109
|
+
| [hkdf_audit][hkdf_audit] | HKDF extract-then-expand, info field domain separation, stream key derivation |
|
|
110
|
+
| [kyber_audit][kyber_audit] | ML-KEM FIPS 203 correctness, NTT/Montgomery/Barrett verification, FO transform CT analysis, ACVP validation |
|
|
111
|
+
| [stream_audit][stream_audit] | Streaming AEAD composition, counter nonce binding, final-chunk detection, key wipe paths |
|
|
79
112
|
|
|
80
113
|
#### Additional Serpent-256 research
|
|
81
114
|
|
|
@@ -92,17 +125,32 @@ See: [`xero/BicliqueFinder/biclique_research.md`][biclique]
|
|
|
92
125
|
|
|
93
126
|
### Authenticated Encryption by Default
|
|
94
127
|
|
|
95
|
-
Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`, `ChaCha20`) and
|
|
129
|
+
stateless caller-managed-nonce primitives (`ChaCha20Poly1305`,
|
|
130
|
+
`XChaCha20Poly1305`) are exposed for power users but are not the recommended
|
|
131
|
+
entry point. The primary API surfaces (`Seal`, `SealStream`, `OpenStream`,
|
|
132
|
+
`SealStreamPool`, and `KyberSuite`) are authenticated by construction with
|
|
133
|
+
internally managed nonces.
|
|
134
|
+
|
|
135
|
+
**All streaming constructions satisfy the _Cryptographic Doom Principle_:**
|
|
136
|
+
|
|
137
|
+
`SealStream` / `OpenStream` with `SerpentCipher` uses encrypt-then-MAC
|
|
138
|
+
(SerpentCbc + HMAC-SHA256). MAC verification is the unconditional gate on
|
|
139
|
+
the open path. Decryption is unreachable until that gate clears. HKDF key
|
|
140
|
+
derivation with the stream nonce and counter-nonce domain separation
|
|
141
|
+
extends this guarantee to full stream integrity.
|
|
142
|
+
|
|
143
|
+
`SealStream` / `OpenStream` with `XChaCha20Cipher` uses XChaCha20-Poly1305
|
|
144
|
+
AEAD per chunk. The Poly1305 tag is verified inside the WASM `aeadDecrypt`
|
|
145
|
+
call before any plaintext is produced. On authentication failure, the full
|
|
146
|
+
chunk output buffer is wiped and plaintext bytes are never returned.
|
|
147
|
+
Counter nonces with TAG_DATA/TAG_FINAL final-flag domain separation ensure
|
|
148
|
+
reorder, splice, truncation, and cross-stream substitution all fail AEAD
|
|
149
|
+
verification before decryption.
|
|
150
|
+
|
|
151
|
+
`SealStreamPool` delegates per-chunk AEAD to isolated Web Workers. Each
|
|
152
|
+
worker holds its own derived subkey and WASM instance. Any authentication
|
|
153
|
+
error kills all workers, wipes all key material, and marks the pool dead. No retry, no partial results.
|
|
106
154
|
|
|
107
155
|
### Dependency Management
|
|
108
156
|
|
|
@@ -163,7 +211,7 @@ or research notes, for full hacker scene credit.
|
|
|
163
211
|
|
|
164
212
|
If you prefer to contact the maintainer directly:
|
|
165
213
|
|
|
166
|
-
- **Email:** x﹫xero.style
|
|
214
|
+
- **Email:** x﹫xero.style · PGP: [`0xAC1D0000`][pgp]
|
|
167
215
|
- **Matrix:** x0﹫rx.haunted.computer
|
|
168
216
|
|
|
169
217
|
> [!NOTE]
|
|
@@ -198,6 +246,7 @@ If you prefer to contact the maintainer directly:
|
|
|
198
246
|
|
|
199
247
|
[fips180]: https://csrc.nist.gov/publications/detail/fips/180/4/final
|
|
200
248
|
[fips202]: https://csrc.nist.gov/publications/detail/fips/202/final
|
|
249
|
+
[fips203]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.pdf
|
|
201
250
|
[rfc8439]: https://www.rfc-editor.org/rfc/rfc8439
|
|
202
251
|
[rfc2104]: https://www.rfc-editor.org/rfc/rfc2104
|
|
203
252
|
[rfc5869]: https://www.rfc-editor.org/rfc/rfc5869
|
|
@@ -210,6 +259,9 @@ If you prefer to contact the maintainer directly:
|
|
|
210
259
|
[sha3_audit]: https://github.com/xero/leviathan-crypto/wiki/sha3_audit
|
|
211
260
|
[hmac_audit]: https://github.com/xero/leviathan-crypto/wiki/hmac_audit
|
|
212
261
|
[hkdf_audit]: https://github.com/xero/leviathan-crypto/wiki/hkdf_audit
|
|
262
|
+
[kyber_audit]: https://github.com/xero/leviathan-crypto/wiki/kyber_audit
|
|
263
|
+
[stream_audit]: https://github.com/xero/leviathan-crypto/wiki/stream_audit
|
|
264
|
+
[audits]: https://github.com/xero/leviathan-crypto/wiki/audits
|
|
213
265
|
[biclique]: https://github.com/xero/BicliqueFinder/blob/main/biclique-research.md
|
|
214
266
|
[argon2id-wiki]: https://github.com/xero/leviathan-crypto/wiki/argon2id
|
|
215
267
|
[workflows]: https://github.com/xero/leviathan-crypto/blob/main/scripts/pin-actions.ts
|
|
@@ -0,0 +1,78 @@
|
|
|
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/chacha20/cipher-suite.ts
|
|
23
|
+
//
|
|
24
|
+
// XChaCha20Cipher — CipherSuite implementation for the STREAM construction.
|
|
25
|
+
// HKDF-SHA-256 key derivation → HChaCha20 subkey → ChaCha20-Poly1305 per chunk.
|
|
26
|
+
import { getInstance } from '../init.js';
|
|
27
|
+
import { HKDF_SHA256 } from '../sha2/index.js';
|
|
28
|
+
import { aeadEncrypt, aeadDecrypt, deriveSubkey } from './ops.js';
|
|
29
|
+
import { wipe, randomBytes } from '../utils.js';
|
|
30
|
+
const INFO = new TextEncoder().encode('xchacha20-sealstream-v2');
|
|
31
|
+
function getExports() {
|
|
32
|
+
return getInstance('chacha20').exports;
|
|
33
|
+
}
|
|
34
|
+
export const XChaCha20Cipher = {
|
|
35
|
+
formatEnum: 0x01,
|
|
36
|
+
formatName: 'xchacha20',
|
|
37
|
+
hkdfInfo: 'xchacha20-sealstream-v2',
|
|
38
|
+
keySize: 32,
|
|
39
|
+
kemCtSize: 0,
|
|
40
|
+
tagSize: 16,
|
|
41
|
+
padded: false,
|
|
42
|
+
wasmModules: ['chacha20'],
|
|
43
|
+
keygen() {
|
|
44
|
+
return randomBytes(32);
|
|
45
|
+
},
|
|
46
|
+
deriveKeys(masterKey, nonce, _kemCt) {
|
|
47
|
+
const hkdf = new HKDF_SHA256();
|
|
48
|
+
const streamKey = hkdf.derive(masterKey, nonce, INFO, 32);
|
|
49
|
+
hkdf.dispose();
|
|
50
|
+
// HChaCha20 subkey derivation — nonce[0:16] as XChaCha input
|
|
51
|
+
const x = getExports();
|
|
52
|
+
const subkey = deriveSubkey(x, streamKey, nonce);
|
|
53
|
+
wipe(streamKey);
|
|
54
|
+
return { bytes: subkey };
|
|
55
|
+
},
|
|
56
|
+
sealChunk(keys, counterNonce, chunk, aad) {
|
|
57
|
+
const x = getExports();
|
|
58
|
+
const { ciphertext, tag } = aeadEncrypt(x, keys.bytes, counterNonce, chunk, aad ?? new Uint8Array(0));
|
|
59
|
+
const out = new Uint8Array(ciphertext.length + 16);
|
|
60
|
+
out.set(ciphertext);
|
|
61
|
+
out.set(tag, ciphertext.length);
|
|
62
|
+
return out;
|
|
63
|
+
},
|
|
64
|
+
openChunk(keys, counterNonce, chunk, aad) {
|
|
65
|
+
if (chunk.length < 16)
|
|
66
|
+
throw new RangeError(`chunk too short for 16-byte tag (got ${chunk.length})`);
|
|
67
|
+
const x = getExports();
|
|
68
|
+
const ct = chunk.subarray(0, chunk.length - 16);
|
|
69
|
+
const tag = chunk.subarray(chunk.length - 16);
|
|
70
|
+
return aeadDecrypt(x, keys.bytes, counterNonce, ct, tag, aad ?? new Uint8Array(0), 'xchacha20-poly1305');
|
|
71
|
+
},
|
|
72
|
+
wipeKeys(keys) {
|
|
73
|
+
wipe(keys.bytes);
|
|
74
|
+
},
|
|
75
|
+
createPoolWorker() {
|
|
76
|
+
return new Worker(new URL('./pool-worker.js', import.meta.url), { type: 'module' });
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WASM_GZ_BASE64 as chacha20Wasm } from '../embedded/chacha20.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
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/chacha20/embedded.ts
|
|
23
|
+
//
|
|
24
|
+
// Exports the gzip+base64 chacha20 WASM blob for use as a WasmSource.
|
|
25
|
+
// This is the only file in the chacha20 subpath that references the embedded blob.
|
|
26
|
+
// Import via `leviathan-crypto/chacha20/embedded`.
|
|
27
|
+
export { WASM_GZ_BASE64 as chacha20Wasm } from '../embedded/chacha20.js';
|
package/dist/chacha20/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { WasmSource } from '../wasm-source.js';
|
|
2
|
+
import { AuthenticationError } from '../errors.js';
|
|
3
|
+
export { AuthenticationError };
|
|
4
|
+
export declare function chacha20Init(source: WasmSource): Promise<void>;
|
|
5
|
+
export type { WasmSource };
|
|
3
6
|
export declare class ChaCha20 {
|
|
4
7
|
private readonly x;
|
|
5
8
|
constructor();
|
|
@@ -18,17 +21,22 @@ export declare class Poly1305 {
|
|
|
18
21
|
/**
|
|
19
22
|
* ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
|
|
20
23
|
*
|
|
24
|
+
* `encrypt()` returns ciphertext || tag(16) as a single Uint8Array.
|
|
25
|
+
* `decrypt()` accepts the same combined format and splits internally.
|
|
26
|
+
*
|
|
27
|
+
* Single-use encrypt guard: `encrypt()` may only be called once per instance.
|
|
28
|
+
* Create a new instance for each encryption to prevent nonce reuse.
|
|
29
|
+
*
|
|
21
30
|
* `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
|
|
22
31
|
* no early return on mismatch. Plaintext is never returned on failure.
|
|
23
32
|
*/
|
|
24
33
|
export declare class ChaCha20Poly1305 {
|
|
25
34
|
private readonly x;
|
|
35
|
+
private _used;
|
|
26
36
|
constructor();
|
|
27
|
-
encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array):
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
37
|
+
encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
38
|
+
decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, // ciphertext || tag(16) combined
|
|
39
|
+
aad?: Uint8Array): Uint8Array;
|
|
32
40
|
dispose(): void;
|
|
33
41
|
}
|
|
34
42
|
/**
|
|
@@ -37,13 +45,18 @@ export declare class ChaCha20Poly1305 {
|
|
|
37
45
|
* Recommended authenticated encryption primitive for most use cases.
|
|
38
46
|
* Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
|
|
39
47
|
*
|
|
48
|
+
* Single-use encrypt guard: `encrypt()` may only be called once per instance.
|
|
49
|
+
* Create a new instance for each encryption to prevent nonce reuse.
|
|
50
|
+
*
|
|
40
51
|
* `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
|
|
41
52
|
*/
|
|
42
53
|
export declare class XChaCha20Poly1305 {
|
|
43
54
|
private readonly x;
|
|
55
|
+
private _used;
|
|
44
56
|
constructor();
|
|
45
57
|
encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
46
58
|
decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
47
59
|
dispose(): void;
|
|
48
60
|
}
|
|
61
|
+
export { XChaCha20Cipher } from './cipher-suite.js';
|
|
49
62
|
export declare function _chachaReady(): boolean;
|
package/dist/chacha20/index.js
CHANGED
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
// src/ts/chacha20/index.ts
|
|
23
23
|
//
|
|
24
24
|
// Public API classes for the ChaCha20 WASM module.
|
|
25
|
-
// Uses the init() module cache — call
|
|
25
|
+
// Uses the init() module cache — call chacha20Init(source) before constructing.
|
|
26
26
|
import { getInstance, initModule } from '../init.js';
|
|
27
27
|
import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
export async function chacha20Init(
|
|
31
|
-
return initModule('chacha20',
|
|
28
|
+
import { AuthenticationError } from '../errors.js';
|
|
29
|
+
export { AuthenticationError };
|
|
30
|
+
export async function chacha20Init(source) {
|
|
31
|
+
return initModule('chacha20', source);
|
|
32
32
|
}
|
|
33
33
|
function getExports() {
|
|
34
34
|
return getInstance('chacha20').exports;
|
|
@@ -58,8 +58,7 @@ export class ChaCha20 {
|
|
|
58
58
|
const ptOff = this.x.getChunkPtOffset();
|
|
59
59
|
const ctOff = this.x.getChunkCtOffset();
|
|
60
60
|
mem.set(chunk, ptOff);
|
|
61
|
-
|
|
62
|
-
fn(chunk.length);
|
|
61
|
+
this.x.chachaEncryptChunk_simd(chunk.length);
|
|
63
62
|
return mem.slice(ctOff, ctOff + chunk.length);
|
|
64
63
|
}
|
|
65
64
|
beginDecrypt(key, nonce) {
|
|
@@ -105,29 +104,47 @@ export class Poly1305 {
|
|
|
105
104
|
/**
|
|
106
105
|
* ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
|
|
107
106
|
*
|
|
107
|
+
* `encrypt()` returns ciphertext || tag(16) as a single Uint8Array.
|
|
108
|
+
* `decrypt()` accepts the same combined format and splits internally.
|
|
109
|
+
*
|
|
110
|
+
* Single-use encrypt guard: `encrypt()` may only be called once per instance.
|
|
111
|
+
* Create a new instance for each encryption to prevent nonce reuse.
|
|
112
|
+
*
|
|
108
113
|
* `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
|
|
109
114
|
* no early return on mismatch. Plaintext is never returned on failure.
|
|
110
115
|
*/
|
|
111
116
|
export class ChaCha20Poly1305 {
|
|
112
117
|
x;
|
|
118
|
+
_used = false;
|
|
113
119
|
constructor() {
|
|
114
120
|
this.x = getExports();
|
|
115
121
|
}
|
|
116
122
|
encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
123
|
+
if (this._used)
|
|
124
|
+
throw new Error('leviathan-crypto: encrypt() already called on this instance. '
|
|
125
|
+
+ 'Create a new instance for each encryption to prevent nonce reuse.');
|
|
117
126
|
if (key.length !== 32)
|
|
118
127
|
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
119
128
|
if (nonce.length !== 12)
|
|
120
129
|
throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
const { ciphertext, tag } = aeadEncrypt(this.x, key, nonce, plaintext, aad);
|
|
131
|
+
this._used = true;
|
|
132
|
+
const out = new Uint8Array(ciphertext.length + 16);
|
|
133
|
+
out.set(ciphertext);
|
|
134
|
+
out.set(tag, ciphertext.length);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
decrypt(key, nonce, ciphertext, // ciphertext || tag(16) combined
|
|
138
|
+
aad = new Uint8Array(0)) {
|
|
124
139
|
if (key.length !== 32)
|
|
125
140
|
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
126
141
|
if (nonce.length !== 12)
|
|
127
142
|
throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
|
|
128
|
-
if (
|
|
129
|
-
throw new RangeError(`
|
|
130
|
-
|
|
143
|
+
if (ciphertext.length < 16)
|
|
144
|
+
throw new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`);
|
|
145
|
+
const ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
146
|
+
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
147
|
+
return aeadDecrypt(this.x, key, nonce, ct, tag, aad);
|
|
131
148
|
}
|
|
132
149
|
dispose() {
|
|
133
150
|
this.x.wipeBuffers();
|
|
@@ -140,19 +157,28 @@ export class ChaCha20Poly1305 {
|
|
|
140
157
|
* Recommended authenticated encryption primitive for most use cases.
|
|
141
158
|
* Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
|
|
142
159
|
*
|
|
160
|
+
* Single-use encrypt guard: `encrypt()` may only be called once per instance.
|
|
161
|
+
* Create a new instance for each encryption to prevent nonce reuse.
|
|
162
|
+
*
|
|
143
163
|
* `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
|
|
144
164
|
*/
|
|
145
165
|
export class XChaCha20Poly1305 {
|
|
146
166
|
x;
|
|
167
|
+
_used = false;
|
|
147
168
|
constructor() {
|
|
148
169
|
this.x = getExports();
|
|
149
170
|
}
|
|
150
171
|
encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
172
|
+
if (this._used)
|
|
173
|
+
throw new Error('leviathan-crypto: encrypt() already called on this instance. '
|
|
174
|
+
+ 'Create a new instance for each encryption to prevent nonce reuse.');
|
|
151
175
|
if (key.length !== 32)
|
|
152
176
|
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
153
177
|
if (nonce.length !== 24)
|
|
154
178
|
throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
|
|
155
|
-
|
|
179
|
+
const result = xcEncrypt(this.x, key, nonce, plaintext, aad);
|
|
180
|
+
this._used = true;
|
|
181
|
+
return result;
|
|
156
182
|
}
|
|
157
183
|
decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
|
|
158
184
|
if (key.length !== 32)
|
|
@@ -167,6 +193,7 @@ export class XChaCha20Poly1305 {
|
|
|
167
193
|
this.x.wipeBuffers();
|
|
168
194
|
}
|
|
169
195
|
}
|
|
196
|
+
export { XChaCha20Cipher } from './cipher-suite.js';
|
|
170
197
|
// ── Ready check ──────────────────────────────────────────────────────────────
|
|
171
198
|
export function _chachaReady() {
|
|
172
199
|
try {
|
package/dist/chacha20/ops.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare function aeadEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Ui
|
|
|
5
5
|
tag: Uint8Array;
|
|
6
6
|
};
|
|
7
7
|
/** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
|
|
8
|
-
export declare function aeadDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function aeadDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad: Uint8Array, cipherName?: string): Uint8Array;
|
|
9
9
|
/** HChaCha20 subkey derivation — first 16 bytes of nonce. */
|
|
10
10
|
export declare function deriveSubkey(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array): Uint8Array;
|
|
11
11
|
/** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
|
package/dist/chacha20/ops.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// ChaChaExports explicitly. Used by both the class wrappers (index.ts)
|
|
5
5
|
// and the pool worker (pool.worker.ts), eliminating duplication.
|
|
6
6
|
import { constantTimeEqual } from '../utils.js';
|
|
7
|
+
import { AuthenticationError } from '../errors.js';
|
|
7
8
|
// ── Module-private helpers ───────────────────────────────────────────────────
|
|
8
9
|
function polyFeed(x, data) {
|
|
9
10
|
if (data.length === 0)
|
|
@@ -20,16 +21,14 @@ function polyFeed(x, data) {
|
|
|
20
21
|
}
|
|
21
22
|
function lenBlock(aadLen, ctLen) {
|
|
22
23
|
const b = new Uint8Array(16);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
n >>>= 8;
|
|
32
|
-
}
|
|
24
|
+
const dv = new DataView(b.buffer);
|
|
25
|
+
// RFC 8439 §2.8 — 64-bit LE lengths.
|
|
26
|
+
// JS numbers are f64 — write low 32 bits directly, high bits via
|
|
27
|
+
// Math.floor(n / 2^32). Safe for n ≤ Number.MAX_SAFE_INTEGER.
|
|
28
|
+
dv.setUint32(0, aadLen >>> 0, true);
|
|
29
|
+
dv.setUint32(4, Math.floor(aadLen / 0x100000000) >>> 0, true);
|
|
30
|
+
dv.setUint32(8, ctLen >>> 0, true);
|
|
31
|
+
dv.setUint32(12, Math.floor(ctLen / 0x100000000) >>> 0, true);
|
|
33
32
|
return b;
|
|
34
33
|
}
|
|
35
34
|
// ── Inner AEAD (12-byte nonce) ───────────────────────────────────────────────
|
|
@@ -42,7 +41,6 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
42
41
|
// Step 1: Generate Poly1305 one-time key at counter=0 (RFC 8439 §2.6)
|
|
43
42
|
mem.set(key, x.getKeyOffset());
|
|
44
43
|
mem.set(nonce, x.getChachaNonceOffset());
|
|
45
|
-
x.chachaSetCounter(1);
|
|
46
44
|
x.chachaLoadKey();
|
|
47
45
|
x.chachaGenPolyKey();
|
|
48
46
|
// Step 2: Initialise Poly1305
|
|
@@ -57,7 +55,7 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
57
55
|
x.chachaLoadKey();
|
|
58
56
|
// Step 5: Encrypt
|
|
59
57
|
mem.set(plaintext, x.getChunkPtOffset());
|
|
60
|
-
x.
|
|
58
|
+
x.chachaEncryptChunk_simd(plaintext.length);
|
|
61
59
|
const ctOff = x.getChunkCtOffset();
|
|
62
60
|
const ciphertext = new Uint8Array(x.memory.buffer).slice(ctOff, ctOff + plaintext.length);
|
|
63
61
|
// Step 6: MAC ciphertext + pad
|
|
@@ -74,7 +72,7 @@ export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
|
74
72
|
return { ciphertext, tag };
|
|
75
73
|
}
|
|
76
74
|
/** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
|
|
77
|
-
export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
75
|
+
export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad, cipherName = 'chacha20-poly1305') {
|
|
78
76
|
const maxChunk = x.getChunkSize();
|
|
79
77
|
if (ciphertext.length > maxChunk)
|
|
80
78
|
throw new RangeError(`ciphertext exceeds ${maxChunk} bytes — split into smaller chunks`);
|
|
@@ -82,7 +80,6 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
|
82
80
|
// Compute expected tag
|
|
83
81
|
mem.set(key, x.getKeyOffset());
|
|
84
82
|
mem.set(nonce, x.getChachaNonceOffset());
|
|
85
|
-
x.chachaSetCounter(1);
|
|
86
83
|
x.chachaLoadKey();
|
|
87
84
|
x.chachaGenPolyKey();
|
|
88
85
|
x.polyInit();
|
|
@@ -99,13 +96,17 @@ export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
|
99
96
|
// Constant-time tag comparison
|
|
100
97
|
const tagOff = x.getPolyTagOffset();
|
|
101
98
|
const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
102
|
-
if (!constantTimeEqual(expectedTag, tag))
|
|
103
|
-
|
|
99
|
+
if (!constantTimeEqual(expectedTag, tag)) {
|
|
100
|
+
// Wipe the full chunk output buffer — defense-in-depth before throwing
|
|
101
|
+
const ctOff = x.getChunkCtOffset();
|
|
102
|
+
mem.fill(0, ctOff, ctOff + maxChunk);
|
|
103
|
+
throw new AuthenticationError(cipherName);
|
|
104
|
+
}
|
|
104
105
|
// Decrypt only after authentication succeeds
|
|
105
106
|
x.chachaSetCounter(1);
|
|
106
107
|
x.chachaLoadKey();
|
|
107
108
|
new Uint8Array(x.memory.buffer).set(ciphertext, x.getChunkPtOffset());
|
|
108
|
-
x.
|
|
109
|
+
x.chachaEncryptChunk_simd(ciphertext.length);
|
|
109
110
|
const ptOff = x.getChunkCtOffset();
|
|
110
111
|
return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
|
|
111
112
|
}
|
|
@@ -142,5 +143,5 @@ export function xcDecrypt(x, key, nonce, ciphertext, aad) {
|
|
|
142
143
|
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
143
144
|
const subkey = deriveSubkey(x, key, nonce);
|
|
144
145
|
const inner = innerNonce(nonce);
|
|
145
|
-
return aeadDecrypt(x, subkey, inner, ct, tag, aad);
|
|
146
|
+
return aeadDecrypt(x, subkey, inner, ct, tag, aad, 'xchacha20-poly1305');
|
|
146
147
|
}
|