@sproutboat/runtime 0.3.0 → 0.5.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/CHANGELOG.md +55 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +532 -2
- package/src/transport-embedded.js +30 -1
- package/src/wrap.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Rate limiter (baronunread/sproutboat#69): `env.<NAME>.limit({ key })` now
|
|
8
|
+
returns `{ success, resetAt }`. `resetAt` is epoch milliseconds for when the
|
|
9
|
+
fixed window rolls and the counter clears, returned on every call (hit or
|
|
10
|
+
miss), so a rejection can carry an accurate `Retry-After`:
|
|
11
|
+
`Math.ceil((resetAt - Date.now()) / 1000)`. The `ratelimit.check` op already
|
|
12
|
+
had `windowStart` and `period` in hand; it just stopped discarding them.
|
|
13
|
+
Additive: existing callers reading only `success` are unaffected.
|
|
14
|
+
|
|
15
|
+
## 0.4.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- `crypto.subtle` subset (baronunread/sproutboat#133): `digest` (SHA-256 / SHA-384
|
|
20
|
+
/ SHA-512) and HMAC `importKey` / `sign` / `verify` over a raw key. Enough for
|
|
21
|
+
JWTs and hand-rolled sessions; no ECDSA, AES or key wrapping.
|
|
22
|
+
|
|
23
|
+
Backed by ~300 lines of reference SHA-2 as inline C rather than BearSSL —
|
|
24
|
+
BearSSL links only in `--standalone` builds and the prelude C is shared with the
|
|
25
|
+
broker transport, so a link dependency would break non-standalone builds. Pure C
|
|
26
|
+
is transport-independent and lets a handler drop a vendored pure-JS SHA-256.
|
|
27
|
+
Verified against NIST vectors on both transports; surface is exactly standard so
|
|
28
|
+
it deletes cleanly when Porffor ships Web Crypto (CanadaHonk/porffor#347).
|
|
29
|
+
- Rate Limiting binding (baronunread/sproutboat#69): `env.<NAME>.limit({ key }) ->
|
|
30
|
+
{ success }`.
|
|
31
|
+
|
|
32
|
+
- Config: `ratelimiters: [{ binding, limit, period }]` in `sproutboat.jsonc` —
|
|
33
|
+
at most `limit` calls per `period` seconds, per key.
|
|
34
|
+
- A fixed-window counter (`ratelimit` table) on **both** transports: the
|
|
35
|
+
embedded one for standalone binaries, and the broker (`ratelimit.check` op,
|
|
36
|
+
in the replay-dedup set) for deployed sprouts. `limit` / `period` travel in
|
|
37
|
+
the message so the op stays stateless.
|
|
38
|
+
- `ponytail`: fixed window, so a burst across the boundary can briefly reach
|
|
39
|
+
~2x; swap for a two-window weighted count if that matters.
|
|
40
|
+
- `crypto.scryptVerify(password, salt, expected, { N, r, p })` (baronunread/sproutboat#153):
|
|
41
|
+
a scrypt (RFC 7914) implementation as inline C — PBKDF2-HMAC-SHA256 (reusing
|
|
42
|
+
#133's HMAC) + Salsa20/8 + BlockMix + ROMix.
|
|
43
|
+
|
|
44
|
+
Deliberately verify-only (re-derive and constant-time compare, no exposed raw
|
|
45
|
+
`scrypt()`): a migration path for password hashes made by Node/Bun `scrypt`
|
|
46
|
+
(e.g. `N=32768 r=8 p=3`), not a KDF blessed for new credentials — those should
|
|
47
|
+
use HMAC/PBKDF2 via `crypto.subtle`. Verified against the RFC 7914 §12 vector.
|
|
48
|
+
`N*r` is capped so a bad config cannot ask for gigabytes of scratch.
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- Embedded transport: set `PRAGMA busy_timeout=5000` on every connection. Within
|
|
53
|
+
one binary all binding ops are serial so there is no self-contention, but when
|
|
54
|
+
several copies of a standalone binary share a data dir behind `SO_REUSEPORT`
|
|
55
|
+
(the #154 scaling recipe) a writer now waits for the WAL lock instead of
|
|
56
|
+
failing `SQLITE_BUSY`.
|
|
57
|
+
|
|
3
58
|
## 0.3.0
|
|
4
59
|
|
|
5
60
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// json(). This adds, additively, the rest of the WHATWG surface Worker code
|
|
5
5
|
// expects: URLSearchParams (read + write), URL.prototype.searchParams and the
|
|
6
6
|
// protocol/host/hostname/port/hash accessors, static Response.json,
|
|
7
|
-
// crypto.randomUUID / crypto.getRandomValues,
|
|
8
|
-
// feature-detected; delete a
|
|
7
|
+
// crypto.randomUUID / crypto.getRandomValues, crypto.subtle (digest + HMAC
|
|
8
|
+
// sign/verify, #133), and structuredClone. Each is feature-detected; delete a
|
|
9
|
+
// block once Porffor ships that global. crypto.scryptVerify (#153) is a
|
|
10
|
+
// Sproutboat extension, not WHATWG, and stays.
|
|
9
11
|
// Tracked upstream in patches/UPSTREAM.md.
|
|
10
12
|
//
|
|
11
13
|
// Declared before it is referenced: a getter body that names a later top-level
|
|
@@ -276,6 +278,141 @@ if (globalThis.crypto.randomUUID == null) {
|
|
|
276
278
|
};
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
// #133 — a WebCrypto subset: crypto.subtle.digest (SHA-256/384/512) and HMAC
|
|
282
|
+
// sign/verify over a raw key. Enough for JWTs and hand-rolled sessions; not a
|
|
283
|
+
// complete SubtleCrypto (no ECDSA, no AES, no key wrapping). The surface is
|
|
284
|
+
// exactly standard so it can be deleted when Porffor ships Web Crypto
|
|
285
|
+
// (CanadaHonk/porffor#347). Backed by the inline-C SHA-2 above, not a link
|
|
286
|
+
// dependency, so it works on the broker transport too.
|
|
287
|
+
function __sbHashBits(algo) {
|
|
288
|
+
const n = String((algo && algo.name) || algo || "").toUpperCase();
|
|
289
|
+
if (n === "SHA-256" || n === "SHA256") return "256";
|
|
290
|
+
if (n === "SHA-384" || n === "SHA384") return "384";
|
|
291
|
+
if (n === "SHA-512" || n === "SHA512") return "512";
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
// -> a latin1 string, one char per byte. Strings are UTF-8 encoded (matching
|
|
295
|
+
// TextEncoder); ArrayBuffer / typed-array input is copied byte for byte.
|
|
296
|
+
function __sbToBytes(input) {
|
|
297
|
+
if (input == null) return "";
|
|
298
|
+
if (__sbIsStr(input)) {
|
|
299
|
+
let s = "";
|
|
300
|
+
for (let i = 0; i < input.length; i++) {
|
|
301
|
+
const c = input.charCodeAt(i);
|
|
302
|
+
if (c < 0x80) s += String.fromCharCode(c);
|
|
303
|
+
else if (c < 0x800) s += String.fromCharCode(0xc0 | (c >> 6)) + String.fromCharCode(0x80 | (c & 0x3f));
|
|
304
|
+
else if (c >= 0xd800 && c < 0xdc00 && i + 1 < input.length) {
|
|
305
|
+
const cp = 0x10000 + ((c - 0xd800) << 10) + (input.charCodeAt(++i) - 0xdc00);
|
|
306
|
+
s +=
|
|
307
|
+
String.fromCharCode(0xf0 | (cp >> 18)) +
|
|
308
|
+
String.fromCharCode(0x80 | ((cp >> 12) & 0x3f)) +
|
|
309
|
+
String.fromCharCode(0x80 | ((cp >> 6) & 0x3f)) +
|
|
310
|
+
String.fromCharCode(0x80 | (cp & 0x3f));
|
|
311
|
+
} else
|
|
312
|
+
s +=
|
|
313
|
+
String.fromCharCode(0xe0 | (c >> 12)) +
|
|
314
|
+
String.fromCharCode(0x80 | ((c >> 6) & 0x3f)) +
|
|
315
|
+
String.fromCharCode(0x80 | (c & 0x3f));
|
|
316
|
+
}
|
|
317
|
+
return s;
|
|
318
|
+
}
|
|
319
|
+
const view = input.length !== undefined && input.buffer !== undefined ? input : new Uint8Array(input);
|
|
320
|
+
let s = "";
|
|
321
|
+
for (let i = 0; i < view.length; i++) s += String.fromCharCode(view[i] & 0xff);
|
|
322
|
+
return s;
|
|
323
|
+
}
|
|
324
|
+
function __sbBufFrom(latin1) {
|
|
325
|
+
const b = new Uint8Array(latin1.length);
|
|
326
|
+
for (let i = 0; i < latin1.length; i++) b[i] = latin1.charCodeAt(i) & 0xff;
|
|
327
|
+
return b.buffer;
|
|
328
|
+
}
|
|
329
|
+
// Constant-time compare of two latin1 byte strings.
|
|
330
|
+
function __sbEqCt(a, b) {
|
|
331
|
+
if (a.length !== b.length) return false;
|
|
332
|
+
let diff = 0;
|
|
333
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
334
|
+
return diff === 0;
|
|
335
|
+
}
|
|
336
|
+
// An even-length all-hex string -> its bytes; anything else -> its raw bytes.
|
|
337
|
+
function __sbHexOrBytes(value) {
|
|
338
|
+
if (__sbIsStr(value) && value.length > 0 && value.length % 2 === 0) {
|
|
339
|
+
let out = "";
|
|
340
|
+
for (let i = 0; i < value.length; i++) {
|
|
341
|
+
const c = value.charCodeAt(i);
|
|
342
|
+
const d = c >= 48 && c <= 57 ? c - 48 : c >= 97 && c <= 102 ? c - 87 : c >= 65 && c <= 70 ? c - 55 : -1;
|
|
343
|
+
if (d < 0) return __sbToBytes(value);
|
|
344
|
+
if (i % 2 === 0) out += String.fromCharCode(d << 4);
|
|
345
|
+
else out = out.slice(0, -1) + String.fromCharCode(out.charCodeAt(out.length - 1) | d);
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
return __sbToBytes(value);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (globalThis.crypto.subtle == null) {
|
|
353
|
+
globalThis.crypto.subtle = {
|
|
354
|
+
async digest(algo, data) {
|
|
355
|
+
const bits = __sbHashBits(algo);
|
|
356
|
+
if (!bits) throw new Error("crypto.subtle.digest: unsupported algorithm");
|
|
357
|
+
const out = __sbDigestRaw(bits, __sbToBytes(data));
|
|
358
|
+
if (!out) throw new Error("crypto.subtle.digest failed");
|
|
359
|
+
return __sbBufFrom(out);
|
|
360
|
+
},
|
|
361
|
+
async importKey(format, keyData, algo, extractable, usages) {
|
|
362
|
+
if (format !== "raw") throw new Error("crypto.subtle.importKey: only format 'raw' is supported");
|
|
363
|
+
const name = String((algo && algo.name) || algo || "").toUpperCase();
|
|
364
|
+
if (name !== "HMAC") throw new Error("crypto.subtle.importKey: only HMAC keys are supported");
|
|
365
|
+
const bits = __sbHashBits((algo && algo.hash) || "SHA-256");
|
|
366
|
+
if (!bits) throw new Error("crypto.subtle.importKey: unsupported hash");
|
|
367
|
+
return {
|
|
368
|
+
type: "secret",
|
|
369
|
+
extractable: !!extractable,
|
|
370
|
+
algorithm: { name: "HMAC", hash: { name: "SHA-" + bits } },
|
|
371
|
+
usages: usages || [],
|
|
372
|
+
__sbKey: __sbToBytes(keyData),
|
|
373
|
+
__sbBits: bits,
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
async sign(algo, key, data) {
|
|
377
|
+
if (!key || key.__sbKey == null) throw new Error("crypto.subtle.sign: only HMAC keys from importKey are supported");
|
|
378
|
+
const out = __sbHmacRaw(key.__sbBits, key.__sbKey, __sbToBytes(data));
|
|
379
|
+
if (!out) throw new Error("crypto.subtle.sign failed");
|
|
380
|
+
return __sbBufFrom(out);
|
|
381
|
+
},
|
|
382
|
+
async verify(algo, key, signature, data) {
|
|
383
|
+
if (!key || key.__sbKey == null) throw new Error("crypto.subtle.verify: only HMAC keys are supported");
|
|
384
|
+
const mac = __sbHmacRaw(key.__sbBits, key.__sbKey, __sbToBytes(data));
|
|
385
|
+
if (!mac) return false;
|
|
386
|
+
return __sbEqCt(mac, __sbToBytes(signature));
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// #153 — scrypt verification, for migrating password hashes made elsewhere
|
|
392
|
+
// (Node/Bun `scrypt`, N/r/p). Not on WebCrypto; deliberately verify-only, so it
|
|
393
|
+
// is a migration tool rather than a KDF blessed for new credentials — new
|
|
394
|
+
// credentials should use HMAC/PBKDF2 via crypto.subtle. `expected` is the stored
|
|
395
|
+
// hash: a hex string, or bytes (ArrayBuffer / Uint8Array).
|
|
396
|
+
if (globalThis.crypto.scryptVerify == null) {
|
|
397
|
+
globalThis.crypto.scryptVerify = function (password, salt, expected, params) {
|
|
398
|
+
const p = params || {};
|
|
399
|
+
const N = p.N || p.cost || 16384;
|
|
400
|
+
const r = p.r || p.blockSize || 8;
|
|
401
|
+
const par = p.p || p.parallelization || 1;
|
|
402
|
+
const want = __sbHexOrBytes(expected);
|
|
403
|
+
if (want.length === 0) return false;
|
|
404
|
+
const dk = __sbScryptRaw(
|
|
405
|
+
__sbToBytes(password),
|
|
406
|
+
__sbToBytes(salt),
|
|
407
|
+
String(N),
|
|
408
|
+
String(r),
|
|
409
|
+
String(par),
|
|
410
|
+
String(want.length),
|
|
411
|
+
);
|
|
412
|
+
return dk.length === want.length && __sbEqCt(dk, want);
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
279
416
|
// ---------------------------------------------------------------------------
|
|
280
417
|
// Bindings: env.<KV>, env.<SECRET>, env.<D1>, env.<R2>, and globalThis.fetch,
|
|
281
418
|
// backed by a Bun broker on a loopback TCP port. The transport is inline C —
|
|
@@ -300,10 +437,291 @@ Porffor.c`
|
|
|
300
437
|
#include <fcntl.h>
|
|
301
438
|
#include <errno.h>
|
|
302
439
|
#include <time.h>
|
|
440
|
+
#include <stdint.h>
|
|
303
441
|
|
|
304
442
|
u32 porf_native_fetch_alloc_bytestring(const char* input, size_t len);
|
|
305
443
|
int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned);
|
|
306
444
|
|
|
445
|
+
// --- #133: SHA-2 + HMAC ---------------------------------------------------
|
|
446
|
+
// Standalone links BearSSL, but a deployed sprout does not, and this inline C
|
|
447
|
+
// is shared by both transports — so a plain reference SHA-2 rather than a link
|
|
448
|
+
// dependency. It also lets a handler drop a vendored pure-JS SHA-256. Exposed
|
|
449
|
+
// as crypto.subtle.digest / sign / verify (HMAC), which deletes cleanly when
|
|
450
|
+
// Porffor ships Web Crypto (CanadaHonk/porffor#347).
|
|
451
|
+
#define SB_ROR32(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
|
|
452
|
+
#define SB_ROR64(x, n) (((x) >> (n)) | ((x) << (64 - (n))))
|
|
453
|
+
|
|
454
|
+
typedef struct { uint32_t h[8]; uint64_t len; unsigned char buf[64]; size_t n; } sb_sha256_ctx;
|
|
455
|
+
static const uint32_t sb_k256[64] = {
|
|
456
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
457
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
458
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
459
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
460
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
461
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
462
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
463
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 };
|
|
464
|
+
static void sb_sha256_block(sb_sha256_ctx* c, const unsigned char* p) {
|
|
465
|
+
uint32_t w[64], a,b,cc,d,e,f,g,h,t1,t2;
|
|
466
|
+
for (int i = 0; i < 16; i++)
|
|
467
|
+
w[i] = ((uint32_t)p[i*4]<<24)|((uint32_t)p[i*4+1]<<16)|((uint32_t)p[i*4+2]<<8)|((uint32_t)p[i*4+3]);
|
|
468
|
+
for (int i = 16; i < 64; i++) {
|
|
469
|
+
uint32_t s0 = SB_ROR32(w[i-15],7) ^ SB_ROR32(w[i-15],18) ^ (w[i-15]>>3);
|
|
470
|
+
uint32_t s1 = SB_ROR32(w[i-2],17) ^ SB_ROR32(w[i-2],19) ^ (w[i-2]>>10);
|
|
471
|
+
w[i] = w[i-16] + s0 + w[i-7] + s1;
|
|
472
|
+
}
|
|
473
|
+
a=c->h[0];b=c->h[1];cc=c->h[2];d=c->h[3];e=c->h[4];f=c->h[5];g=c->h[6];h=c->h[7];
|
|
474
|
+
for (int i = 0; i < 64; i++) {
|
|
475
|
+
uint32_t S1 = SB_ROR32(e,6) ^ SB_ROR32(e,11) ^ SB_ROR32(e,25);
|
|
476
|
+
uint32_t ch = (e & f) ^ (~e & g);
|
|
477
|
+
t1 = h + S1 + ch + sb_k256[i] + w[i];
|
|
478
|
+
uint32_t S0 = SB_ROR32(a,2) ^ SB_ROR32(a,13) ^ SB_ROR32(a,22);
|
|
479
|
+
uint32_t maj = (a & b) ^ (a & cc) ^ (b & cc);
|
|
480
|
+
t2 = S0 + maj;
|
|
481
|
+
h=g;g=f;f=e;e=d+t1;d=cc;cc=b;b=a;a=t1+t2;
|
|
482
|
+
}
|
|
483
|
+
c->h[0]+=a;c->h[1]+=b;c->h[2]+=cc;c->h[3]+=d;c->h[4]+=e;c->h[5]+=f;c->h[6]+=g;c->h[7]+=h;
|
|
484
|
+
}
|
|
485
|
+
static void sb_sha256_init(sb_sha256_ctx* c) {
|
|
486
|
+
c->h[0]=0x6a09e667;c->h[1]=0xbb67ae85;c->h[2]=0x3c6ef372;c->h[3]=0xa54ff53a;
|
|
487
|
+
c->h[4]=0x510e527f;c->h[5]=0x9b05688c;c->h[6]=0x1f83d9ab;c->h[7]=0x5be0cd19;c->len=0;c->n=0;
|
|
488
|
+
}
|
|
489
|
+
static void sb_sha256_update(sb_sha256_ctx* c, const unsigned char* p, size_t n) {
|
|
490
|
+
c->len += n;
|
|
491
|
+
while (n) {
|
|
492
|
+
size_t k = 64 - c->n; if (k > n) k = n;
|
|
493
|
+
memcpy(c->buf + c->n, p, k); c->n += k; p += k; n -= k;
|
|
494
|
+
if (c->n == 64) { sb_sha256_block(c, c->buf); c->n = 0; }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
static void sb_sha256_final(sb_sha256_ctx* c, unsigned char out[32]) {
|
|
498
|
+
uint64_t bits = c->len * 8;
|
|
499
|
+
unsigned char pad = 0x80;
|
|
500
|
+
sb_sha256_update(c, &pad, 1);
|
|
501
|
+
unsigned char z = 0;
|
|
502
|
+
while (c->n != 56) sb_sha256_update(c, &z, 1);
|
|
503
|
+
unsigned char lb[8];
|
|
504
|
+
for (int i = 0; i < 8; i++) lb[i] = (unsigned char)(bits >> (56 - i*8));
|
|
505
|
+
sb_sha256_update(c, lb, 8);
|
|
506
|
+
for (int i = 0; i < 8; i++) {
|
|
507
|
+
out[i*4]=(unsigned char)(c->h[i]>>24);out[i*4+1]=(unsigned char)(c->h[i]>>16);
|
|
508
|
+
out[i*4+2]=(unsigned char)(c->h[i]>>8);out[i*4+3]=(unsigned char)c->h[i];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
typedef struct { uint64_t h[8]; uint64_t lenhi, lenlo; unsigned char buf[128]; size_t n; } sb_sha512_ctx;
|
|
513
|
+
static const uint64_t sb_k512[80] = {
|
|
514
|
+
0x428a2f98d728ae22ULL,0x7137449123ef65cdULL,0xb5c0fbcfec4d3b2fULL,0xe9b5dba58189dbbcULL,
|
|
515
|
+
0x3956c25bf348b538ULL,0x59f111f1b605d019ULL,0x923f82a4af194f9bULL,0xab1c5ed5da6d8118ULL,
|
|
516
|
+
0xd807aa98a3030242ULL,0x12835b0145706fbeULL,0x243185be4ee4b28cULL,0x550c7dc3d5ffb4e2ULL,
|
|
517
|
+
0x72be5d74f27b896fULL,0x80deb1fe3b1696b1ULL,0x9bdc06a725c71235ULL,0xc19bf174cf692694ULL,
|
|
518
|
+
0xe49b69c19ef14ad2ULL,0xefbe4786384f25e3ULL,0x0fc19dc68b8cd5b5ULL,0x240ca1cc77ac9c65ULL,
|
|
519
|
+
0x2de92c6f592b0275ULL,0x4a7484aa6ea6e483ULL,0x5cb0a9dcbd41fbd4ULL,0x76f988da831153b5ULL,
|
|
520
|
+
0x983e5152ee66dfabULL,0xa831c66d2db43210ULL,0xb00327c898fb213fULL,0xbf597fc7beef0ee4ULL,
|
|
521
|
+
0xc6e00bf33da88fc2ULL,0xd5a79147930aa725ULL,0x06ca6351e003826fULL,0x142929670a0e6e70ULL,
|
|
522
|
+
0x27b70a8546d22ffcULL,0x2e1b21385c26c926ULL,0x4d2c6dfc5ac42aedULL,0x53380d139d95b3dfULL,
|
|
523
|
+
0x650a73548baf63deULL,0x766a0abb3c77b2a8ULL,0x81c2c92e47edaee6ULL,0x92722c851482353bULL,
|
|
524
|
+
0xa2bfe8a14cf10364ULL,0xa81a664bbc423001ULL,0xc24b8b70d0f89791ULL,0xc76c51a30654be30ULL,
|
|
525
|
+
0xd192e819d6ef5218ULL,0xd69906245565a910ULL,0xf40e35855771202aULL,0x106aa07032bbd1b8ULL,
|
|
526
|
+
0x19a4c116b8d2d0c8ULL,0x1e376c085141ab53ULL,0x2748774cdf8eeb99ULL,0x34b0bcb5e19b48a8ULL,
|
|
527
|
+
0x391c0cb3c5c95a63ULL,0x4ed8aa4ae3418acbULL,0x5b9cca4f7763e373ULL,0x682e6ff3d6b2b8a3ULL,
|
|
528
|
+
0x748f82ee5defb2fcULL,0x78a5636f43172f60ULL,0x84c87814a1f0ab72ULL,0x8cc702081a6439ecULL,
|
|
529
|
+
0x90befffa23631e28ULL,0xa4506cebde82bde9ULL,0xbef9a3f7b2c67915ULL,0xc67178f2e372532bULL,
|
|
530
|
+
0xca273eceea26619cULL,0xd186b8c721c0c207ULL,0xeada7dd6cde0eb1eULL,0xf57d4f7fee6ed178ULL,
|
|
531
|
+
0x06f067aa72176fbaULL,0x0a637dc5a2c898a6ULL,0x113f9804bef90daeULL,0x1b710b35131c471bULL,
|
|
532
|
+
0x28db77f523047d84ULL,0x32caab7b40c72493ULL,0x3c9ebe0a15c9bebcULL,0x431d67c49c100d4cULL,
|
|
533
|
+
0x4cc5d4becb3e42b6ULL,0x597f299cfc657e2aULL,0x5fcb6fab3ad6faecULL,0x6c44198c4a475817ULL };
|
|
534
|
+
static void sb_sha512_block(sb_sha512_ctx* c, const unsigned char* p) {
|
|
535
|
+
uint64_t w[80], a,b,cc,d,e,f,g,h,t1,t2;
|
|
536
|
+
for (int i = 0; i < 16; i++) {
|
|
537
|
+
w[i] = 0; for (int j = 0; j < 8; j++) w[i] = (w[i]<<8) | p[i*8+j];
|
|
538
|
+
}
|
|
539
|
+
for (int i = 16; i < 80; i++) {
|
|
540
|
+
uint64_t s0 = SB_ROR64(w[i-15],1) ^ SB_ROR64(w[i-15],8) ^ (w[i-15]>>7);
|
|
541
|
+
uint64_t s1 = SB_ROR64(w[i-2],19) ^ SB_ROR64(w[i-2],61) ^ (w[i-2]>>6);
|
|
542
|
+
w[i] = w[i-16] + s0 + w[i-7] + s1;
|
|
543
|
+
}
|
|
544
|
+
a=c->h[0];b=c->h[1];cc=c->h[2];d=c->h[3];e=c->h[4];f=c->h[5];g=c->h[6];h=c->h[7];
|
|
545
|
+
for (int i = 0; i < 80; i++) {
|
|
546
|
+
uint64_t S1 = SB_ROR64(e,14) ^ SB_ROR64(e,18) ^ SB_ROR64(e,41);
|
|
547
|
+
uint64_t ch = (e & f) ^ (~e & g);
|
|
548
|
+
t1 = h + S1 + ch + sb_k512[i] + w[i];
|
|
549
|
+
uint64_t S0 = SB_ROR64(a,28) ^ SB_ROR64(a,34) ^ SB_ROR64(a,39);
|
|
550
|
+
uint64_t maj = (a & b) ^ (a & cc) ^ (b & cc);
|
|
551
|
+
t2 = S0 + maj;
|
|
552
|
+
h=g;g=f;f=e;e=d+t1;d=cc;cc=b;b=a;a=t1+t2;
|
|
553
|
+
}
|
|
554
|
+
c->h[0]+=a;c->h[1]+=b;c->h[2]+=cc;c->h[3]+=d;c->h[4]+=e;c->h[5]+=f;c->h[6]+=g;c->h[7]+=h;
|
|
555
|
+
}
|
|
556
|
+
static void sb_sha512_init(sb_sha512_ctx* c, int is384) {
|
|
557
|
+
if (is384) {
|
|
558
|
+
c->h[0]=0xcbbb9d5dc1059ed8ULL;c->h[1]=0x629a292a367cd507ULL;c->h[2]=0x9159015a3070dd17ULL;c->h[3]=0x152fecd8f70e5939ULL;
|
|
559
|
+
c->h[4]=0x67332667ffc00b31ULL;c->h[5]=0x8eb44a8768581511ULL;c->h[6]=0xdb0c2e0d64f98fa7ULL;c->h[7]=0x47b5481dbefa4fa4ULL;
|
|
560
|
+
} else {
|
|
561
|
+
c->h[0]=0x6a09e667f3bcc908ULL;c->h[1]=0xbb67ae8584caa73bULL;c->h[2]=0x3c6ef372fe94f82bULL;c->h[3]=0xa54ff53a5f1d36f1ULL;
|
|
562
|
+
c->h[4]=0x510e527fade682d1ULL;c->h[5]=0x9b05688c2b3e6c1fULL;c->h[6]=0x1f83d9abfb41bd6bULL;c->h[7]=0x5be0cd19137e2179ULL;
|
|
563
|
+
}
|
|
564
|
+
c->lenhi=0;c->lenlo=0;c->n=0;
|
|
565
|
+
}
|
|
566
|
+
static void sb_sha512_update(sb_sha512_ctx* c, const unsigned char* p, size_t n) {
|
|
567
|
+
uint64_t add = n; if ((c->lenlo += add) < add) c->lenhi++;
|
|
568
|
+
while (n) {
|
|
569
|
+
size_t k = 128 - c->n; if (k > n) k = n;
|
|
570
|
+
memcpy(c->buf + c->n, p, k); c->n += k; p += k; n -= k;
|
|
571
|
+
if (c->n == 128) { sb_sha512_block(c, c->buf); c->n = 0; }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
static void sb_sha512_final(sb_sha512_ctx* c, unsigned char* out, int is384) {
|
|
575
|
+
uint64_t bhi = (c->lenhi << 3) | (c->lenlo >> 61), blo = c->lenlo << 3;
|
|
576
|
+
unsigned char pad = 0x80;
|
|
577
|
+
sb_sha512_update(c, &pad, 1);
|
|
578
|
+
unsigned char z = 0;
|
|
579
|
+
while (c->n != 112) sb_sha512_update(c, &z, 1);
|
|
580
|
+
unsigned char lb[16];
|
|
581
|
+
for (int i = 0; i < 8; i++) lb[i] = (unsigned char)(bhi >> (56 - i*8));
|
|
582
|
+
for (int i = 0; i < 8; i++) lb[8+i] = (unsigned char)(blo >> (56 - i*8));
|
|
583
|
+
sb_sha512_update(c, lb, 16);
|
|
584
|
+
int words = is384 ? 6 : 8;
|
|
585
|
+
for (int i = 0; i < words; i++)
|
|
586
|
+
for (int j = 0; j < 8; j++) out[i*8+j] = (unsigned char)(c->h[i] >> (56 - j*8));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// One-shot digest. algo: 256 | 384 | 512. out must hold 32/48/64 bytes.
|
|
590
|
+
// Returns the digest length, or 0 for an unknown algo.
|
|
591
|
+
static size_t sb_digest(int algo, const unsigned char* msg, size_t mlen, unsigned char* out) {
|
|
592
|
+
if (algo == 256) { sb_sha256_ctx c; sb_sha256_init(&c); sb_sha256_update(&c, msg, mlen); sb_sha256_final(&c, out); return 32; }
|
|
593
|
+
if (algo == 384) { sb_sha512_ctx c; sb_sha512_init(&c, 1); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, out, 1); return 48; }
|
|
594
|
+
if (algo == 512) { sb_sha512_ctx c; sb_sha512_init(&c, 0); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, out, 0); return 64; }
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// HMAC(algo) per RFC 2104. Block size is 64 for SHA-256, 128 for SHA-384/512.
|
|
599
|
+
static size_t sb_hmac(int algo, const unsigned char* key, size_t klen,
|
|
600
|
+
const unsigned char* msg, size_t mlen, unsigned char* out) {
|
|
601
|
+
size_t bs = algo == 256 ? 64 : 128;
|
|
602
|
+
unsigned char k[128], ipad[128], opad[128], inner[64];
|
|
603
|
+
memset(k, 0, sizeof(k));
|
|
604
|
+
if (klen > bs) { sb_digest(algo, key, klen, k); }
|
|
605
|
+
else memcpy(k, key, klen);
|
|
606
|
+
for (size_t i = 0; i < bs; i++) { ipad[i] = k[i] ^ 0x36; opad[i] = k[i] ^ 0x5c; }
|
|
607
|
+
size_t dl;
|
|
608
|
+
if (algo == 256) {
|
|
609
|
+
sb_sha256_ctx c; sb_sha256_init(&c);
|
|
610
|
+
sb_sha256_update(&c, ipad, bs); sb_sha256_update(&c, msg, mlen); sb_sha256_final(&c, inner);
|
|
611
|
+
sb_sha256_ctx o; sb_sha256_init(&o);
|
|
612
|
+
sb_sha256_update(&o, opad, bs); sb_sha256_update(&o, inner, 32); sb_sha256_final(&o, out);
|
|
613
|
+
dl = 32;
|
|
614
|
+
} else {
|
|
615
|
+
int is384 = algo == 384; dl = is384 ? 48 : 64;
|
|
616
|
+
sb_sha512_ctx c; sb_sha512_init(&c, is384);
|
|
617
|
+
sb_sha512_update(&c, ipad, bs); sb_sha512_update(&c, msg, mlen); sb_sha512_final(&c, inner, is384);
|
|
618
|
+
sb_sha512_ctx o; sb_sha512_init(&o, is384);
|
|
619
|
+
sb_sha512_update(&o, opad, bs); sb_sha512_update(&o, inner, dl); sb_sha512_final(&o, out, is384);
|
|
620
|
+
}
|
|
621
|
+
return dl;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// --- #153: scrypt (RFC 7914), for verifying pre-existing password hashes ----
|
|
625
|
+
// PBKDF2-HMAC-SHA256 (reuses sb_hmac) + Salsa20/8 + BlockMix + ROMix. Exposed
|
|
626
|
+
// only as crypto.scryptVerify: a migration path for hashes made elsewhere
|
|
627
|
+
// (Node/Bun scrypt), not a blessed KDF for new credentials.
|
|
628
|
+
static void sb_pbkdf2_hmac256(const unsigned char* pw, size_t pwlen,
|
|
629
|
+
const unsigned char* salt, size_t saltlen,
|
|
630
|
+
uint32_t iters, unsigned char* out, size_t dklen) {
|
|
631
|
+
unsigned char block[64], u[32], t[32];
|
|
632
|
+
uint32_t i = 1;
|
|
633
|
+
size_t done = 0;
|
|
634
|
+
while (done < dklen) {
|
|
635
|
+
unsigned char* s2 = (unsigned char*)malloc(saltlen + 4);
|
|
636
|
+
memcpy(s2, salt, saltlen);
|
|
637
|
+
s2[saltlen] = (unsigned char)(i >> 24); s2[saltlen+1] = (unsigned char)(i >> 16);
|
|
638
|
+
s2[saltlen+2] = (unsigned char)(i >> 8); s2[saltlen+3] = (unsigned char)i;
|
|
639
|
+
sb_hmac(256, pw, pwlen, s2, saltlen + 4, u);
|
|
640
|
+
free(s2);
|
|
641
|
+
memcpy(t, u, 32);
|
|
642
|
+
// iters is 1 for scrypt's use of PBKDF2, but support the general case.
|
|
643
|
+
for (uint32_t j = 1; j < iters; j++) {
|
|
644
|
+
sb_hmac(256, pw, pwlen, u, 32, u);
|
|
645
|
+
for (int k = 0; k < 32; k++) t[k] ^= u[k];
|
|
646
|
+
}
|
|
647
|
+
memcpy(block, t, 32);
|
|
648
|
+
size_t take = dklen - done; if (take > 32) take = 32;
|
|
649
|
+
memcpy(out + done, block, take);
|
|
650
|
+
done += take; i++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
static void sb_salsa20_8(uint32_t out[16], const uint32_t in[16]) {
|
|
654
|
+
uint32_t x[16];
|
|
655
|
+
for (int i = 0; i < 16; i++) x[i] = in[i];
|
|
656
|
+
for (int i = 0; i < 4; i++) {
|
|
657
|
+
x[ 4] ^= SB_ROR32(x[ 0] + x[12], 32 - 7); x[ 8] ^= SB_ROR32(x[ 4] + x[ 0], 32 - 9);
|
|
658
|
+
x[12] ^= SB_ROR32(x[ 8] + x[ 4], 32 - 13); x[ 0] ^= SB_ROR32(x[12] + x[ 8], 32 - 18);
|
|
659
|
+
x[ 9] ^= SB_ROR32(x[ 5] + x[ 1], 32 - 7); x[13] ^= SB_ROR32(x[ 9] + x[ 5], 32 - 9);
|
|
660
|
+
x[ 1] ^= SB_ROR32(x[13] + x[ 9], 32 - 13); x[ 5] ^= SB_ROR32(x[ 1] + x[13], 32 - 18);
|
|
661
|
+
x[14] ^= SB_ROR32(x[10] + x[ 6], 32 - 7); x[ 2] ^= SB_ROR32(x[14] + x[10], 32 - 9);
|
|
662
|
+
x[ 6] ^= SB_ROR32(x[ 2] + x[14], 32 - 13); x[10] ^= SB_ROR32(x[ 6] + x[ 2], 32 - 18);
|
|
663
|
+
x[ 3] ^= SB_ROR32(x[15] + x[11], 32 - 7); x[ 7] ^= SB_ROR32(x[ 3] + x[15], 32 - 9);
|
|
664
|
+
x[11] ^= SB_ROR32(x[ 7] + x[ 3], 32 - 13); x[15] ^= SB_ROR32(x[11] + x[ 7], 32 - 18);
|
|
665
|
+
x[ 1] ^= SB_ROR32(x[ 0] + x[ 3], 32 - 7); x[ 2] ^= SB_ROR32(x[ 1] + x[ 0], 32 - 9);
|
|
666
|
+
x[ 3] ^= SB_ROR32(x[ 2] + x[ 1], 32 - 13); x[ 0] ^= SB_ROR32(x[ 3] + x[ 2], 32 - 18);
|
|
667
|
+
x[ 6] ^= SB_ROR32(x[ 5] + x[ 4], 32 - 7); x[ 7] ^= SB_ROR32(x[ 6] + x[ 5], 32 - 9);
|
|
668
|
+
x[ 4] ^= SB_ROR32(x[ 7] + x[ 6], 32 - 13); x[ 5] ^= SB_ROR32(x[ 4] + x[ 7], 32 - 18);
|
|
669
|
+
x[11] ^= SB_ROR32(x[10] + x[ 9], 32 - 7); x[ 8] ^= SB_ROR32(x[11] + x[10], 32 - 9);
|
|
670
|
+
x[ 9] ^= SB_ROR32(x[ 8] + x[11], 32 - 13); x[10] ^= SB_ROR32(x[ 9] + x[ 8], 32 - 18);
|
|
671
|
+
x[12] ^= SB_ROR32(x[15] + x[14], 32 - 7); x[13] ^= SB_ROR32(x[12] + x[15], 32 - 9);
|
|
672
|
+
x[14] ^= SB_ROR32(x[13] + x[12], 32 - 13); x[15] ^= SB_ROR32(x[14] + x[13], 32 - 18);
|
|
673
|
+
}
|
|
674
|
+
for (int i = 0; i < 16; i++) out[i] = x[i] + in[i];
|
|
675
|
+
}
|
|
676
|
+
// BlockMix: B is 2r 64-byte blocks (as uint32 words). Result into out.
|
|
677
|
+
static void sb_blockmix(const uint32_t* b, uint32_t* out, size_t r) {
|
|
678
|
+
uint32_t x[16], y[16];
|
|
679
|
+
memcpy(x, b + (2*r - 1) * 16, 64);
|
|
680
|
+
for (size_t i = 0; i < 2*r; i++) {
|
|
681
|
+
for (int k = 0; k < 16; k++) x[k] ^= b[i*16 + k];
|
|
682
|
+
sb_salsa20_8(y, x);
|
|
683
|
+
memcpy(x, y, 64);
|
|
684
|
+
size_t dst = (i % 2 == 0) ? (i/2) : (r + (i-1)/2);
|
|
685
|
+
memcpy(out + dst*16, x, 64);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
static uint64_t sb_integerify(const uint32_t* b, size_t r) {
|
|
689
|
+
const uint32_t* p = b + (2*r - 1) * 16;
|
|
690
|
+
return ((uint64_t)p[0]) | (((uint64_t)p[1]) << 32);
|
|
691
|
+
}
|
|
692
|
+
// scrypt(pw, salt, N, r, p) -> dk[dklen]. Returns 0 on success, -1 on bad
|
|
693
|
+
// params or allocation failure. Caps N*r so a bad config cannot ask for GB.
|
|
694
|
+
static int sb_scrypt(const unsigned char* pw, size_t pwlen, const unsigned char* salt, size_t saltlen,
|
|
695
|
+
uint64_t N, uint32_t r, uint32_t p, unsigned char* dk, size_t dklen) {
|
|
696
|
+
if (r == 0 || p == 0 || N < 2 || (N & (N - 1)) != 0) return -1;
|
|
697
|
+
if ((uint64_t)r * N > (1ULL << 20)) return -1; // <= 128 MiB of V
|
|
698
|
+
size_t blk = 128 * (size_t)r; // one B block, bytes
|
|
699
|
+
unsigned char* b = (unsigned char*)malloc(blk * p);
|
|
700
|
+
uint32_t* v = (uint32_t*)malloc(blk * (size_t)N);
|
|
701
|
+
uint32_t* xy = (uint32_t*)malloc(blk * 2);
|
|
702
|
+
if (!b || !v || !xy) { free(b); free(v); free(xy); return -1; }
|
|
703
|
+
sb_pbkdf2_hmac256(pw, pwlen, salt, saltlen, 1, b, blk * p);
|
|
704
|
+
for (uint32_t i = 0; i < p; i++) {
|
|
705
|
+
uint32_t* x = (uint32_t*)(b + (size_t)i * blk);
|
|
706
|
+
uint32_t* y = xy;
|
|
707
|
+
for (uint64_t j = 0; j < N; j++) {
|
|
708
|
+
memcpy(v + j * (blk / 4), x, blk);
|
|
709
|
+
sb_blockmix(x, y, r);
|
|
710
|
+
memcpy(x, y, blk);
|
|
711
|
+
}
|
|
712
|
+
for (uint64_t j = 0; j < N; j++) {
|
|
713
|
+
uint64_t k = sb_integerify(x, r) & (N - 1);
|
|
714
|
+
const uint32_t* vk = v + k * (blk / 4);
|
|
715
|
+
for (size_t w = 0; w < blk / 4; w++) x[w] ^= vk[w];
|
|
716
|
+
sb_blockmix(x, y, r);
|
|
717
|
+
memcpy(x, y, blk);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
sb_pbkdf2_hmac256(pw, pwlen, b, blk * p, 1, dk, dklen);
|
|
721
|
+
free(b); free(v); free(xy);
|
|
722
|
+
return 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
307
725
|
// Fill buf with n bytes from the OS CSPRNG. /dev/urandom is present on Linux and
|
|
308
726
|
// macOS and inside the bubblewrap sandbox; blocking is not a concern after the
|
|
309
727
|
// pool is seeded. Returns 0, or -1 if the source could not be read in full.
|
|
@@ -354,6 +772,101 @@ function __sbRandomBytes(nStr) {
|
|
|
354
772
|
return out;
|
|
355
773
|
}
|
|
356
774
|
|
|
775
|
+
// #133 — one-shot SHA-2 digest. `algoStr` is "256"|"384"|"512"; `data` is a
|
|
776
|
+
// latin1 string (one char per byte). Returns the raw digest as a bytestring.
|
|
777
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
778
|
+
function __sbDigestRaw(algoStr, data) {
|
|
779
|
+
let out = "";
|
|
780
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
781
|
+
Porffor.c`
|
|
782
|
+
const char* __as; size_t __asl; char* __aso = 0;
|
|
783
|
+
porf_native_fetch_read_value(algoStr, &__as, &__asl, &__aso);
|
|
784
|
+
char __ab[8]; size_t __ak = __asl < 7 ? __asl : 7;
|
|
785
|
+
memcpy(__ab, __as, __ak); __ab[__ak] = 0;
|
|
786
|
+
if (__aso) free(__aso);
|
|
787
|
+
int __algo = atoi(__ab);
|
|
788
|
+
|
|
789
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
790
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
791
|
+
unsigned char __out[64];
|
|
792
|
+
size_t __n = sb_digest(__algo, (const unsigned char*)__d, __dl, __out);
|
|
793
|
+
if (__do) free(__do);
|
|
794
|
+
if (__n) out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__out, __n), 195);
|
|
795
|
+
`;
|
|
796
|
+
return out;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// #133 — HMAC-SHA-2. `algoStr` "256"|"384"|"512"; `key` and `data` latin1.
|
|
800
|
+
// Returns the raw MAC as a bytestring.
|
|
801
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
802
|
+
function __sbHmacRaw(algoStr, key, data) {
|
|
803
|
+
let out = "";
|
|
804
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
805
|
+
Porffor.c`
|
|
806
|
+
const char* __as; size_t __asl; char* __aso = 0;
|
|
807
|
+
porf_native_fetch_read_value(algoStr, &__as, &__asl, &__aso);
|
|
808
|
+
char __ab[8]; size_t __ak = __asl < 7 ? __asl : 7;
|
|
809
|
+
memcpy(__ab, __as, __ak); __ab[__ak] = 0;
|
|
810
|
+
if (__aso) free(__aso);
|
|
811
|
+
int __algo = atoi(__ab);
|
|
812
|
+
|
|
813
|
+
const char* __k; size_t __kl; char* __ko = 0;
|
|
814
|
+
porf_native_fetch_read_value(key, &__k, &__kl, &__ko);
|
|
815
|
+
unsigned char* __kc = (unsigned char*)malloc(__kl ? __kl : 1);
|
|
816
|
+
if (__kc) memcpy(__kc, __k, __kl);
|
|
817
|
+
if (__ko) free(__ko);
|
|
818
|
+
|
|
819
|
+
const char* __d; size_t __dl; char* __do = 0;
|
|
820
|
+
porf_native_fetch_read_value(data, &__d, &__dl, &__do);
|
|
821
|
+
unsigned char __out[64];
|
|
822
|
+
size_t __n = __kc ? sb_hmac(__algo, __kc, __kl, (const unsigned char*)__d, __dl, __out) : 0;
|
|
823
|
+
if (__do) free(__do);
|
|
824
|
+
if (__kc) free(__kc);
|
|
825
|
+
if (__n) out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__out, __n), 195);
|
|
826
|
+
`;
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// #153 — scrypt derivation. All params are decimal strings. Returns the derived
|
|
831
|
+
// key as a bytestring, or '' on bad params / allocation failure.
|
|
832
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block, not by JS.
|
|
833
|
+
function __sbScryptRaw(pw, salt, nStr, rStr, pStr, dkLenStr) {
|
|
834
|
+
let out = "";
|
|
835
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
836
|
+
Porffor.c`
|
|
837
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
838
|
+
porf_native_fetch_read_value(pw, &__p, &__pl, &__po);
|
|
839
|
+
unsigned char* __pc = (unsigned char*)malloc(__pl ? __pl : 1);
|
|
840
|
+
if (__pc) memcpy(__pc, __p, __pl);
|
|
841
|
+
if (__po) free(__po);
|
|
842
|
+
|
|
843
|
+
const char* __s; size_t __sl; char* __so = 0;
|
|
844
|
+
porf_native_fetch_read_value(salt, &__s, &__sl, &__so);
|
|
845
|
+
unsigned char* __sc = (unsigned char*)malloc(__sl ? __sl : 1);
|
|
846
|
+
if (__sc) memcpy(__sc, __s, __sl);
|
|
847
|
+
if (__so) free(__so);
|
|
848
|
+
|
|
849
|
+
long __N = 0, __r = 0, __pp = 0, __dk = 0;
|
|
850
|
+
{ const char* q; size_t ql; char* qo = 0; char nb[24];
|
|
851
|
+
porf_native_fetch_read_value(nStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __N = atol(nb);
|
|
852
|
+
porf_native_fetch_read_value(rStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __r = atol(nb);
|
|
853
|
+
porf_native_fetch_read_value(pStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __pp = atol(nb);
|
|
854
|
+
porf_native_fetch_read_value(dkLenStr, &q, &ql, &qo); { size_t k = ql < 23 ? ql : 23; memcpy(nb, q, k); nb[k] = 0; } if (qo) free(qo); __dk = atol(nb);
|
|
855
|
+
}
|
|
856
|
+
if (__pc && __sc && __dk > 0 && __dk <= 1024) {
|
|
857
|
+
unsigned char* __d = (unsigned char*)malloc((size_t)__dk);
|
|
858
|
+
if (__d) {
|
|
859
|
+
if (sb_scrypt(__pc, __pl, __sc, __sl, (uint64_t)__N, (uint32_t)__r, (uint32_t)__pp, __d, (size_t)__dk) == 0)
|
|
860
|
+
out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__d, (size_t)__dk), 195);
|
|
861
|
+
free(__d);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (__pc) free(__pc);
|
|
865
|
+
if (__sc) free(__sc);
|
|
866
|
+
`;
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
|
|
357
870
|
// #63 — every request carries the protocol version it was built against and an
|
|
358
871
|
// id unique to this process. The id is what makes a resend safe: the transport
|
|
359
872
|
// retries the *same bytes*, so a broker that already applied the request can
|
|
@@ -593,6 +1106,23 @@ globalThis.__sbInstallBindings = function (target, bindings) {
|
|
|
593
1106
|
target[b.binding] = __sbMakeDONamespace(b.binding, b.className);
|
|
594
1107
|
}
|
|
595
1108
|
|
|
1109
|
+
// #69 — env.<NAME>.limit({ key }) -> { success, resetAt }. Fixed window: at
|
|
1110
|
+
// most `limit` calls per `period` seconds for a given key. limit/period
|
|
1111
|
+
// travel in the message so the op stays stateless (the transport has no
|
|
1112
|
+
// config). `resetAt` is epoch ms when the window rolls and the count clears,
|
|
1113
|
+
// so a 429 can send `Retry-After: ceil((resetAt - Date.now()) / 1000)`.
|
|
1114
|
+
// Returns sync like the other shims (CF's is a Promise; the runtime is sync).
|
|
1115
|
+
for (let i = 0; i < (bindings.ratelimiters || []).length; i++) {
|
|
1116
|
+
const rl = bindings.ratelimiters[i];
|
|
1117
|
+
target[rl.binding] = {
|
|
1118
|
+
limit(options) {
|
|
1119
|
+
const key = options && options.key != null ? String(options.key) : "";
|
|
1120
|
+
const r = __sbRpc("ratelimit.check", { name: rl.binding, key, limit: rl.limit, period: rl.period });
|
|
1121
|
+
return { success: !!r.success, resetAt: Number(r.resetAt) || 0 };
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
596
1126
|
// Static assets: env.<ASSETS>.fetch(request) -> broker `assets.get`. The edge
|
|
597
1127
|
// already serves matching files directly; the sprout only calls this for paths
|
|
598
1128
|
// it wants to own (SPA fallback, auth-gated files). The transport keeps the
|
|
@@ -96,8 +96,13 @@ static int sb_db_for(const char* path) {
|
|
|
96
96
|
// copy it saves once the file holds a few large objects. The page cache is
|
|
97
97
|
// capped instead, and journal_size_limit stops the WAL staying huge after one
|
|
98
98
|
// big write.
|
|
99
|
+
// busy_timeout: within one binary every binding op is serial, so there is no
|
|
100
|
+
// self-contention. It matters when the same data dir is served by several
|
|
101
|
+
// copies of the binary behind SO_REUSEPORT (#154 scaling recipe): WAL takes
|
|
102
|
+
// concurrent readers plus one writer across processes, and this makes a
|
|
103
|
+
// writer wait for the lock instead of failing SQLITE_BUSY.
|
|
99
104
|
sqlite3_exec(db,
|
|
100
|
-
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
|
|
105
|
+
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;"
|
|
101
106
|
"PRAGMA cache_size=-2000; PRAGMA journal_size_limit=16777216;",
|
|
102
107
|
0, 0, 0);
|
|
103
108
|
sb_dbs[sb_db_count] = db;
|
|
@@ -1179,6 +1184,10 @@ function __sbEnsureSchema() {
|
|
|
1179
1184
|
s,
|
|
1180
1185
|
"CREATE TABLE IF NOT EXISTS ae (dataset TEXT NOT NULL, ts INTEGER NOT NULL, indexes_json TEXT NOT NULL, blobs_json TEXT NOT NULL, doubles_json TEXT NOT NULL)",
|
|
1181
1186
|
);
|
|
1187
|
+
__sbSql(
|
|
1188
|
+
s,
|
|
1189
|
+
"CREATE TABLE IF NOT EXISTS ratelimit (name TEXT NOT NULL, key TEXT NOT NULL, window_start INTEGER NOT NULL, count INTEGER NOT NULL, PRIMARY KEY (name, key))",
|
|
1190
|
+
);
|
|
1182
1191
|
}
|
|
1183
1192
|
|
|
1184
1193
|
function __sbHex(n) {
|
|
@@ -1513,6 +1522,26 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
1513
1522
|
return { ok: true, count: total.rows.length ? Number(total.rows[0][0]) : 0, rows };
|
|
1514
1523
|
}
|
|
1515
1524
|
|
|
1525
|
+
if (op === "ratelimit.check") {
|
|
1526
|
+
// #69 — fixed window. limit/period come in the message (the transport holds
|
|
1527
|
+
// no config). One upsert: same window -> count+1, new window -> reset to 1.
|
|
1528
|
+
// ponytail: fixed window, so a burst straddling the boundary can briefly
|
|
1529
|
+
// reach ~2x limit. Swap for a two-window weighted count if that matters.
|
|
1530
|
+
const limit = Math.max(Number(msg.limit) || 1, 1);
|
|
1531
|
+
const period = Math.max(Number(msg.period) || 60, 1);
|
|
1532
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1533
|
+
const windowStart = now - (now % period);
|
|
1534
|
+
const r = __sbSql(
|
|
1535
|
+
store,
|
|
1536
|
+
"INSERT INTO ratelimit (name, key, window_start, count) VALUES (?1, ?2, ?3, 1) " +
|
|
1537
|
+
"ON CONFLICT (name, key) DO UPDATE SET count = CASE WHEN window_start = ?3 THEN count + 1 ELSE 1 END, " +
|
|
1538
|
+
"window_start = ?3 RETURNING count",
|
|
1539
|
+
[String(msg.name), String(msg.key == null ? "" : msg.key), windowStart],
|
|
1540
|
+
);
|
|
1541
|
+
const count = r.rows.length ? Number(r.rows[0][0]) : 1;
|
|
1542
|
+
return { ok: true, success: count <= limit, resetAt: (windowStart + period) * 1000 };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1516
1545
|
throw new Error("unknown op: " + op);
|
|
1517
1546
|
}
|
|
1518
1547
|
|
package/src/wrap.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type Bindings = {
|
|
|
65
65
|
* it resolves to is a runtime input, not part of the artifact. */
|
|
66
66
|
services: Array<{ binding: string; service: string }>;
|
|
67
67
|
crons: string[];
|
|
68
|
+
/** #69 — rate limiters: `env.<binding>.limit({ key }) -> { success }`, a
|
|
69
|
+
* fixed window of `limit` calls per `period` seconds, per key. */
|
|
70
|
+
ratelimiters: Array<{ binding: string; limit: number; period: number }>;
|
|
68
71
|
/** Static-asset binding name for `env.<NAME>.fetch(request)`; `""` when assets are edge-only. */
|
|
69
72
|
assets: string;
|
|
70
73
|
};
|
|
@@ -80,6 +83,7 @@ export const EMPTY_BINDINGS: Bindings = {
|
|
|
80
83
|
do: [],
|
|
81
84
|
services: [],
|
|
82
85
|
crons: [],
|
|
86
|
+
ratelimiters: [],
|
|
83
87
|
assets: "",
|
|
84
88
|
};
|
|
85
89
|
|
|
@@ -94,6 +98,7 @@ function hasBindings(b: Bindings): boolean {
|
|
|
94
98
|
b.analytics.length > 0 ||
|
|
95
99
|
b.do.length > 0 ||
|
|
96
100
|
b.services.length > 0 ||
|
|
101
|
+
b.ratelimiters.length > 0 ||
|
|
97
102
|
b.assets !== ""
|
|
98
103
|
);
|
|
99
104
|
}
|
|
@@ -251,6 +256,15 @@ export function readBindingsFromEnv(): Bindings {
|
|
|
251
256
|
}
|
|
252
257
|
}
|
|
253
258
|
}
|
|
259
|
+
const isCount = (v: VarsJson): v is number => Number(v) === v && Number.isInteger(v) && v >= 1;
|
|
260
|
+
const ratelimiters: Array<{ binding: string; limit: number; period: number }> = [];
|
|
261
|
+
if (Array.isArray(parsed.ratelimiters)) {
|
|
262
|
+
for (const entry of parsed.ratelimiters) {
|
|
263
|
+
if (isVarsObject(entry) && isVarsString(entry.binding) && isCount(entry.limit) && isCount(entry.period)) {
|
|
264
|
+
ratelimiters.push({ binding: entry.binding, limit: entry.limit, period: entry.period });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
254
268
|
return {
|
|
255
269
|
kv: strings(parsed.kv),
|
|
256
270
|
secrets: strings(parsed.secrets),
|
|
@@ -262,6 +276,7 @@ export function readBindingsFromEnv(): Bindings {
|
|
|
262
276
|
do: dos,
|
|
263
277
|
services,
|
|
264
278
|
crons: strings(parsed.crons),
|
|
279
|
+
ratelimiters,
|
|
265
280
|
assets: isVarsString(parsed.assets) ? parsed.assets : "",
|
|
266
281
|
};
|
|
267
282
|
}
|