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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Encoding utilities, comparison functions, and random byte generation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Pure TypeScript utilities that ship alongside the WASM-backed primitives. These functions have **no `init()` dependency** -- they work immediately on import, without loading any WASM module.
|
|
6
|
+
|
|
7
|
+
The module covers four areas:
|
|
8
|
+
|
|
9
|
+
- **Encoding** -- hex, UTF-8, and base64 conversions between strings and `Uint8Array`
|
|
10
|
+
- **Security** -- constant-time comparison and secure memory wiping
|
|
11
|
+
- **Byte manipulation** -- XOR and concatenation of byte arrays
|
|
12
|
+
- **Random** -- cryptographically secure random byte generation
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Security Notes
|
|
17
|
+
|
|
18
|
+
**`constantTimeEqual`** uses an XOR-accumulate pattern with no early return on byte mismatch. Use this function whenever you compare MACs, hashes, authentication tags, or any secret-derived values. **Never** use `===`, `Buffer.equals`, or manual loop-with-break for security comparisons -- those leak timing information that can be exploited to recover secrets.
|
|
19
|
+
|
|
20
|
+
The length check in `constantTimeEqual` is _not_ constant-time, because array length is non-secret in all standard protocols. If your use case treats length as secret, you must pad to equal length before comparing.
|
|
21
|
+
|
|
22
|
+
**`wipe`** zeroes a typed array in-place. Call it on keys, plaintext buffers, and any other sensitive data as soon as you are done with them. JavaScript's garbage collector does not guarantee timely or complete erasure of memory.
|
|
23
|
+
|
|
24
|
+
**`randomBytes`** delegates to `crypto.getRandomValues` (Web Crypto API), which is cryptographically secure in all modern browsers and Node.js 19+. It does not fall back to `Math.random` or any insecure source.
|
|
25
|
+
|
|
26
|
+
The encoding functions (`hexToBytes`, `bytesToHex`, `utf8ToBytes`, `bytesToUtf8`, `base64ToBytes`, `bytesToBase64`) perform no security-sensitive operations.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## API Reference
|
|
31
|
+
|
|
32
|
+
### hexToBytes
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
hexToBytes(hex: string): Uint8Array
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Converts a hex string to a `Uint8Array`. Accepts lowercase or uppercase characters. An optional `0x` or `0X` prefix is stripped automatically. If the hex string has an odd number of characters, a trailing `0` is appended before decoding.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### bytesToHex
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
bytesToHex(bytes: Uint8Array): string
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Converts a `Uint8Array` to a lowercase hex string (no prefix).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### utf8ToBytes
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
utf8ToBytes(str: string): Uint8Array
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Encodes a JavaScript string as UTF-8 bytes using the platform `TextEncoder`.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### bytesToUtf8
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
bytesToUtf8(bytes: Uint8Array): string
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Decodes UTF-8 bytes to a JavaScript string using the platform `TextDecoder`.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### base64ToBytes
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
base64ToBytes(b64: string): Uint8Array | undefined
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Decodes a base64 or base64url string to a `Uint8Array`. Base64url characters (`-`, `_`, `%3d`) are normalized to standard base64 before decoding. Returns `undefined` if the input is not valid base64 (e.g., incorrect length or illegal characters).
|
|
79
|
+
|
|
80
|
+
> [!NOTE]
|
|
81
|
+
> Valid base64 characters are `A-Z`, `a-z`, `0-9`, `+`, `/`, and `=`.
|
|
82
|
+
> Any other character causes the function to return `undefined`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### bytesToBase64
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
bytesToBase64(bytes: Uint8Array, url?: boolean): string
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Encodes a `Uint8Array` to a base64 string. Pass `url = true` to get base64url encoding (uses `-` and `_` instead of `+` and `/`, and `%3d` instead of `=`). Defaults to standard base64.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### constantTimeEqual
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Returns `true` if `a` and `b` contain identical bytes. Uses XOR-accumulate with no early return on mismatch. Returns `false` immediately if the arrays differ in length (length is non-secret).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### wipe
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
wipe(data: Uint8Array | Uint16Array | Uint32Array): void
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Zeroes a typed array in-place by calling `fill(0)`. Use this to clear keys, plaintext, or any sensitive material when you are done with it.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### xor
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
xor(a: Uint8Array, b: Uint8Array): Uint8Array
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Returns a new `Uint8Array` where each byte is `a[i] ^ b[i]`. Both arrays must have the same length; throws `RangeError` if they differ.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### concat
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
concat(a: Uint8Array, b: Uint8Array): Uint8Array
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Returns a new `Uint8Array` containing `a` followed by `b`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### randomBytes
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
randomBytes(n: number): Uint8Array
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Returns `n` cryptographically secure random bytes via the Web Crypto API (`crypto.getRandomValues`).
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Usage Examples
|
|
147
|
+
|
|
148
|
+
### Converting between formats
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { hexToBytes, bytesToHex, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
|
|
152
|
+
|
|
153
|
+
// Hex round-trip
|
|
154
|
+
const bytes = hexToBytes('deadbeef')
|
|
155
|
+
console.log(bytesToHex(bytes)) // "deadbeef"
|
|
156
|
+
|
|
157
|
+
// 0x prefix is accepted
|
|
158
|
+
const prefixed = hexToBytes('0xCAFE')
|
|
159
|
+
console.log(bytesToHex(prefixed)) // "cafe"
|
|
160
|
+
|
|
161
|
+
// UTF-8 round-trip
|
|
162
|
+
const encoded = utf8ToBytes('hello world')
|
|
163
|
+
console.log(bytesToUtf8(encoded)) // "hello world"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Base64 encoding and decoding
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { bytesToBase64, base64ToBytes, utf8ToBytes, bytesToUtf8 } from 'leviathan-crypto'
|
|
172
|
+
|
|
173
|
+
const data = utf8ToBytes('leviathan-crypto')
|
|
174
|
+
const b64 = bytesToBase64(data)
|
|
175
|
+
console.log(b64) // "bGV2aWF0aGFuLWNyeXB0bw=="
|
|
176
|
+
|
|
177
|
+
// base64url variant (safe for URLs and filenames)
|
|
178
|
+
const b64url = bytesToBase64(data, true)
|
|
179
|
+
console.log(b64url) // "bGV2aWF0aGFuLWNyeXB0bw%3d%3d"
|
|
180
|
+
|
|
181
|
+
// Decoding (accepts both standard and url variants)
|
|
182
|
+
const decoded = base64ToBytes(b64)
|
|
183
|
+
if (decoded) console.log(bytesToUtf8(decoded)) // "leviathan-crypto"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Secure MAC comparison
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { constantTimeEqual } from 'leviathan-crypto'
|
|
192
|
+
|
|
193
|
+
// After computing a MAC over received data, compare it to the expected tag.
|
|
194
|
+
// NEVER use === or .every() for this -- timing leaks enable forgery attacks.
|
|
195
|
+
const computedMac: Uint8Array = hmac.hash(key, message)
|
|
196
|
+
const receivedMac: Uint8Array = getTagFromNetwork()
|
|
197
|
+
|
|
198
|
+
if (!constantTimeEqual(computedMac, receivedMac)) {
|
|
199
|
+
throw new Error('Authentication failed: MAC mismatch')
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### Generating random keys and nonces
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { randomBytes } from 'leviathan-crypto'
|
|
209
|
+
|
|
210
|
+
const key = randomBytes(32) // 256-bit symmetric key
|
|
211
|
+
const nonce = randomBytes(24) // 192-bit nonce for XChaCha20
|
|
212
|
+
const iv = randomBytes(16) // 128-bit IV for Serpent-CBC
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### Wiping sensitive data after use
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { randomBytes, wipe } from 'leviathan-crypto'
|
|
221
|
+
|
|
222
|
+
const key = randomBytes(32)
|
|
223
|
+
|
|
224
|
+
// ... use the key for encryption / decryption ...
|
|
225
|
+
|
|
226
|
+
// When done, zero the key material so it does not linger in memory
|
|
227
|
+
wipe(key)
|
|
228
|
+
// key is now all zeroes
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### XOR and concatenation
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { xor, concat, randomBytes } from 'leviathan-crypto'
|
|
237
|
+
|
|
238
|
+
const a = randomBytes(16)
|
|
239
|
+
const b = randomBytes(16)
|
|
240
|
+
|
|
241
|
+
// XOR two equal-length arrays
|
|
242
|
+
const xored = xor(a, b)
|
|
243
|
+
|
|
244
|
+
// Concatenate two arrays
|
|
245
|
+
const combined = concat(a, b)
|
|
246
|
+
console.log(combined.length) // 32
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Error Conditions
|
|
252
|
+
|
|
253
|
+
| Function | Condition | Behavior |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| `hexToBytes` | Odd-length string | Trailing `0` appended (no error) |
|
|
256
|
+
| `hexToBytes` | Invalid hex characters | Bytes decode as `NaN` -> `0` |
|
|
257
|
+
| `base64ToBytes` | Invalid length or characters | Returns `undefined` |
|
|
258
|
+
| `constantTimeEqual` | Arrays differ in length | Returns `false` immediately |
|
|
259
|
+
| `xor` | Arrays differ in length | Throws `RangeError` |
|
|
260
|
+
| `randomBytes` | `crypto` not available | Throws (runtime-dependent) |
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Cross-References
|
|
265
|
+
|
|
266
|
+
- [README.md](./README.md) — library documentation index and exports table
|
|
267
|
+
- [architecture.md](./architecture.md) — module structure and security rationale
|
|
268
|
+
- [serpent.md](./serpent.md) — Serpent modes consume keys from `randomBytes`; wrappers use `wipe` and `constantTimeEqual`
|
|
269
|
+
- [chacha20.md](./chacha20.md) — ChaCha20/Poly1305 classes use `randomBytes` for nonce generation
|
|
270
|
+
- [sha2.md](./sha2.md) — SHA-2 and HMAC classes; output often converted with `bytesToHex`
|
|
271
|
+
- [sha3.md](./sha3.md) — SHA-3 and SHAKE classes; output often converted with `bytesToHex`
|
|
272
|
+
- [types.md](./types.md) — public interfaces whose implementations rely on these utilities
|
|
273
|
+
- [test-suite.md](./test-suite.md) — test suite structure and vector corpus
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# WebAssembly Primer
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> - A short introduction to WebAssembly concepts as they apply to the leviathan-crypto library.
|
|
5
|
+
> - If you already understand WASM, skip to [Project-Specific Concepts](#project-specific-concepts).
|
|
6
|
+
> - WebAssembly specification and documentation: [webassembly.org](https://webassembly.org/)
|
|
7
|
+
|
|
8
|
+
## What is WebAssembly?
|
|
9
|
+
|
|
10
|
+
WebAssembly (WASM) is a binary instruction format that runs in browsers and
|
|
11
|
+
server-side runtimes alongside JavaScript. Rather than a programming language
|
|
12
|
+
one writes by hand, it serves as a compilation target. Code is written in a higher-level
|
|
13
|
+
language, compiled to `.wasm`, and then executed by the browser.
|
|
14
|
+
|
|
15
|
+
Consider it a small, fast virtual machine built into every modern browser.
|
|
16
|
+
JavaScript can load a `.wasm` binary, call its exported functions, and read
|
|
17
|
+
its results. The WASM code runs in its own sandboxed memory space, and thus cannot
|
|
18
|
+
touch the DOM, access JavaScript variables, or reach the network. It computes
|
|
19
|
+
and returns values, and that is its sole function.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## How It Runs
|
|
24
|
+
|
|
25
|
+
When a browser encounters a `.wasm` binary, it performs two steps:
|
|
26
|
+
|
|
27
|
+
1. Compilation: The binary is validated and compiled into native machine code.
|
|
28
|
+
This is fast because WASM is already a low-level format, requiring less
|
|
29
|
+
work for the compiler compared to parsing and optimizing JavaScript.
|
|
30
|
+
|
|
31
|
+
2. Instantiation: The compiled module is paired with its imports, such as a
|
|
32
|
+
memory object, to create a live instance. The instance's exported functions
|
|
33
|
+
are then callable from JavaScript.
|
|
34
|
+
|
|
35
|
+
Once instantiated, calling a WASM function is similar to calling any JavaScript
|
|
36
|
+
function: you pass arguments, it runs, and it returns a result. The key difference
|
|
37
|
+
lies in how it runs. WASM execution is deterministic and not subject to the
|
|
38
|
+
JIT compiler's speculative optimizations. Unlike JavaScript, the browser does not rewrite, inline,
|
|
39
|
+
or de-optimize WASM code paths based on runtime profiling.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why We Use It
|
|
44
|
+
|
|
45
|
+
Leviathan performs all cryptographic computations in WASM because JavaScript
|
|
46
|
+
engines offer no formal constant-time guarantees for arbitrary code. The JIT
|
|
47
|
+
compiler can introduce timing variations that leak information about secret
|
|
48
|
+
data; WASM execution avoids this class of problem.
|
|
49
|
+
|
|
50
|
+
For architectural details and security rationale, see [architecture.md](./architecture.md).
|
|
51
|
+
|
|
52
|
+
**TLDR:** _TypeScript handles the API, and WASM handles the math._
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Core Concepts
|
|
57
|
+
|
|
58
|
+
### Module
|
|
59
|
+
|
|
60
|
+
A `WebAssembly.Module` is a compiled `.wasm` binary and a stateless
|
|
61
|
+
template for creating instances. You can compile a module once and create
|
|
62
|
+
multiple instances from it. For example, [XChaCha20Poly1305Pool](./chacha20_pool.md)
|
|
63
|
+
uses one compiled module to create many worker instances.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### Instance
|
|
68
|
+
|
|
69
|
+
A WebAssembly.Instance is a live, runnable copy of a module, complete with
|
|
70
|
+
its own memory and state. When you call `init('serpent')`, the library
|
|
71
|
+
compiles the Serpent WASM binary and creates a single instance. All
|
|
72
|
+
Serpent classes (`Serpent`, `SerpentCtr`, `SerpentCbc`) share this instance.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Memory
|
|
77
|
+
|
|
78
|
+
A `WebAssembly.Memory` is a contiguous block of bytes, essentially a
|
|
79
|
+
`Uint8Array` that WASM functions can read and write, also known as **linear
|
|
80
|
+
memory**. Each of our WASM modules gets its own memory (3 pages = 192 KB).
|
|
81
|
+
|
|
82
|
+
The TypeScript layer communicates with WASM by writing inputs to specific offsets
|
|
83
|
+
in this memory, calling a WASM function, and then reading the outputs from other
|
|
84
|
+
offsets. There is no other communication channel, no function arguments for
|
|
85
|
+
large data, and no return values beyond a single number. Memory is the data bus.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Exports
|
|
90
|
+
|
|
91
|
+
A WASM instance exposes exports: functions and memory that JavaScript can
|
|
92
|
+
access. In leviathan-crypto, every WASM module exports:
|
|
93
|
+
|
|
94
|
+
- **Getter functions** like getKeyOffset() and getChunkPtOffset(): these
|
|
95
|
+
return the memory offsets where the TypeScript layer should write inputs
|
|
96
|
+
or read outputs.
|
|
97
|
+
- **Operation functions** like chachaEncryptChunk() and sha256Final():
|
|
98
|
+
these perform the actual cryptographic computation on data already in memory.
|
|
99
|
+
- **wipeBuffers():** this zeros all sensitive regions of memory and is called
|
|
100
|
+
by every class's dispose() method.
|
|
101
|
+
- **memory:** the linear memory object itself, which allows the TypeScript
|
|
102
|
+
layer to create Uint8Array views over it.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Imports
|
|
107
|
+
|
|
108
|
+
When instantiating a module, you can pass **imports:** objects the WASM code
|
|
109
|
+
needs from the host. Our modules import only one thing: the Memory object.
|
|
110
|
+
We create the memory on the JavaScript side and pass it in, giving us
|
|
111
|
+
control over its size and lifecycle.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Project-Specific Concepts
|
|
116
|
+
|
|
117
|
+
### AssemblyScript
|
|
118
|
+
|
|
119
|
+
The WASM binaries in this project are written in [AssemblyScript](https://www.assemblyscript.org/):
|
|
120
|
+
a TypeScript-like language that compiles to WebAssembly. It resembles
|
|
121
|
+
TypeScript but targets WASM instead of JavaScript. The source code
|
|
122
|
+
resides in `src/asm/` and compiles into `.wasm` binaries in `build/`.
|
|
123
|
+
|
|
124
|
+
AssemblyScript was selected because its syntax is familiar to TypeScript
|
|
125
|
+
developers. It produces small binaries and grants low-level control over
|
|
126
|
+
memory layout without requiring C, C++, or Rust.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Thunks
|
|
131
|
+
|
|
132
|
+
In this project, a **thunk** is a base64-encoded WASM binary embedded directly
|
|
133
|
+
within a TypeScript file. The files in `src/ts/embedded/`
|
|
134
|
+
(such as chacha.ts and serpent.ts) each export a single constant:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export const WASM_BASE64 = 'AGFzbQEAAAAB...'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This represents the entire compiled .wasm binary, encoded as a base64 string. When
|
|
141
|
+
you call init('chacha20') in embedded mode (the default), the library
|
|
142
|
+
imports this string, decodes it back into bytes, and compiles it into a
|
|
143
|
+
`WebAssembly.Module`.
|
|
144
|
+
|
|
145
|
+
Why embed the binary as a string? This approach enables the library to function with zero
|
|
146
|
+
configuration. There's no need to serve .wasm files from a CDN, configure MIME
|
|
147
|
+
types, or establish a build plugin to manage binary imports. Simply npm install and
|
|
148
|
+
import. The tradeoff is bundle size, as base64 encoding increases the size by
|
|
149
|
+
approximately 33% compared to the raw binary. For production deployments where bundle size is
|
|
150
|
+
critical, the library also supports [streaming and manual loading modes](./loader.md).
|
|
151
|
+
|
|
152
|
+
**TLDR:** Thunks are build artifacts generated by `scripts/embed-wasm.ts`.
|
|
153
|
+
They are gitignored and regenerated during each build. Avoid manual edits.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Buffer Layout
|
|
158
|
+
|
|
159
|
+
Each WASM module divides its linear memory into fixed regions at known offsets.
|
|
160
|
+
For example, the ChaCha20 module has a region for the key, a region for the
|
|
161
|
+
nonce, a region for plaintext input, a region for ciphertext output, and so on.
|
|
162
|
+
These offsets are defined in `src/asm/*/buffers.ts` and never change at runtime.
|
|
163
|
+
The TypeScript layer calls getter functions (like `getKeyOffset()`) to
|
|
164
|
+
determine where each region starts, then reads and writes `Uint8Array` slices at those
|
|
165
|
+
positions. This is the only way data moves between TypeScript and
|
|
166
|
+
WASM. There is no serialization, no copying to intermediate buffers, and no function call
|
|
167
|
+
overhead for large data. Data is transferred via direct byte writes to shared memory.
|
|
168
|
+
|
|
169
|
+
The buffer layouts for each module are documented in [architecture.md](./architecture.md).
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### The `init()` Gate
|
|
174
|
+
|
|
175
|
+
WASM modules must be compiled and instantiated before use. Because compilation
|
|
176
|
+
returns a Promise, this is an asynchronous operation. Rather than hiding this
|
|
177
|
+
behind lazy auto-initialization, which would make every cryptographic call
|
|
178
|
+
implicitly asynchronous and create race conditions, the library requires an explicit
|
|
179
|
+
`init()` call up front. If you forget, every class immediately throws an error message
|
|
180
|
+
indicating which `init(init()` call is missing. _This is deliberate._
|
|
181
|
+
|
|
182
|
+
See [init.md](./init.md) for the full API.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Cross-References
|
|
187
|
+
|
|
188
|
+
- [architecture.md](./architecture.md) — module structure, buffer layouts,
|
|
189
|
+
security rationale
|
|
190
|
+
- [init.md](./init.md) — `init()` API and the three loading modes
|
|
191
|
+
- [loader.md](./loader.md) — how WASM binaries are loaded and instantiated
|
|
192
|
+
- [chacha20_pool.md](./chacha20_pool.md) — example of one compiled module
|
|
193
|
+
spawning many instances across workers
|