toiljs 0.0.26 → 0.0.28
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/CHANGELOG.md +10 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.js +64 -15
- package/build/devserver/.tsbuildinfo +1 -0
- package/build/devserver/crypto.d.ts +18 -0
- package/build/devserver/crypto.js +327 -0
- package/build/devserver/envelope.d.ts +18 -0
- package/build/devserver/envelope.js +88 -0
- package/build/devserver/host.d.ts +16 -0
- package/build/devserver/host.js +73 -0
- package/build/devserver/index.d.ts +22 -0
- package/build/devserver/index.js +144 -0
- package/build/devserver/module.d.ts +21 -0
- package/build/devserver/module.js +86 -0
- package/build/devserver/proxy.d.ts +7 -0
- package/build/devserver/proxy.js +98 -0
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +1 -1
- package/examples/basic/server/HelloHandler.ts +4 -1
- package/package.json +9 -3
- package/server/runtime/handlers/ToilHandler.ts +4 -3
- package/server/runtime/index.ts +1 -1
- package/server/runtime/response.ts +23 -0
- package/server/runtime/rest/RestHandler.ts +2 -2
- package/src/compiler/index.ts +96 -21
- package/src/devserver/crypto.ts +421 -0
- package/src/devserver/envelope.ts +154 -0
- package/src/devserver/host.ts +145 -0
- package/src/devserver/index.ts +242 -0
- package/src/devserver/module.ts +173 -0
- package/src/devserver/proxy.ts +155 -0
- package/src/io/codec.ts +4 -2
- package/test/devserver.test.ts +199 -0
- package/tsconfig.devserver.json +13 -0
|
@@ -12,11 +12,12 @@ import { Response } from '../response';
|
|
|
12
12
|
export class ToilHandler {
|
|
13
13
|
/**
|
|
14
14
|
* Override to declare your routes. The default implementation
|
|
15
|
-
* returns
|
|
16
|
-
* still produces a valid envelope
|
|
15
|
+
* returns an unhandled-marked 404 so a handler that hasn't been
|
|
16
|
+
* wired up still produces a valid envelope, and the host knows it
|
|
17
|
+
* may serve the path itself.
|
|
17
18
|
*/
|
|
18
19
|
public handle(_req: Request): Response {
|
|
19
|
-
return Response.
|
|
20
|
+
return Response.unhandled();
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
package/server/runtime/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
export { Header, Method, Request } from './request';
|
|
18
|
-
export { Response } from './response';
|
|
18
|
+
export { Response, TOIL_UNHANDLED_HEADER } from './response';
|
|
19
19
|
export { ToilHandler } from './handlers/ToilHandler';
|
|
20
20
|
export { Server, ServerEnvironment } from './env/Server';
|
|
21
21
|
|
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
import { Header } from './request';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Marker header on the runtime's fallback 404 (no route matched, no handler
|
|
11
|
+
* wired). The host can use it to fall through to another layer, the dev
|
|
12
|
+
* server hands such requests to Vite (client routes, assets), and strips the
|
|
13
|
+
* marker before anything reaches the browser. A deliberate
|
|
14
|
+
* `Response.notFound()` does not carry it. Mirrored as `UNHANDLED_HEADER` in
|
|
15
|
+
* `src/devserver/module.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export const TOIL_UNHANDLED_HEADER: string = 'x-toil-unhandled';
|
|
18
|
+
|
|
9
19
|
export class Response {
|
|
10
20
|
status: u16;
|
|
11
21
|
headers: Array<Header>;
|
|
@@ -63,6 +73,19 @@ export class Response {
|
|
|
63
73
|
return Response.text('not found\n', 404);
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The "this server has no answer for that path" 404: a `notFound()`
|
|
78
|
+
* carrying {@link TOIL_UNHANDLED_HEADER} so the host may serve the path
|
|
79
|
+
* itself (static files, the client app). Returned by the framework when
|
|
80
|
+
* dispatch misses; handlers that mean "looked it up, does not exist"
|
|
81
|
+
* should return `notFound()` instead.
|
|
82
|
+
*/
|
|
83
|
+
public static unhandled(): Response {
|
|
84
|
+
const r = Response.notFound();
|
|
85
|
+
r.setHeader(TOIL_UNHANDLED_HEADER, '1');
|
|
86
|
+
return r;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
public static badRequest(msg: string = 'bad request'): Response {
|
|
67
90
|
return Response.text(msg + '\n', 400);
|
|
68
91
|
}
|
|
@@ -11,10 +11,10 @@ import { ToilHandler } from '../handlers/ToilHandler';
|
|
|
11
11
|
import { Rest } from './Rest';
|
|
12
12
|
|
|
13
13
|
export class RestHandler extends ToilHandler {
|
|
14
|
-
/** Dispatches to the registered `@rest` controllers;
|
|
14
|
+
/** Dispatches to the registered `@rest` controllers; an unhandled-marked 404 when none match. */
|
|
15
15
|
handle(req: Request): Response {
|
|
16
16
|
const hit = Rest.dispatch(req);
|
|
17
17
|
if (hit != null) return hit;
|
|
18
|
-
return Response.
|
|
18
|
+
return Response.unhandled();
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import net from 'node:net';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
|
|
6
7
|
import pc from 'picocolors';
|
|
7
|
-
import { build as viteBuild, createServer, type ViteDevServer } from 'vite';
|
|
8
|
+
import { build as viteBuild, createServer, mergeConfig, type ViteDevServer } from 'vite';
|
|
8
9
|
import { startBackend, type RunningBackend } from 'toiljs/backend';
|
|
10
|
+
import { startDevServer } from 'toiljs/devserver';
|
|
9
11
|
|
|
10
12
|
import { loadConfig } from './config.js';
|
|
11
13
|
import { generate } from './generate.js';
|
|
@@ -140,11 +142,14 @@ async function buildServer(root: string): Promise<void> {
|
|
|
140
142
|
/**
|
|
141
143
|
* Watches the server source dirs and rebuilds the server (toilscript) on change, so editing a
|
|
142
144
|
* `@data`/`@rest` file under `toiljs dev` regenerates `shared/server.ts` - which Vite then HMRs
|
|
143
|
-
* into the client - the server-
|
|
144
|
-
*
|
|
145
|
-
*
|
|
145
|
+
* into the client - and the dev server hot-swaps the recompiled wasm: the server-side equivalent
|
|
146
|
+
* of Vite's client HMR. Client-only edits never touch these dirs, so they only trigger Vite,
|
|
147
|
+
* never a server rebuild. Rebuilds are debounced and never overlap. Rides Vite's chokidar
|
|
148
|
+
* watcher instead of a separate `fs.watch`: the native recursive watcher silently stops
|
|
149
|
+
* delivering events on Linux after editors replace files via rename, which left hot reload
|
|
150
|
+
* working exactly once. A no-op for client-only projects.
|
|
146
151
|
*/
|
|
147
|
-
function watchServer(root: string): void {
|
|
152
|
+
function watchServer(root: string, watcher: ViteDevServer['watcher']): void {
|
|
148
153
|
const dirs = serverDirs(root);
|
|
149
154
|
if (dirs.length === 0) return;
|
|
150
155
|
|
|
@@ -172,20 +177,47 @@ function watchServer(root: string): void {
|
|
|
172
177
|
};
|
|
173
178
|
|
|
174
179
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
175
|
-
const
|
|
176
|
-
|
|
180
|
+
const isServerSource = (file: string): boolean =>
|
|
181
|
+
file.endsWith('.ts') &&
|
|
182
|
+
!file.endsWith('.d.ts') &&
|
|
183
|
+
dirs.some((dir) => file === dir || file.startsWith(dir + path.sep));
|
|
184
|
+
watcher.add(dirs);
|
|
185
|
+
watcher.on('all', (_event, file) => {
|
|
186
|
+
if (!isServerSource(file)) return;
|
|
177
187
|
if (timer) clearTimeout(timer);
|
|
178
188
|
timer = setTimeout(rebuild, 150); // debounce bursts (save-all, formatters)
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The server wasm artifact path from the toilconfig `release` target (toilscript's output). */
|
|
193
|
+
function serverWasmFile(root: string): string {
|
|
194
|
+
let outFile = 'build/server/release.wasm';
|
|
195
|
+
try {
|
|
196
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
|
|
197
|
+
targets?: Record<string, { outFile?: string }>;
|
|
198
|
+
};
|
|
199
|
+
outFile = cfg.targets?.release?.outFile ?? outFile;
|
|
200
|
+
} catch {
|
|
201
|
+
// No readable toilconfig: caller already gated on its existence; keep the default.
|
|
188
202
|
}
|
|
203
|
+
return path.resolve(root, outFile);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** An OS-assigned free loopback port (for the internal Vite server behind the dev front). */
|
|
207
|
+
async function freeLoopbackPort(): Promise<number> {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const probe = net.createServer();
|
|
210
|
+
probe.once('error', reject);
|
|
211
|
+
probe.listen(0, '127.0.0.1', () => {
|
|
212
|
+
const address = probe.address();
|
|
213
|
+
if (address === null || typeof address === 'string') {
|
|
214
|
+
probe.close();
|
|
215
|
+
reject(new Error('could not allocate a loopback port'));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
probe.close(() => resolve(address.port));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
export interface ToilCommandOptions {
|
|
@@ -197,7 +229,16 @@ export interface ToilCommandOptions {
|
|
|
197
229
|
readonly serverOnly?: boolean;
|
|
198
230
|
}
|
|
199
231
|
|
|
200
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Starts the dev server. Client-only projects get the plain Vite dev server on
|
|
234
|
+
* the configured port, unchanged. Projects with a server target
|
|
235
|
+
* (toilconfig.json) get the WASM dev server in front: a uWebSockets.js server
|
|
236
|
+
* on the configured port that dispatches requests into the ToilScript server
|
|
237
|
+
* wasm (same envelope ABI as the production edge) and transparently proxies
|
|
238
|
+
* everything the server does not claim, HMR websocket included, to a Vite dev
|
|
239
|
+
* server on an internal loopback port. Vite keeps 100% of its dev behavior;
|
|
240
|
+
* it just stops being the public listener. Returns the running Vite server.
|
|
241
|
+
*/
|
|
201
242
|
export async function dev(opts: ToilCommandOptions = {}): Promise<ViteDevServer> {
|
|
202
243
|
const cfg = await loadConfig(opts);
|
|
203
244
|
// Server first: build it (regenerating shared/server.ts) before the client dev server starts.
|
|
@@ -206,11 +247,45 @@ export async function dev(opts: ToilCommandOptions = {}): Promise<ViteDevServer>
|
|
|
206
247
|
await buildServer(cfg.root);
|
|
207
248
|
if (hasServer) process.stdout.write(pc.green(' ✓ ') + pc.dim('server built') + '\n');
|
|
208
249
|
generate(cfg);
|
|
209
|
-
|
|
250
|
+
|
|
251
|
+
if (!hasServer) {
|
|
252
|
+
const server = await createServer(await createViteConfig(cfg));
|
|
253
|
+
await server.listen();
|
|
254
|
+
server.printUrls();
|
|
255
|
+
return server;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Vite moves to an internal loopback port; the WASM dev server takes the public one.
|
|
259
|
+
const vitePort = await freeLoopbackPort();
|
|
260
|
+
const viteConfig = mergeConfig(await createViteConfig(cfg), {
|
|
261
|
+
server: { port: vitePort, host: '127.0.0.1', strictPort: true },
|
|
262
|
+
});
|
|
263
|
+
const server = await createServer(viteConfig);
|
|
210
264
|
await server.listen();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
265
|
+
|
|
266
|
+
const front = await startDevServer({
|
|
267
|
+
root: cfg.root,
|
|
268
|
+
port: cfg.port,
|
|
269
|
+
wasmFile: serverWasmFile(cfg.root),
|
|
270
|
+
vite: { host: '127.0.0.1', port: vitePort },
|
|
271
|
+
});
|
|
272
|
+
server.httpServer?.once('close', () => {
|
|
273
|
+
void front.close();
|
|
274
|
+
});
|
|
275
|
+
process.stdout.write(
|
|
276
|
+
'\n ' +
|
|
277
|
+
pc.green('➜') +
|
|
278
|
+
' ' +
|
|
279
|
+
pc.bold('Local') +
|
|
280
|
+
': ' +
|
|
281
|
+
pc.cyan(`http://localhost:${pc.bold(String(front.port))}/`) +
|
|
282
|
+
pc.dim(' (wasm server + vite)') +
|
|
283
|
+
'\n',
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Rebuild the server on server-file changes; Vite HMRs the regenerated shared/server.ts
|
|
287
|
+
// and the dev server hot-swaps the recompiled wasm module.
|
|
288
|
+
watchServer(cfg.root, server.watcher);
|
|
214
289
|
return server;
|
|
215
290
|
}
|
|
216
291
|
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-server mock of the `env.crypto.*` host functions, mirroring the
|
|
3
|
+
* production edge (`toil-backend/src/wasm/host/import_functions/crypto`) and the
|
|
4
|
+
* toilscript std ABI (`std/assembly/crypto/algorithms.ts`). Backed by Node's
|
|
5
|
+
* `crypto`. The dev server intentionally skips the edge's metering, so these
|
|
6
|
+
* charge nothing; results must still be byte-identical to the edge for the
|
|
7
|
+
* common algorithms.
|
|
8
|
+
*
|
|
9
|
+
* Variable-length results use the same two-step pull as the edge: the op
|
|
10
|
+
* returns the length and stashes the bytes; the guest then calls `take_result`.
|
|
11
|
+
*
|
|
12
|
+
* Dev-only limitations: raw-format import of asymmetric keys (EC/Ed25519/
|
|
13
|
+
* X25519) returns the "unsupported" code (-3) here because Node can't import a
|
|
14
|
+
* bare key without DER; use pkcs8/spki in the dev server. The production edge
|
|
15
|
+
* supports raw too. These are catchable guest-side errors, never crashes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as nodeCrypto from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
import type { MemoryRef } from './host.js';
|
|
21
|
+
|
|
22
|
+
// --- ABI id tables (must match the std + Rust backend) ----------------------
|
|
23
|
+
const ALG = {
|
|
24
|
+
SHA1: 1, SHA256: 2, SHA384: 3, SHA512: 4, SHA3_256: 5, SHA3_384: 6, SHA3_512: 7,
|
|
25
|
+
AES_GCM: 10, AES_CBC: 11, AES_CTR: 12, AES_KW: 13, HMAC: 20,
|
|
26
|
+
ECDSA: 32, ED25519: 33, ECDH: 50, X25519: 51, HKDF: 52, PBKDF2: 53,
|
|
27
|
+
} as const;
|
|
28
|
+
const FMT = { RAW: 0, PKCS8: 1, SPKI: 2, JWK: 3 } as const;
|
|
29
|
+
|
|
30
|
+
// Recoverable error codes (negative returns).
|
|
31
|
+
const ERR_GENERIC = -1;
|
|
32
|
+
const ERR_UNSUPPORTED = -3;
|
|
33
|
+
const ERR_INVALID_PARAMS = -4;
|
|
34
|
+
const ERR_OPERATION_FAILED = -5;
|
|
35
|
+
const ERR_NOT_EXTRACTABLE = -7;
|
|
36
|
+
|
|
37
|
+
const MAX_OUTPUT = 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
interface KeyEntry {
|
|
40
|
+
/** Raw bytes for symmetric/MAC/KDF keys. */
|
|
41
|
+
raw: Buffer | null;
|
|
42
|
+
/** Node KeyObject for asymmetric keys. */
|
|
43
|
+
keyObject: nodeCrypto.KeyObject | null;
|
|
44
|
+
alg: number;
|
|
45
|
+
hash: number;
|
|
46
|
+
extractable: boolean;
|
|
47
|
+
isPrivate: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CryptoState {
|
|
51
|
+
keys: Map<number, KeyEntry>;
|
|
52
|
+
nextHandle: number;
|
|
53
|
+
lastResult: Buffer | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function freshCryptoState(): CryptoState {
|
|
57
|
+
return { keys: new Map(), nextHandle: 1, lastResult: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function memBuf(ref: MemoryRef): Buffer {
|
|
61
|
+
if (!ref.memory) throw new Error('crypto host import called before memory was bound');
|
|
62
|
+
return Buffer.from(ref.memory.buffer);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readBytes(ref: MemoryRef, ptr: number, len: number): Buffer {
|
|
66
|
+
const m = memBuf(ref);
|
|
67
|
+
if (ptr < 0 || len < 0 || ptr + len > m.length)
|
|
68
|
+
throw new Error(`crypto read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
|
|
69
|
+
// Copy out so later writes/grows can't alias the input.
|
|
70
|
+
return Buffer.from(m.subarray(ptr, ptr + len));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeBytes(ref: MemoryRef, ptr: number, bytes: Buffer | Uint8Array): void {
|
|
74
|
+
const m = memBuf(ref);
|
|
75
|
+
if (ptr < 0 || ptr + bytes.length > m.length)
|
|
76
|
+
throw new Error(`crypto write out of bounds: ptr=${String(ptr)} len=${String(bytes.length)}`);
|
|
77
|
+
m.set(bytes, ptr);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Little-endian reader over a packed params buffer (mirrors the Rust ParamReader). */
|
|
81
|
+
class ParamReader {
|
|
82
|
+
private pos = 0;
|
|
83
|
+
constructor(private readonly buf: Buffer) {}
|
|
84
|
+
/** Bounds-check before a read so a malformed buffer throws a controlled
|
|
85
|
+
* error (trap-equivalent, caught by the dispatcher) rather than a raw
|
|
86
|
+
* Node RangeError. */
|
|
87
|
+
private need(n: number): void {
|
|
88
|
+
if (n < 0 || this.pos + n > this.buf.length)
|
|
89
|
+
throw new Error('crypto: malformed params buffer (truncated)');
|
|
90
|
+
}
|
|
91
|
+
readI32(): number {
|
|
92
|
+
this.need(4);
|
|
93
|
+
const v = this.buf.readInt32LE(this.pos);
|
|
94
|
+
this.pos += 4;
|
|
95
|
+
return v;
|
|
96
|
+
}
|
|
97
|
+
readU32(): number {
|
|
98
|
+
this.need(4);
|
|
99
|
+
const v = this.buf.readUInt32LE(this.pos);
|
|
100
|
+
this.pos += 4;
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
readBlob(): Buffer {
|
|
104
|
+
const n = this.readU32();
|
|
105
|
+
this.need(n);
|
|
106
|
+
const s = Buffer.from(this.buf.subarray(this.pos, this.pos + n));
|
|
107
|
+
this.pos += n;
|
|
108
|
+
return s;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hashName(id: number): string {
|
|
113
|
+
switch (id) {
|
|
114
|
+
case ALG.SHA1: return 'sha1';
|
|
115
|
+
case ALG.SHA256: return 'sha256';
|
|
116
|
+
case ALG.SHA384: return 'sha384';
|
|
117
|
+
case ALG.SHA512: return 'sha512';
|
|
118
|
+
case ALG.SHA3_256: return 'sha3-256';
|
|
119
|
+
case ALG.SHA3_384: return 'sha3-384';
|
|
120
|
+
case ALG.SHA3_512: return 'sha3-512';
|
|
121
|
+
default: throw new Error(`crypto: bad hash id ${String(id)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stash(state: CryptoState, bytes: Buffer): number {
|
|
126
|
+
state.lastResult = bytes;
|
|
127
|
+
return bytes.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build the `env.crypto.*` import functions. `state.crypto` holds the per-
|
|
132
|
+
* dispatch keystore + result scratch.
|
|
133
|
+
*/
|
|
134
|
+
export function buildCryptoImports(
|
|
135
|
+
ref: MemoryRef,
|
|
136
|
+
cs: CryptoState,
|
|
137
|
+
): Record<string, (...args: number[]) => number | void> {
|
|
138
|
+
return {
|
|
139
|
+
'crypto.fill_random': (outPtr: number, len: number): void => {
|
|
140
|
+
if (len < 0 || len > MAX_OUTPUT) throw new Error('crypto.fill_random: bad length');
|
|
141
|
+
writeBytes(ref, outPtr, nodeCrypto.randomBytes(len));
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
'crypto.random_uuid': (outPtr: number): void => {
|
|
145
|
+
writeBytes(ref, outPtr, nodeCrypto.randomBytes(16));
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
'crypto.take_result': (outPtr: number, outLen: number): number => {
|
|
149
|
+
const r = cs.lastResult;
|
|
150
|
+
if (!r || r.length !== outLen)
|
|
151
|
+
throw new Error('crypto.take_result: length mismatch');
|
|
152
|
+
writeBytes(ref, outPtr, r);
|
|
153
|
+
cs.lastResult = null;
|
|
154
|
+
return r.length;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
'crypto.digest': (alg: number, dataPtr: number, dataLen: number): number => {
|
|
158
|
+
const data = readBytes(ref, dataPtr, dataLen);
|
|
159
|
+
try {
|
|
160
|
+
return stash(cs, nodeCrypto.createHash(hashName(alg)).update(data).digest());
|
|
161
|
+
} catch {
|
|
162
|
+
return ERR_UNSUPPORTED;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
'crypto.import_key': (
|
|
167
|
+
format: number,
|
|
168
|
+
keyPtr: number,
|
|
169
|
+
keyLen: number,
|
|
170
|
+
paramsPtr: number,
|
|
171
|
+
paramsLen: number,
|
|
172
|
+
extractable: number,
|
|
173
|
+
_usages: number,
|
|
174
|
+
): number => {
|
|
175
|
+
const key = readBytes(ref, keyPtr, keyLen);
|
|
176
|
+
const pr = new ParamReader(readBytes(ref, paramsPtr, paramsLen));
|
|
177
|
+
const alg = pr.readI32();
|
|
178
|
+
const hash = pr.readI32();
|
|
179
|
+
return importKey(cs, format, alg, hash, pr, key, extractable !== 0);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
'crypto.export_key': (format: number, handle: number): number => {
|
|
183
|
+
const e = cs.keys.get(handle);
|
|
184
|
+
if (!e) throw new Error('crypto.export_key: invalid handle');
|
|
185
|
+
if (!e.extractable) return ERR_NOT_EXTRACTABLE;
|
|
186
|
+
try {
|
|
187
|
+
if (e.raw && format === FMT.RAW) return stash(cs, e.raw);
|
|
188
|
+
if (e.keyObject) {
|
|
189
|
+
if (format === FMT.PKCS8 && e.isPrivate)
|
|
190
|
+
return stash(cs, e.keyObject.export({ format: 'der', type: 'pkcs8' }) as Buffer);
|
|
191
|
+
if (format === FMT.SPKI && !e.isPrivate)
|
|
192
|
+
return stash(cs, e.keyObject.export({ format: 'der', type: 'spki' }) as Buffer);
|
|
193
|
+
}
|
|
194
|
+
return ERR_UNSUPPORTED;
|
|
195
|
+
} catch {
|
|
196
|
+
return ERR_OPERATION_FAILED;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
'crypto.encrypt': (h: number, pp: number, pl: number, dp: number, dl: number): number =>
|
|
201
|
+
aesOp(cs, ref, true, h, pp, pl, dp, dl),
|
|
202
|
+
'crypto.decrypt': (h: number, pp: number, pl: number, dp: number, dl: number): number =>
|
|
203
|
+
aesOp(cs, ref, false, h, pp, pl, dp, dl),
|
|
204
|
+
|
|
205
|
+
'crypto.sign': (h: number, pp: number, pl: number, dp: number, dl: number): number =>
|
|
206
|
+
signOp(cs, ref, h, pp, pl, dp, dl),
|
|
207
|
+
|
|
208
|
+
'crypto.verify': (
|
|
209
|
+
h: number, pp: number, pl: number, sp: number, sl: number, dp: number, dl: number,
|
|
210
|
+
): number => verifyOp(cs, ref, h, pp, pl, sp, sl, dp, dl),
|
|
211
|
+
|
|
212
|
+
'crypto.derive_bits': (h: number, pp: number, pl: number, lengthBits: number): number =>
|
|
213
|
+
deriveBitsOp(cs, ref, h, pp, pl, lengthBits),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function importKey(
|
|
218
|
+
cs: CryptoState,
|
|
219
|
+
format: number,
|
|
220
|
+
alg: number,
|
|
221
|
+
hash: number,
|
|
222
|
+
pr: ParamReader,
|
|
223
|
+
key: Buffer,
|
|
224
|
+
extractable: boolean,
|
|
225
|
+
): number {
|
|
226
|
+
const newEntry = (e: Omit<KeyEntry, 'extractable'>): number => {
|
|
227
|
+
const handle = cs.nextHandle++;
|
|
228
|
+
cs.keys.set(handle, { ...e, extractable });
|
|
229
|
+
return handle;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// Symmetric / MAC / KDF: raw bytes.
|
|
234
|
+
if (
|
|
235
|
+
alg === ALG.AES_GCM || alg === ALG.AES_CBC || alg === ALG.AES_CTR ||
|
|
236
|
+
alg === ALG.AES_KW || alg === ALG.HMAC || alg === ALG.PBKDF2 || alg === ALG.HKDF
|
|
237
|
+
) {
|
|
238
|
+
if (format !== FMT.RAW) return ERR_UNSUPPORTED;
|
|
239
|
+
return newEntry({ raw: key, keyObject: null, alg, hash, isPrivate: false });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Asymmetric: pkcs8 (private) / spki (public). raw not supported in dev.
|
|
243
|
+
const isPrivate = format === FMT.PKCS8;
|
|
244
|
+
if (format === FMT.PKCS8) {
|
|
245
|
+
const ko = nodeCrypto.createPrivateKey({ key, format: 'der', type: 'pkcs8' });
|
|
246
|
+
return newEntry({ raw: null, keyObject: ko, alg, hash, isPrivate });
|
|
247
|
+
}
|
|
248
|
+
if (format === FMT.SPKI) {
|
|
249
|
+
const ko = nodeCrypto.createPublicKey({ key, format: 'der', type: 'spki' });
|
|
250
|
+
return newEntry({ raw: null, keyObject: ko, alg, hash, isPrivate });
|
|
251
|
+
}
|
|
252
|
+
return ERR_UNSUPPORTED;
|
|
253
|
+
} catch {
|
|
254
|
+
return ERR_INVALID_PARAMS;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function aesAlgName(keyLen: number, mode: 'gcm' | 'cbc' | 'ctr'): string {
|
|
259
|
+
const bits = keyLen === 16 ? 128 : keyLen === 32 ? 256 : 0;
|
|
260
|
+
if (!bits) throw new Error('crypto: bad AES key length');
|
|
261
|
+
return `aes-${String(bits)}-${mode}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function aesOp(
|
|
265
|
+
cs: CryptoState, ref: MemoryRef, encrypt: boolean,
|
|
266
|
+
handle: number, pp: number, pl: number, dp: number, dl: number,
|
|
267
|
+
): number {
|
|
268
|
+
const e = cs.keys.get(handle);
|
|
269
|
+
if (!e || !e.raw) throw new Error('crypto: invalid AES key handle');
|
|
270
|
+
const pr = new ParamReader(readBytes(ref, pp, pl));
|
|
271
|
+
const alg = pr.readI32();
|
|
272
|
+
pr.readI32(); // hash (unused)
|
|
273
|
+
const data = readBytes(ref, dp, dl);
|
|
274
|
+
try {
|
|
275
|
+
if (alg === ALG.AES_GCM) {
|
|
276
|
+
const iv = pr.readBlob();
|
|
277
|
+
const tagBits = pr.readI32();
|
|
278
|
+
const aad = pr.readBlob();
|
|
279
|
+
if (tagBits !== 0 && tagBits !== 128) return ERR_INVALID_PARAMS;
|
|
280
|
+
if (encrypt) {
|
|
281
|
+
const c = nodeCrypto.createCipheriv(
|
|
282
|
+
aesAlgName(e.raw.length, 'gcm'), e.raw, iv,
|
|
283
|
+
) as nodeCrypto.CipherGCM;
|
|
284
|
+
if (aad.length) c.setAAD(aad);
|
|
285
|
+
const ct = Buffer.concat([c.update(data), c.final()]);
|
|
286
|
+
return stash(cs, Buffer.concat([ct, c.getAuthTag()]));
|
|
287
|
+
}
|
|
288
|
+
// Ciphertext must be at least the 16-byte tag; shorter input can
|
|
289
|
+
// never authenticate.
|
|
290
|
+
if (data.length < 16) return ERR_OPERATION_FAILED;
|
|
291
|
+
const d = nodeCrypto.createDecipheriv(
|
|
292
|
+
aesAlgName(e.raw.length, 'gcm'), e.raw, iv,
|
|
293
|
+
) as nodeCrypto.DecipherGCM;
|
|
294
|
+
if (aad.length) d.setAAD(aad);
|
|
295
|
+
const tag = data.subarray(data.length - 16);
|
|
296
|
+
const ct = data.subarray(0, data.length - 16);
|
|
297
|
+
d.setAuthTag(tag);
|
|
298
|
+
return stash(cs, Buffer.concat([d.update(ct), d.final()]));
|
|
299
|
+
}
|
|
300
|
+
if (alg === ALG.AES_CBC) {
|
|
301
|
+
const iv = pr.readBlob();
|
|
302
|
+
const c = encrypt
|
|
303
|
+
? nodeCrypto.createCipheriv(aesAlgName(e.raw.length, 'cbc'), e.raw, iv)
|
|
304
|
+
: nodeCrypto.createDecipheriv(aesAlgName(e.raw.length, 'cbc'), e.raw, iv);
|
|
305
|
+
return stash(cs, Buffer.concat([c.update(data), c.final()]));
|
|
306
|
+
}
|
|
307
|
+
if (alg === ALG.AES_CTR) {
|
|
308
|
+
const counter = pr.readBlob();
|
|
309
|
+
const c = encrypt
|
|
310
|
+
? nodeCrypto.createCipheriv(aesAlgName(e.raw.length, 'ctr'), e.raw, counter)
|
|
311
|
+
: nodeCrypto.createDecipheriv(aesAlgName(e.raw.length, 'ctr'), e.raw, counter);
|
|
312
|
+
return stash(cs, Buffer.concat([c.update(data), c.final()]));
|
|
313
|
+
}
|
|
314
|
+
return ERR_UNSUPPORTED;
|
|
315
|
+
} catch {
|
|
316
|
+
return ERR_OPERATION_FAILED;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function signOp(
|
|
321
|
+
cs: CryptoState, ref: MemoryRef,
|
|
322
|
+
handle: number, pp: number, pl: number, dp: number, dl: number,
|
|
323
|
+
): number {
|
|
324
|
+
const e = cs.keys.get(handle);
|
|
325
|
+
if (!e) throw new Error('crypto.sign: invalid handle');
|
|
326
|
+
const pr = new ParamReader(readBytes(ref, pp, pl));
|
|
327
|
+
const alg = pr.readI32();
|
|
328
|
+
const hash = pr.readI32();
|
|
329
|
+
const data = readBytes(ref, dp, dl);
|
|
330
|
+
try {
|
|
331
|
+
if (e.alg === ALG.HMAC && e.raw) {
|
|
332
|
+
return stash(cs, nodeCrypto.createHmac(hashName(e.hash), e.raw).update(data).digest());
|
|
333
|
+
}
|
|
334
|
+
if (e.alg === ALG.ECDSA && e.keyObject) {
|
|
335
|
+
const sig = nodeCrypto.sign(hashName(hash), data, {
|
|
336
|
+
key: e.keyObject,
|
|
337
|
+
dsaEncoding: 'ieee-p1363',
|
|
338
|
+
});
|
|
339
|
+
return stash(cs, sig);
|
|
340
|
+
}
|
|
341
|
+
if (e.alg === ALG.ED25519 && e.keyObject) {
|
|
342
|
+
return stash(cs, nodeCrypto.sign(null, data, e.keyObject));
|
|
343
|
+
}
|
|
344
|
+
void alg;
|
|
345
|
+
return ERR_UNSUPPORTED;
|
|
346
|
+
} catch {
|
|
347
|
+
return ERR_OPERATION_FAILED;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function verifyOp(
|
|
352
|
+
cs: CryptoState, ref: MemoryRef,
|
|
353
|
+
handle: number, pp: number, pl: number, sp: number, sl: number, dp: number, dl: number,
|
|
354
|
+
): number {
|
|
355
|
+
const e = cs.keys.get(handle);
|
|
356
|
+
if (!e) throw new Error('crypto.verify: invalid handle');
|
|
357
|
+
const pr = new ParamReader(readBytes(ref, pp, pl));
|
|
358
|
+
pr.readI32(); // alg
|
|
359
|
+
const hash = pr.readI32();
|
|
360
|
+
const sig = readBytes(ref, sp, sl);
|
|
361
|
+
const data = readBytes(ref, dp, dl);
|
|
362
|
+
try {
|
|
363
|
+
if (e.alg === ALG.HMAC && e.raw) {
|
|
364
|
+
const mac = nodeCrypto.createHmac(hashName(e.hash), e.raw).update(data).digest();
|
|
365
|
+
return mac.length === sig.length && nodeCrypto.timingSafeEqual(mac, sig) ? 1 : 0;
|
|
366
|
+
}
|
|
367
|
+
if (e.alg === ALG.ECDSA && e.keyObject) {
|
|
368
|
+
const ok = nodeCrypto.verify(hashName(hash), data, {
|
|
369
|
+
key: e.keyObject,
|
|
370
|
+
dsaEncoding: 'ieee-p1363',
|
|
371
|
+
}, sig);
|
|
372
|
+
return ok ? 1 : 0;
|
|
373
|
+
}
|
|
374
|
+
if (e.alg === ALG.ED25519 && e.keyObject) {
|
|
375
|
+
return nodeCrypto.verify(null, data, e.keyObject, sig) ? 1 : 0;
|
|
376
|
+
}
|
|
377
|
+
return ERR_UNSUPPORTED;
|
|
378
|
+
} catch {
|
|
379
|
+
return ERR_GENERIC;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function deriveBitsOp(
|
|
384
|
+
cs: CryptoState, ref: MemoryRef,
|
|
385
|
+
handle: number, pp: number, pl: number, lengthBits: number,
|
|
386
|
+
): number {
|
|
387
|
+
if (lengthBits < 0 || lengthBits % 8 !== 0) return ERR_INVALID_PARAMS;
|
|
388
|
+
const outLen = lengthBits / 8;
|
|
389
|
+
if (outLen > MAX_OUTPUT) return ERR_INVALID_PARAMS;
|
|
390
|
+
const e = cs.keys.get(handle);
|
|
391
|
+
if (!e) throw new Error('crypto.derive_bits: invalid handle');
|
|
392
|
+
const pr = new ParamReader(readBytes(ref, pp, pl));
|
|
393
|
+
const alg = pr.readI32();
|
|
394
|
+
const hash = pr.readI32();
|
|
395
|
+
try {
|
|
396
|
+
if (alg === ALG.PBKDF2 && e.raw) {
|
|
397
|
+
const iterations = pr.readU32();
|
|
398
|
+
const salt = pr.readBlob();
|
|
399
|
+
return stash(cs, nodeCrypto.pbkdf2Sync(e.raw, salt, iterations, outLen, hashName(hash)));
|
|
400
|
+
}
|
|
401
|
+
if (alg === ALG.HKDF && e.raw) {
|
|
402
|
+
const salt = pr.readBlob();
|
|
403
|
+
const info = pr.readBlob();
|
|
404
|
+
const okm = nodeCrypto.hkdfSync(hashName(hash), e.raw, salt, info, outLen);
|
|
405
|
+
return stash(cs, Buffer.from(okm));
|
|
406
|
+
}
|
|
407
|
+
if ((alg === ALG.ECDH || alg === ALG.X25519) && e.keyObject) {
|
|
408
|
+
const peerHandle = pr.readI32();
|
|
409
|
+
const peer = cs.keys.get(peerHandle);
|
|
410
|
+
if (!peer || !peer.keyObject) throw new Error('crypto.derive_bits: invalid peer handle');
|
|
411
|
+
const shared = nodeCrypto.diffieHellman({
|
|
412
|
+
privateKey: e.keyObject,
|
|
413
|
+
publicKey: peer.keyObject,
|
|
414
|
+
});
|
|
415
|
+
return stash(cs, Buffer.from(shared.subarray(0, Math.min(outLen, shared.length))));
|
|
416
|
+
}
|
|
417
|
+
return ERR_UNSUPPORTED;
|
|
418
|
+
} catch {
|
|
419
|
+
return ERR_OPERATION_FAILED;
|
|
420
|
+
}
|
|
421
|
+
}
|