leviathan-crypto 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +265 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +174 -0
- package/dist/chacha.wasm +0 -0
- package/dist/chacha20/index.d.ts +49 -0
- package/dist/chacha20/index.js +177 -0
- package/dist/chacha20/ops.d.ts +16 -0
- package/dist/chacha20/ops.js +146 -0
- package/dist/chacha20/pool.d.ts +52 -0
- package/dist/chacha20/pool.js +188 -0
- package/dist/chacha20/pool.worker.d.ts +1 -0
- package/dist/chacha20/pool.worker.js +37 -0
- package/dist/chacha20/types.d.ts +30 -0
- package/dist/chacha20/types.js +1 -0
- package/dist/docs/architecture.md +795 -0
- package/dist/docs/argon2id.md +290 -0
- package/dist/docs/chacha20.md +602 -0
- package/dist/docs/chacha20_pool.md +306 -0
- package/dist/docs/fortuna.md +322 -0
- package/dist/docs/init.md +308 -0
- package/dist/docs/loader.md +206 -0
- package/dist/docs/serpent.md +914 -0
- package/dist/docs/sha2.md +620 -0
- package/dist/docs/sha3.md +509 -0
- package/dist/docs/types.md +198 -0
- package/dist/docs/utils.md +273 -0
- package/dist/docs/wasm.md +193 -0
- package/dist/embedded/chacha.d.ts +1 -0
- package/dist/embedded/chacha.js +2 -0
- package/dist/embedded/serpent.d.ts +1 -0
- package/dist/embedded/serpent.js +2 -0
- package/dist/embedded/sha2.d.ts +1 -0
- package/dist/embedded/sha2.js +2 -0
- package/dist/embedded/sha3.d.ts +1 -0
- package/dist/embedded/sha3.js +2 -0
- package/dist/fortuna.d.ts +72 -0
- package/dist/fortuna.js +445 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +44 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +49 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.js +30 -0
- package/dist/serpent/index.d.ts +65 -0
- package/dist/serpent/index.js +242 -0
- package/dist/serpent/seal.d.ts +8 -0
- package/dist/serpent/seal.js +70 -0
- package/dist/serpent/stream-encoder.d.ts +20 -0
- package/dist/serpent/stream-encoder.js +167 -0
- package/dist/serpent/stream-pool.d.ts +48 -0
- package/dist/serpent/stream-pool.js +285 -0
- package/dist/serpent/stream-sealer.d.ts +34 -0
- package/dist/serpent/stream-sealer.js +223 -0
- package/dist/serpent/stream.d.ts +28 -0
- package/dist/serpent/stream.js +205 -0
- package/dist/serpent/stream.worker.d.ts +32 -0
- package/dist/serpent/stream.worker.js +117 -0
- package/dist/serpent/types.d.ts +5 -0
- package/dist/serpent/types.js +1 -0
- package/dist/serpent.wasm +0 -0
- package/dist/sha2/hkdf.d.ts +16 -0
- package/dist/sha2/hkdf.js +108 -0
- package/dist/sha2/index.d.ts +40 -0
- package/dist/sha2/index.js +190 -0
- package/dist/sha2/types.d.ts +5 -0
- package/dist/sha2/types.js +1 -0
- package/dist/sha2.wasm +0 -0
- package/dist/sha3/index.d.ts +55 -0
- package/dist/sha3/index.js +246 -0
- package/dist/sha3/types.d.ts +5 -0
- package/dist/sha3/types.js +1 -0
- package/dist/sha3.wasm +0 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +26 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +169 -0
- package/package.json +90 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Leviathan Crypto Library Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---------|-----------|
|
|
7
|
+
| v1.x | ✓ |
|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> v1.0.0 is the current stable release.
|
|
11
|
+
> This table will be updated as new versions ship.
|
|
12
|
+
|
|
13
|
+
## Security Posture
|
|
14
|
+
|
|
15
|
+
leviathan-crypto is a cryptography library. Security is not an afterthought,
|
|
16
|
+
it is the primary design constraint at every layer of the stack.
|
|
17
|
+
|
|
18
|
+
### Algorithm Correctness
|
|
19
|
+
|
|
20
|
+
Every primitive in this library was implemented by hand in AssemblyScript
|
|
21
|
+
against the authoritative specification for that algorithm:
|
|
22
|
+
[FIPS 180-4][fips180] (SHA-2), [FIPS 202][fips202] (SHA-3),
|
|
23
|
+
[RFC 8439][rfc8439] (ChaCha20-Poly1305), [RFC 2104][rfc2104] (HMAC),
|
|
24
|
+
[RFC 5869][rfc5869] (HKDF), and the original
|
|
25
|
+
[Serpent-256 specification][serpent] and S-box reference. No algorithm was
|
|
26
|
+
ported from an existing implementation — the specs are the source of truth.
|
|
27
|
+
|
|
28
|
+
All implementations are verified against published known-answer test vectors
|
|
29
|
+
from NIST, RFC appendices, NESSIE, and the Argon2 reference suite. Vectors
|
|
30
|
+
are immutable: if an implementation produces incorrect output, the
|
|
31
|
+
implementation is fixed — vectors are never adjusted to match code.
|
|
32
|
+
|
|
33
|
+
### Side-Channel Resistance
|
|
34
|
+
|
|
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
|
|
37
|
+
bit is processed unconditionally on every block. This is the most
|
|
38
|
+
timing-safe cipher implementation approach available in a WASM runtime,
|
|
39
|
+
where JIT optimisation can otherwise introduce observable timing variation.
|
|
40
|
+
|
|
41
|
+
All security-sensitive comparisons (MAC verification, padding validation)
|
|
42
|
+
use XOR-accumulate patterns with no early return on mismatch.
|
|
43
|
+
[`constantTimeEqual`][utils] is the mandated comparison function throughout
|
|
44
|
+
the library and its demos.
|
|
45
|
+
|
|
46
|
+
### WASM Execution Model
|
|
47
|
+
|
|
48
|
+
All cryptographic computation runs in WebAssembly, isolated outside the
|
|
49
|
+
JavaScript JIT. WASM execution is deterministic and not subject to JIT
|
|
50
|
+
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.
|
|
53
|
+
|
|
54
|
+
### Cryptanalytic Review
|
|
55
|
+
|
|
56
|
+
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
|
|
59
|
+
ciphertexts. This provides less than one bit of advantage over exhaustive
|
|
60
|
+
key search and has zero practical impact. Independent research conducted
|
|
61
|
+
against this implementation improved on the published result by −0.20 bits
|
|
62
|
+
through systematic parameter search, confirming no structural weakness
|
|
63
|
+
beyond what the published literature describes.
|
|
64
|
+
|
|
65
|
+
See: [`xero/BicliqueFinder/biclique_research.md`][biclique] and
|
|
66
|
+
[`leviathan-crypto/wiki/serpent_audit`][serpent-audit] for the full
|
|
67
|
+
analysis.
|
|
68
|
+
|
|
69
|
+
### Authenticated Encryption by Default
|
|
70
|
+
|
|
71
|
+
Raw unauthenticated cipher modes (`SerpentCbc`, `SerpentCtr`) are exposed
|
|
72
|
+
for power users but are not the recommended entry point. The primary API
|
|
73
|
+
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
|
|
78
|
+
guarantee to full stream integrity.
|
|
79
|
+
|
|
80
|
+
### Dependency Management
|
|
81
|
+
|
|
82
|
+
The library has _zero_ runtime JavaScript dependencies by design.
|
|
83
|
+
`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].
|
|
86
|
+
|
|
87
|
+
Build toolchain dependencies are pinned with exact version locks in
|
|
88
|
+
`bun.lock`. GitHub Actions workflows use SHA-pinned action references
|
|
89
|
+
throughout with no floating tags. Supply chain integrity is treated as a
|
|
90
|
+
first-class concern for a cryptography library.
|
|
91
|
+
|
|
92
|
+
### Explicit Initialisation
|
|
93
|
+
|
|
94
|
+
No class silently auto-initialises. The [`init()`][init] gate is mandatory and
|
|
95
|
+
explicit, giving consumers full control over when WASM modules are loaded
|
|
96
|
+
and ensuring no hidden initialisation costs or race conditions. Classes
|
|
97
|
+
throw immediately if used before initialisation rather than failing
|
|
98
|
+
silently.
|
|
99
|
+
|
|
100
|
+
### Agentic Development Contracts
|
|
101
|
+
|
|
102
|
+
All AI-assisted development on this repository operates under a strict
|
|
103
|
+
agentic contract defined in [`AGENTS.md`][agents]. The contract enforces
|
|
104
|
+
spec authority over planning documents, immutable test vectors, gate
|
|
105
|
+
discipline before extending any test suite, independent algorithm
|
|
106
|
+
derivation from published standards, and constant-time/wipe requirements
|
|
107
|
+
for all security-sensitive code paths. Agents are explicitly prohibited
|
|
108
|
+
from guessing cryptographic values or resolving spec ambiguities silently.
|
|
109
|
+
|
|
110
|
+
The contract has been verified against Claude, GitHub Copilot (VS Code),
|
|
111
|
+
OpenHands, Kilo Code, Cursor, Windsurf, and Aider. Configuration files for
|
|
112
|
+
each are present in the repository and all route to [`AGENTS.md`][agents]
|
|
113
|
+
as the single source of authority.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Reporting a Vulnerability
|
|
118
|
+
|
|
119
|
+
> [!IMPORTANT]
|
|
120
|
+
> **_Please do not open a public issue for security vulnerabilities._**
|
|
121
|
+
|
|
122
|
+
### Private Advisory (preferred)
|
|
123
|
+
|
|
124
|
+
Use GitHub's private vulnerability reporting form:
|
|
125
|
+
[https://github.com/xero/leviathan-crypto/security/advisories/new][advisory]
|
|
126
|
+
|
|
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.
|
|
131
|
+
|
|
132
|
+
### Direct Contact
|
|
133
|
+
|
|
134
|
+
If you prefer to contact the maintainer directly:
|
|
135
|
+
|
|
136
|
+
- **Email:** x﹫xero.style — PGP: [`0xAC1D0000`][pgp]
|
|
137
|
+
- **Matrix:** x0﹫rx.haunted.computer
|
|
138
|
+
|
|
139
|
+
> [!NOTE]
|
|
140
|
+
> Encrypted communication is welcome and _preferred_ for sensitive reports.
|
|
141
|
+
|
|
142
|
+
### Scope
|
|
143
|
+
|
|
144
|
+
Reports are in scope for:
|
|
145
|
+
|
|
146
|
+
- Correctness bugs in cryptographic implementations (wrong output against
|
|
147
|
+
test vectors)
|
|
148
|
+
- Side-channel vulnerabilities (timing, memory access patterns)
|
|
149
|
+
- Authentication bypass in AEAD constructions
|
|
150
|
+
- Key material exposure or improper zeroing
|
|
151
|
+
- Supply chain issues (dependency tampering, workflow compromise)
|
|
152
|
+
|
|
153
|
+
Out of scope:
|
|
154
|
+
|
|
155
|
+
- Unpatched vulnerabilities in third-party packages not maintained by this
|
|
156
|
+
project
|
|
157
|
+
- 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
|
package/dist/chacha.wasm
ADDED
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Mode, InitOpts } from '../init.js';
|
|
2
|
+
export declare function chacha20Init(mode?: Mode, opts?: InitOpts): Promise<void>;
|
|
3
|
+
export declare class ChaCha20 {
|
|
4
|
+
private readonly x;
|
|
5
|
+
constructor();
|
|
6
|
+
beginEncrypt(key: Uint8Array, nonce: Uint8Array): void;
|
|
7
|
+
encryptChunk(chunk: Uint8Array): Uint8Array;
|
|
8
|
+
beginDecrypt(key: Uint8Array, nonce: Uint8Array): void;
|
|
9
|
+
decryptChunk(chunk: Uint8Array): Uint8Array;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare class Poly1305 {
|
|
13
|
+
private readonly x;
|
|
14
|
+
constructor();
|
|
15
|
+
mac(key: Uint8Array, msg: Uint8Array): Uint8Array;
|
|
16
|
+
dispose(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
|
|
20
|
+
*
|
|
21
|
+
* `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
|
|
22
|
+
* no early return on mismatch. Plaintext is never returned on failure.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ChaCha20Poly1305 {
|
|
25
|
+
private readonly x;
|
|
26
|
+
constructor();
|
|
27
|
+
encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): {
|
|
28
|
+
ciphertext: Uint8Array;
|
|
29
|
+
tag: Uint8Array;
|
|
30
|
+
};
|
|
31
|
+
decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, tag: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* XChaCha20-Poly1305 AEAD (IETF draft-irtf-cfrg-xchacha).
|
|
36
|
+
*
|
|
37
|
+
* Recommended authenticated encryption primitive for most use cases.
|
|
38
|
+
* Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
|
|
39
|
+
*
|
|
40
|
+
* `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
|
|
41
|
+
*/
|
|
42
|
+
export declare class XChaCha20Poly1305 {
|
|
43
|
+
private readonly x;
|
|
44
|
+
constructor();
|
|
45
|
+
encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
46
|
+
decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
49
|
+
export declare function _chachaReady(): boolean;
|
|
@@ -0,0 +1,177 @@
|
|
|
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/index.ts
|
|
23
|
+
//
|
|
24
|
+
// Public API classes for the ChaCha20 WASM module.
|
|
25
|
+
// Uses the init() module cache — call init('chacha20') before constructing.
|
|
26
|
+
import { getInstance, initModule } from '../init.js';
|
|
27
|
+
import { aeadEncrypt, aeadDecrypt, xcEncrypt, xcDecrypt } from './ops.js';
|
|
28
|
+
const _embedded = () => import('../embedded/chacha.js').then(m => m.WASM_BASE64);
|
|
29
|
+
export async function chacha20Init(mode = 'embedded', opts) {
|
|
30
|
+
return initModule('chacha20', _embedded, mode, opts);
|
|
31
|
+
}
|
|
32
|
+
function getExports() {
|
|
33
|
+
return getInstance('chacha20').exports;
|
|
34
|
+
}
|
|
35
|
+
// ── ChaCha20 ──────────────────────────────────────────────────────────────────
|
|
36
|
+
export class ChaCha20 {
|
|
37
|
+
x;
|
|
38
|
+
constructor() {
|
|
39
|
+
this.x = getExports();
|
|
40
|
+
}
|
|
41
|
+
beginEncrypt(key, nonce) {
|
|
42
|
+
if (key.length !== 32)
|
|
43
|
+
throw new RangeError(`ChaCha20 key must be 32 bytes (got ${key.length})`);
|
|
44
|
+
if (nonce.length !== 12)
|
|
45
|
+
throw new RangeError(`ChaCha20 nonce must be 12 bytes (got ${nonce.length})`);
|
|
46
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
47
|
+
mem.set(key, this.x.getKeyOffset());
|
|
48
|
+
mem.set(nonce, this.x.getChachaNonceOffset());
|
|
49
|
+
this.x.chachaSetCounter(1);
|
|
50
|
+
this.x.chachaLoadKey();
|
|
51
|
+
}
|
|
52
|
+
encryptChunk(chunk) {
|
|
53
|
+
const maxChunk = this.x.getChunkSize();
|
|
54
|
+
if (chunk.length > maxChunk)
|
|
55
|
+
throw new RangeError(`chunk exceeds maximum size of ${maxChunk} bytes — split into smaller chunks`);
|
|
56
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
57
|
+
const ptOff = this.x.getChunkPtOffset();
|
|
58
|
+
const ctOff = this.x.getChunkCtOffset();
|
|
59
|
+
mem.set(chunk, ptOff);
|
|
60
|
+
this.x.chachaEncryptChunk(chunk.length);
|
|
61
|
+
return mem.slice(ctOff, ctOff + chunk.length);
|
|
62
|
+
}
|
|
63
|
+
beginDecrypt(key, nonce) {
|
|
64
|
+
this.beginEncrypt(key, nonce);
|
|
65
|
+
}
|
|
66
|
+
decryptChunk(chunk) {
|
|
67
|
+
return this.encryptChunk(chunk);
|
|
68
|
+
}
|
|
69
|
+
dispose() {
|
|
70
|
+
this.x.wipeBuffers();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Poly1305 ──────────────────────────────────────────────────────────────────
|
|
74
|
+
export class Poly1305 {
|
|
75
|
+
x;
|
|
76
|
+
constructor() {
|
|
77
|
+
this.x = getExports();
|
|
78
|
+
}
|
|
79
|
+
mac(key, msg) {
|
|
80
|
+
if (key.length !== 32)
|
|
81
|
+
throw new RangeError(`Poly1305 key must be 32 bytes (got ${key.length})`);
|
|
82
|
+
const mem = new Uint8Array(this.x.memory.buffer);
|
|
83
|
+
const keyOff = this.x.getPolyKeyOffset();
|
|
84
|
+
const msgOff = this.x.getPolyMsgOffset();
|
|
85
|
+
const tagOff = this.x.getPolyTagOffset();
|
|
86
|
+
mem.set(key, keyOff);
|
|
87
|
+
this.x.polyInit();
|
|
88
|
+
let pos = 0;
|
|
89
|
+
while (pos < msg.length) {
|
|
90
|
+
const chunk = Math.min(64, msg.length - pos);
|
|
91
|
+
mem.set(msg.subarray(pos, pos + chunk), msgOff);
|
|
92
|
+
this.x.polyUpdate(chunk);
|
|
93
|
+
pos += chunk;
|
|
94
|
+
}
|
|
95
|
+
this.x.polyFinal();
|
|
96
|
+
return new Uint8Array(this.x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
97
|
+
}
|
|
98
|
+
dispose() {
|
|
99
|
+
this.x.wipeBuffers();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── ChaCha20Poly1305 ─────────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* ChaCha20-Poly1305 AEAD (RFC 8439 §2.8).
|
|
105
|
+
*
|
|
106
|
+
* `decrypt()` uses constant-time tag comparison — XOR-accumulate pattern,
|
|
107
|
+
* no early return on mismatch. Plaintext is never returned on failure.
|
|
108
|
+
*/
|
|
109
|
+
export class ChaCha20Poly1305 {
|
|
110
|
+
x;
|
|
111
|
+
constructor() {
|
|
112
|
+
this.x = getExports();
|
|
113
|
+
}
|
|
114
|
+
encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
115
|
+
if (key.length !== 32)
|
|
116
|
+
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
117
|
+
if (nonce.length !== 12)
|
|
118
|
+
throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
|
|
119
|
+
return aeadEncrypt(this.x, key, nonce, plaintext, aad);
|
|
120
|
+
}
|
|
121
|
+
decrypt(key, nonce, ciphertext, tag, aad = new Uint8Array(0)) {
|
|
122
|
+
if (key.length !== 32)
|
|
123
|
+
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
124
|
+
if (nonce.length !== 12)
|
|
125
|
+
throw new RangeError(`nonce must be 12 bytes (got ${nonce.length})`);
|
|
126
|
+
if (tag.length !== 16)
|
|
127
|
+
throw new RangeError(`tag must be 16 bytes (got ${tag.length})`);
|
|
128
|
+
return aeadDecrypt(this.x, key, nonce, ciphertext, tag, aad);
|
|
129
|
+
}
|
|
130
|
+
dispose() {
|
|
131
|
+
this.x.wipeBuffers();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ── XChaCha20Poly1305 ────────────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* XChaCha20-Poly1305 AEAD (IETF draft-irtf-cfrg-xchacha).
|
|
137
|
+
*
|
|
138
|
+
* Recommended authenticated encryption primitive for most use cases.
|
|
139
|
+
* Uses a 24-byte nonce — safe for random generation via crypto.getRandomValues.
|
|
140
|
+
*
|
|
141
|
+
* `decrypt()` constant-time guarantee is inherited from the inner AEAD path.
|
|
142
|
+
*/
|
|
143
|
+
export class XChaCha20Poly1305 {
|
|
144
|
+
x;
|
|
145
|
+
constructor() {
|
|
146
|
+
this.x = getExports();
|
|
147
|
+
}
|
|
148
|
+
encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
149
|
+
if (key.length !== 32)
|
|
150
|
+
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
151
|
+
if (nonce.length !== 24)
|
|
152
|
+
throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
|
|
153
|
+
return xcEncrypt(this.x, key, nonce, plaintext, aad);
|
|
154
|
+
}
|
|
155
|
+
decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
|
|
156
|
+
if (key.length !== 32)
|
|
157
|
+
throw new RangeError(`key must be 32 bytes (got ${key.length})`);
|
|
158
|
+
if (nonce.length !== 24)
|
|
159
|
+
throw new RangeError(`XChaCha20 nonce must be 24 bytes (got ${nonce.length})`);
|
|
160
|
+
if (ciphertext.length < 16)
|
|
161
|
+
throw new RangeError(`ciphertext too short — must include 16-byte tag (got ${ciphertext.length})`);
|
|
162
|
+
return xcDecrypt(this.x, key, nonce, ciphertext, aad);
|
|
163
|
+
}
|
|
164
|
+
dispose() {
|
|
165
|
+
this.x.wipeBuffers();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ── Ready check ──────────────────────────────────────────────────────────────
|
|
169
|
+
export function _chachaReady() {
|
|
170
|
+
try {
|
|
171
|
+
getInstance('chacha20');
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChaChaExports } from './types.js';
|
|
2
|
+
/** ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8). */
|
|
3
|
+
export declare function aeadEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): {
|
|
4
|
+
ciphertext: Uint8Array;
|
|
5
|
+
tag: Uint8Array;
|
|
6
|
+
};
|
|
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;
|
|
9
|
+
/** HChaCha20 subkey derivation — first 16 bytes of nonce. */
|
|
10
|
+
export declare function deriveSubkey(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array): Uint8Array;
|
|
11
|
+
/** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
|
|
12
|
+
export declare function innerNonce(nonce: Uint8Array): Uint8Array;
|
|
13
|
+
/** XChaCha20-Poly1305 encrypt → ciphertext || tag. */
|
|
14
|
+
export declare function xcEncrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Uint8Array;
|
|
15
|
+
/** XChaCha20-Poly1305 decrypt → plaintext (throws on auth failure). */
|
|
16
|
+
export declare function xcDecrypt(x: ChaChaExports, key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// src/ts/chacha20/ops.ts
|
|
2
|
+
//
|
|
3
|
+
// Raw XChaCha20-Poly1305 operations — standalone functions that take
|
|
4
|
+
// ChaChaExports explicitly. Used by both the class wrappers (index.ts)
|
|
5
|
+
// and the pool worker (pool.worker.ts), eliminating duplication.
|
|
6
|
+
import { constantTimeEqual } from '../utils.js';
|
|
7
|
+
// ── Module-private helpers ───────────────────────────────────────────────────
|
|
8
|
+
function polyFeed(x, data) {
|
|
9
|
+
if (data.length === 0)
|
|
10
|
+
return;
|
|
11
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
12
|
+
const msgOff = x.getPolyMsgOffset();
|
|
13
|
+
let pos = 0;
|
|
14
|
+
while (pos < data.length) {
|
|
15
|
+
const chunk = Math.min(64, data.length - pos);
|
|
16
|
+
mem.set(data.subarray(pos, pos + chunk), msgOff);
|
|
17
|
+
x.polyUpdate(chunk);
|
|
18
|
+
pos += chunk;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function lenBlock(aadLen, ctLen) {
|
|
22
|
+
const b = new Uint8Array(16);
|
|
23
|
+
let n = aadLen;
|
|
24
|
+
for (let i = 0; i < 4; i++) {
|
|
25
|
+
b[i] = n & 0xff;
|
|
26
|
+
n >>>= 8;
|
|
27
|
+
}
|
|
28
|
+
n = ctLen;
|
|
29
|
+
for (let i = 0; i < 4; i++) {
|
|
30
|
+
b[8 + i] = n & 0xff;
|
|
31
|
+
n >>>= 8;
|
|
32
|
+
}
|
|
33
|
+
return b;
|
|
34
|
+
}
|
|
35
|
+
// ── Inner AEAD (12-byte nonce) ───────────────────────────────────────────────
|
|
36
|
+
/** ChaCha20-Poly1305 AEAD encrypt (RFC 8439 §2.8). */
|
|
37
|
+
export function aeadEncrypt(x, key, nonce, plaintext, aad) {
|
|
38
|
+
const maxChunk = x.getChunkSize();
|
|
39
|
+
if (plaintext.length > maxChunk)
|
|
40
|
+
throw new RangeError(`plaintext exceeds ${maxChunk} bytes — split into smaller chunks`);
|
|
41
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
42
|
+
// Step 1: Generate Poly1305 one-time key at counter=0 (RFC 8439 §2.6)
|
|
43
|
+
mem.set(key, x.getKeyOffset());
|
|
44
|
+
mem.set(nonce, x.getChachaNonceOffset());
|
|
45
|
+
x.chachaSetCounter(1);
|
|
46
|
+
x.chachaLoadKey();
|
|
47
|
+
x.chachaGenPolyKey();
|
|
48
|
+
// Step 2: Initialise Poly1305
|
|
49
|
+
x.polyInit();
|
|
50
|
+
// Step 3: MAC AAD + pad
|
|
51
|
+
polyFeed(x, aad);
|
|
52
|
+
const aadPad = (16 - aad.length % 16) % 16;
|
|
53
|
+
if (aadPad > 0)
|
|
54
|
+
polyFeed(x, new Uint8Array(aadPad));
|
|
55
|
+
// Step 4: Re-init ChaCha20 at counter=1
|
|
56
|
+
x.chachaSetCounter(1);
|
|
57
|
+
x.chachaLoadKey();
|
|
58
|
+
// Step 5: Encrypt
|
|
59
|
+
mem.set(plaintext, x.getChunkPtOffset());
|
|
60
|
+
x.chachaEncryptChunk(plaintext.length);
|
|
61
|
+
const ctOff = x.getChunkCtOffset();
|
|
62
|
+
const ciphertext = new Uint8Array(x.memory.buffer).slice(ctOff, ctOff + plaintext.length);
|
|
63
|
+
// Step 6: MAC ciphertext + pad
|
|
64
|
+
polyFeed(x, ciphertext);
|
|
65
|
+
const ctPad = (16 - plaintext.length % 16) % 16;
|
|
66
|
+
if (ctPad > 0)
|
|
67
|
+
polyFeed(x, new Uint8Array(ctPad));
|
|
68
|
+
// Step 7: MAC length footer
|
|
69
|
+
polyFeed(x, lenBlock(aad.length, plaintext.length));
|
|
70
|
+
// Step 8: Finalise
|
|
71
|
+
x.polyFinal();
|
|
72
|
+
const tagOff = x.getPolyTagOffset();
|
|
73
|
+
const tag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
74
|
+
return { ciphertext, tag };
|
|
75
|
+
}
|
|
76
|
+
/** ChaCha20-Poly1305 AEAD decrypt (RFC 8439 §2.8). Constant-time tag comparison. */
|
|
77
|
+
export function aeadDecrypt(x, key, nonce, ciphertext, tag, aad) {
|
|
78
|
+
const maxChunk = x.getChunkSize();
|
|
79
|
+
if (ciphertext.length > maxChunk)
|
|
80
|
+
throw new RangeError(`ciphertext exceeds ${maxChunk} bytes — split into smaller chunks`);
|
|
81
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
82
|
+
// Compute expected tag
|
|
83
|
+
mem.set(key, x.getKeyOffset());
|
|
84
|
+
mem.set(nonce, x.getChachaNonceOffset());
|
|
85
|
+
x.chachaSetCounter(1);
|
|
86
|
+
x.chachaLoadKey();
|
|
87
|
+
x.chachaGenPolyKey();
|
|
88
|
+
x.polyInit();
|
|
89
|
+
polyFeed(x, aad);
|
|
90
|
+
const aadPad = (16 - aad.length % 16) % 16;
|
|
91
|
+
if (aadPad > 0)
|
|
92
|
+
polyFeed(x, new Uint8Array(aadPad));
|
|
93
|
+
polyFeed(x, ciphertext);
|
|
94
|
+
const ctPad = (16 - ciphertext.length % 16) % 16;
|
|
95
|
+
if (ctPad > 0)
|
|
96
|
+
polyFeed(x, new Uint8Array(ctPad));
|
|
97
|
+
polyFeed(x, lenBlock(aad.length, ciphertext.length));
|
|
98
|
+
x.polyFinal();
|
|
99
|
+
// Constant-time tag comparison
|
|
100
|
+
const tagOff = x.getPolyTagOffset();
|
|
101
|
+
const expectedTag = new Uint8Array(x.memory.buffer).slice(tagOff, tagOff + 16);
|
|
102
|
+
if (!constantTimeEqual(expectedTag, tag))
|
|
103
|
+
throw new Error('ChaCha20Poly1305: authentication failed');
|
|
104
|
+
// Decrypt only after authentication succeeds
|
|
105
|
+
x.chachaSetCounter(1);
|
|
106
|
+
x.chachaLoadKey();
|
|
107
|
+
new Uint8Array(x.memory.buffer).set(ciphertext, x.getChunkPtOffset());
|
|
108
|
+
x.chachaEncryptChunk(ciphertext.length);
|
|
109
|
+
const ptOff = x.getChunkCtOffset();
|
|
110
|
+
return new Uint8Array(x.memory.buffer).slice(ptOff, ptOff + ciphertext.length);
|
|
111
|
+
}
|
|
112
|
+
// ── XChaCha20 helpers ────────────────────────────────────────────────────────
|
|
113
|
+
/** HChaCha20 subkey derivation — first 16 bytes of nonce. */
|
|
114
|
+
export function deriveSubkey(x, key, nonce) {
|
|
115
|
+
const mem = new Uint8Array(x.memory.buffer);
|
|
116
|
+
mem.set(key, x.getKeyOffset());
|
|
117
|
+
mem.set(nonce.subarray(0, 16), x.getXChaChaNonceOffset());
|
|
118
|
+
x.hchacha20();
|
|
119
|
+
const off = x.getXChaChaSubkeyOffset();
|
|
120
|
+
return new Uint8Array(x.memory.buffer).slice(off, off + 32);
|
|
121
|
+
}
|
|
122
|
+
/** Build inner 12-byte nonce from bytes 16–23 of XChaCha nonce. */
|
|
123
|
+
export function innerNonce(nonce) {
|
|
124
|
+
const n = new Uint8Array(12);
|
|
125
|
+
n.set(nonce.subarray(16, 24), 4);
|
|
126
|
+
return n;
|
|
127
|
+
}
|
|
128
|
+
// ── Full XChaCha20-Poly1305 ──────────────────────────────────────────────────
|
|
129
|
+
/** XChaCha20-Poly1305 encrypt → ciphertext || tag. */
|
|
130
|
+
export function xcEncrypt(x, key, nonce, plaintext, aad) {
|
|
131
|
+
const subkey = deriveSubkey(x, key, nonce);
|
|
132
|
+
const inner = innerNonce(nonce);
|
|
133
|
+
const { ciphertext, tag } = aeadEncrypt(x, subkey, inner, plaintext, aad);
|
|
134
|
+
const result = new Uint8Array(ciphertext.length + 16);
|
|
135
|
+
result.set(ciphertext);
|
|
136
|
+
result.set(tag, ciphertext.length);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/** XChaCha20-Poly1305 decrypt → plaintext (throws on auth failure). */
|
|
140
|
+
export function xcDecrypt(x, key, nonce, ciphertext, aad) {
|
|
141
|
+
const ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
142
|
+
const tag = ciphertext.subarray(ciphertext.length - 16);
|
|
143
|
+
const subkey = deriveSubkey(x, key, nonce);
|
|
144
|
+
const inner = innerNonce(nonce);
|
|
145
|
+
return aeadDecrypt(x, subkey, inner, ct, tag, aad);
|
|
146
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface PoolOpts {
|
|
2
|
+
/** Number of workers. Default: navigator.hardwareConcurrency ?? 4 */
|
|
3
|
+
workers?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Parallel worker pool for XChaCha20-Poly1305 AEAD.
|
|
7
|
+
*
|
|
8
|
+
* Each worker owns its own `WebAssembly.Instance` with isolated linear memory.
|
|
9
|
+
* Jobs are dispatched round-robin to idle workers; excess jobs queue until a
|
|
10
|
+
* worker frees up.
|
|
11
|
+
*
|
|
12
|
+
* **Warning:** Input buffers (`key`, `nonce`, `plaintext`/`ciphertext`, `aad`)
|
|
13
|
+
* are transferred to the worker and neutered on the calling side. The caller
|
|
14
|
+
* must copy any buffer they need to retain after calling `encrypt()`/`decrypt()`.
|
|
15
|
+
*/
|
|
16
|
+
export declare class XChaCha20Poly1305Pool {
|
|
17
|
+
private readonly _workers;
|
|
18
|
+
private readonly _idle;
|
|
19
|
+
private readonly _queue;
|
|
20
|
+
private readonly _pending;
|
|
21
|
+
private _nextId;
|
|
22
|
+
private _disposed;
|
|
23
|
+
private constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Create a new pool. Requires `init(['chacha20'])` to have been called.
|
|
26
|
+
* Compiles the WASM module once and distributes it to all workers.
|
|
27
|
+
*/
|
|
28
|
+
static create(opts?: PoolOpts): Promise<XChaCha20Poly1305Pool>;
|
|
29
|
+
/**
|
|
30
|
+
* Encrypt plaintext with XChaCha20-Poly1305.
|
|
31
|
+
* Returns `ciphertext || tag` (plaintext.length + 16 bytes).
|
|
32
|
+
*
|
|
33
|
+
* **Warning:** All input buffers are transferred and neutered after dispatch.
|
|
34
|
+
*/
|
|
35
|
+
encrypt(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Promise<Uint8Array>;
|
|
36
|
+
/**
|
|
37
|
+
* Decrypt ciphertext with XChaCha20-Poly1305.
|
|
38
|
+
* Input is `ciphertext || tag` (at least 16 bytes).
|
|
39
|
+
*
|
|
40
|
+
* **Warning:** All input buffers are transferred and neutered after dispatch.
|
|
41
|
+
*/
|
|
42
|
+
decrypt(key: Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, aad?: Uint8Array): Promise<Uint8Array>;
|
|
43
|
+
/** Terminates all workers. Rejects all pending and queued jobs. */
|
|
44
|
+
dispose(): void;
|
|
45
|
+
/** Number of workers in the pool. */
|
|
46
|
+
get size(): number;
|
|
47
|
+
/** Number of jobs currently queued (waiting for a free worker). */
|
|
48
|
+
get queueDepth(): number;
|
|
49
|
+
private _dispatch;
|
|
50
|
+
private _send;
|
|
51
|
+
private _onMessage;
|
|
52
|
+
}
|