toiljs 0.0.53 → 0.0.55

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.
@@ -10,7 +10,7 @@
10
10
  // and the toiljs dev-server mock).
11
11
 
12
12
  import { DataWriter, DataReader } from 'data';
13
- import { HmacImportParams, HmacParams, ALG_SHA_256, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
13
+ import { HmacImportParams, HmacParams, ALG_SHA_256, FMT_RAW, USAGE_SIGN, USAGE_VERIFY } from 'crypto';
14
14
 
15
15
  import {
16
16
  Server,
@@ -70,46 +70,97 @@ declare function __toilVoprfEvaluate(
70
70
  outPtr: usize,
71
71
  ): i32;
72
72
 
73
- // HMAC key for signing session cookies. The SAME secret must be configured on
74
- // every edge instance (a sealed cookie minted by one is opened by another) and
75
- // must NEVER reach the client. There is no host-config secret mechanism yet, so
76
- // the tenant supplies one at startup via `AuthService.setSecret(...)` (a
77
- // build-time constant is consistent across instances). The default below is a
78
- // well-known DEV placeholder: a deployment that does not call `setSecret` gets a
79
- // loud, insecure-but-functional session so local dev works out of the box.
80
- // TODO(secret): replace with a per-deployment host-config secret.
81
- let __sessionSecret: Uint8Array = Uint8Array.wrap(
82
- String.UTF8.encode('toil-dev-insecure-session-secret-CHANGE-ME'),
83
- );
84
-
85
- // OPRF master seed (RFC 9497 DeriveKeyPair input). Per-user OPRF keys are
86
- // derived from this + the username, so it is a server secret of the same
87
- // sensitivity as a password-hash pepper: a leak enables an offline dictionary
88
- // attack (but precomputation stays impossible until it leaks). Configured at
89
- // startup via `AuthService.setOprfSeed`; the DEV default below is well-known.
90
- // 32 bytes (RFC 9497 Ns for ristretto255); `setOprfSeed` MUST also pass 32.
91
- let __oprfSeed: Uint8Array = Uint8Array.wrap(
92
- String.UTF8.encode('toil-dev-oprf-seedXXCHANGE-ME-32'),
93
- );
94
-
95
- // Server static ML-KEM-768 secret (decapsulation) key. The matching public key
96
- // is PINNED in the client; only the holder of this key can decapsulate, so a
97
- // correct shared secret authenticates the server. Configured at startup via
98
- // `AuthService.setServerKemSecretKey` (2400 bytes). Empty until set; mutual-auth
99
- // calls fail closed if unset.
100
- let __serverKemSk: Uint8Array = new Uint8Array(0);
101
-
102
- // Server static ML-KEM-768 PUBLIC (encapsulation) key, used only to compute the
103
- // key identity bound into the login transcript (`serverKemKeyId`). The client
104
- // pins the same key. Configured at startup via `setServerKemPublicKey`.
105
- let __serverKemPk: Uint8Array = new Uint8Array(0);
73
+ // Secret configuration is AUTOMATIC. Every secret below resolves on first use
74
+ // from, in order: (1) an explicit `set*()` override, (2) the tenant's env store
75
+ // (`Environment.getSecure`, backed by `.env.secrets` / the dashboard) under the
76
+ // key named below, then (3) a well-known, clearly-insecure DEV fallback so local
77
+ // dev and the examples run with zero config. The resolved value is cached in the
78
+ // module global. Because every request runs in a FRESH wasm instance that reads
79
+ // the SAME env value the SAME way, there is no per-route / per-instance secret to
80
+ // keep in sync by hand: a cookie minted by one route can never fail to verify in
81
+ // another for want of a `setSecret` call. A real deployment sets the env values
82
+ // (and pins its own server KEM public key in the client); the DEV fallbacks are
83
+ // never a place to put a real secret.
84
+
85
+ /** Env-store keys the framework reads automatically (tenant-set secrets). */
86
+ const ENV_SESSION_SECRET: string = 'AUTH_SESSION_SECRET';
87
+ const ENV_OPRF_SEED: string = 'AUTH_OPRF_SEED';
88
+ const ENV_KEM_SK: string = 'AUTH_KEM_SK';
89
+
90
+ /** Well-known DEV fallbacks (insecure; overridden by env or `set*()`). */
91
+ const DEV_SESSION_SECRET: string = 'toil-dev-insecure-session-secret-CHANGE-ME';
92
+ const DEV_OPRF_SEED_SRC: string = 'toil-dev-oprf-seed-v1';
93
+ // A well-known DEV ML-KEM-768 decapsulation key (hex). Its public half (the `ek`
94
+ // at bytes [1152, 2336) of the dk) is PINNED in the client (`src/client/auth.ts`,
95
+ // `SERVER_KEM_PUBLIC_KEY`), so the PQ-auth example runs with zero config. A real
96
+ // deployment sets `AUTH_KEM_SK` and pins its own public key; never treat this as
97
+ // secret. Tree-shaken away unless a server actually calls the ML-KEM path.
98
+ const DEV_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
99
+
100
+ // Resolved-and-cached per instance; `null` = not yet resolved and not overridden.
101
+ let __sessionSecret: Uint8Array | null = null;
102
+ let __oprfSeed: Uint8Array | null = null;
103
+ let __serverKemSk: Uint8Array | null = null;
104
+ let __serverKemPk: Uint8Array | null = null;
105
+
106
+ function __hexNibble(c: i32): i32 {
107
+ if (c >= 48 && c <= 57) return c - 48; // 0-9
108
+ if (c >= 97 && c <= 102) return c - 87; // a-f
109
+ if (c >= 65 && c <= 70) return c - 55; // A-F
110
+ return 0;
111
+ }
112
+ function __fromHex(s: string): Uint8Array {
113
+ const out = new Uint8Array(s.length >> 1);
114
+ for (let i = 0; i < out.length; i++) {
115
+ out[i] = <u8>((__hexNibble(s.charCodeAt(i * 2)) << 4) | __hexNibble(s.charCodeAt(i * 2 + 1)));
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /** The session HMAC secret (UTF-8 of the env value or the DEV fallback). */
121
+ function __resolveSessionSecret(): Uint8Array {
122
+ let s = __sessionSecret;
123
+ if (s != null) return s;
124
+ const v = Environment.getSecure(ENV_SESSION_SECRET);
125
+ s = Uint8Array.wrap(String.UTF8.encode(v != null ? v : DEV_SESSION_SECRET));
126
+ __sessionSecret = s;
127
+ return s;
128
+ }
129
+ /** The OPRF master seed, hashed to 32 bytes (RFC 9497 Ns) so any env value works. */
130
+ function __resolveOprfSeed(): Uint8Array {
131
+ let s = __oprfSeed;
132
+ if (s != null) return s;
133
+ const v = Environment.getSecure(ENV_OPRF_SEED);
134
+ s = crypto.sha256Text(v != null ? v : DEV_OPRF_SEED_SRC);
135
+ __oprfSeed = s;
136
+ return s;
137
+ }
138
+ /** The server static ML-KEM-768 secret (decapsulation) key (2400 bytes). */
139
+ function __resolveServerKemSk(): Uint8Array {
140
+ let s = __serverKemSk;
141
+ if (s != null) return s;
142
+ const v = Environment.getSecure(ENV_KEM_SK);
143
+ s = __fromHex(v != null ? v : DEV_KEM_SK_HEX);
144
+ __serverKemSk = s;
145
+ return s;
146
+ }
147
+ /** The server static ML-KEM-768 PUBLIC key (the `ek` embedded in the dk at bytes
148
+ * [1152, 2336), FIPS 203 layout), used for the key id bound into the login
149
+ * transcript. The client pins the same key. */
150
+ function __resolveServerKemPk(): Uint8Array {
151
+ let s = __serverKemPk;
152
+ if (s != null) return s;
153
+ s = __resolveServerKemSk().slice(1152, 2336);
154
+ __serverKemPk = s;
155
+ return s;
156
+ }
106
157
 
107
158
  // HMAC-SHA256(key, msg) via the ambient Web Crypto (same path SecureCookies
108
159
  // uses). The session-key derivation and the mutual-auth confirmation tag are
109
160
  // both keyed PRFs over the transcript; the client mirrors this with hash-wasm.
110
161
  function __hmacSha256(key: Uint8Array, msg: Uint8Array): Uint8Array {
111
162
  const k = crypto.subtle.importKey(
112
- 'raw',
163
+ FMT_RAW,
113
164
  key,
114
165
  new HmacImportParams(ALG_SHA_256),
115
166
  false,
@@ -165,9 +216,10 @@ export namespace AuthService {
165
216
  export const DEFAULT_SESSION_TTL_SECS: u64 = 86400; // 24h
166
217
 
167
218
  /**
168
- * Configure the server secret used to sign session cookies. Call once at
169
- * startup from the tenant's `main.ts`. Must be identical on every edge
170
- * instance and kept out of any client bundle.
219
+ * Override the session-signing secret programmatically. OPTIONAL: by default
220
+ * AuthService reads `AUTH_SESSION_SECRET` from the env store (with a DEV
221
+ * fallback), so most apps never call this. An override takes precedence over
222
+ * the env value for the current request; keep it out of any client bundle.
171
223
  */
172
224
  export function setSecret(secret: Uint8Array): void {
173
225
  __sessionSecret = secret;
@@ -183,7 +235,7 @@ export namespace AuthService {
183
235
  const req = Server.currentRequest;
184
236
  if (req == null) return null;
185
237
 
186
- const sealed = SecureCookies.signed(__sessionSecret).open(
238
+ const sealed = SecureCookies.signed(__resolveSessionSecret()).open(
187
239
  req.cookies(),
188
240
  sessionCookieName(__reqIsSecure()),
189
241
  );
@@ -246,7 +298,7 @@ export namespace AuthService {
246
298
  .sameSite(SameSite.Lax)
247
299
  .maxAge(<i64>ttlSecs);
248
300
  cookie = secure ? cookie.asHostPrefixed() : cookie.path('/');
249
- return SecureCookies.signed(__sessionSecret).seal(cookie);
301
+ return SecureCookies.signed(__resolveSessionSecret()).seal(cookie);
250
302
  }
251
303
 
252
304
  /** A `Set-Cookie` that immediately clears the session (logout). */
@@ -384,28 +436,30 @@ export namespace AuthService {
384
436
  export const OPRF_SEED_LEN: i32 = 32;
385
437
 
386
438
  /**
387
- * Configure the OPRF master seed (32 bytes). Per-user OPRF keys are derived
388
- * from this + the username. Call once at startup from `main.ts`; identical
389
- * on every instance, kept out of any client bundle.
439
+ * Override the OPRF master seed (32 bytes) programmatically. OPTIONAL: by
440
+ * default AuthService reads `AUTH_OPRF_SEED` from the env store (hashed to 32
441
+ * bytes, with a DEV fallback). Per-user OPRF keys derive from this + the
442
+ * username; keep it out of any client bundle.
390
443
  */
391
444
  export function setOprfSeed(seed: Uint8Array): void {
392
445
  __oprfSeed = seed;
393
446
  }
394
447
 
395
448
  /**
396
- * Configure the server static ML-KEM-768 secret (decapsulation) key (2400
397
- * bytes). The matching public key is pinned in the client. Call once at
398
- * startup; identical on every instance, never in a client bundle.
449
+ * Override the server static ML-KEM-768 secret (decapsulation) key (2400
450
+ * bytes) programmatically. OPTIONAL: by default AuthService reads `AUTH_KEM_SK`
451
+ * (hex) from the env store, with a DEV fallback whose public half is pinned in
452
+ * the client. Never put this in a client bundle.
399
453
  */
400
454
  export function setServerKemSecretKey(secretKey: Uint8Array): void {
401
455
  __serverKemSk = secretKey;
402
456
  }
403
457
 
404
458
  /**
405
- * Configure the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
406
- * compute {@link serverKemKeyId}. Must be the key the client pins. (It is the
407
- * `ek` embedded in the decapsulation key, so a tenant can pass
408
- * `secretKey.slice(1152, 2336)` rather than store it twice.)
459
+ * Override the server static ML-KEM-768 PUBLIC key (1184 bytes), used to
460
+ * compute {@link serverKemKeyId}. OPTIONAL: by default it is derived from the
461
+ * secret key (the `ek` embedded at bytes [1152, 2336) of the dk), so setting
462
+ * the secret key is enough. Must be the key the client pins.
409
463
  */
410
464
  export function setServerKemPublicKey(publicKey: Uint8Array): void {
411
465
  __serverKemPk = publicKey;
@@ -421,9 +475,10 @@ export namespace AuthService {
421
475
  if (blinded.length != OPRF_ELEMENT_LEN) return new Uint8Array(0);
422
476
  const info = Uint8Array.wrap(String.UTF8.encode(username));
423
477
  const out = new Uint8Array(OPRF_ELEMENT_LEN);
478
+ const seed = __resolveOprfSeed();
424
479
  const rc = __toilVoprfEvaluate(
425
- __oprfSeed.dataStart,
426
- __oprfSeed.length,
480
+ seed.dataStart,
481
+ seed.length,
427
482
  info.dataStart,
428
483
  info.length,
429
484
  blinded.dataStart,
@@ -439,15 +494,16 @@ export namespace AuthService {
439
494
  * Only the genuine server can produce this, so it underpins mutual auth.
440
495
  */
441
496
  export function mlkemDecapsulate(ciphertext: Uint8Array): Uint8Array {
442
- if (__serverKemSk.length != KEM_SECRET_KEY_LEN || ciphertext.length != KEM_CIPHERTEXT_LEN) {
497
+ const sk = __resolveServerKemSk();
498
+ if (sk.length != KEM_SECRET_KEY_LEN || ciphertext.length != KEM_CIPHERTEXT_LEN) {
443
499
  return new Uint8Array(0);
444
500
  }
445
501
  const out = new Uint8Array(SHARED_SECRET_LEN);
446
502
  const rc = __toilMlkemDecapsulate(
447
503
  ciphertext.dataStart,
448
504
  ciphertext.length,
449
- __serverKemSk.dataStart,
450
- __serverKemSk.length,
505
+ sk.dataStart,
506
+ sk.length,
451
507
  out.dataStart,
452
508
  );
453
509
  return rc == 0 ? out : new Uint8Array(0);
@@ -455,14 +511,14 @@ export namespace AuthService {
455
511
 
456
512
  /** SHA-256 over `data` (ambient Web Crypto), for transcript/confirm hashing. */
457
513
  export function sha256(data: Uint8Array): Uint8Array {
458
- return crypto.subtle.digest('SHA-256', data);
514
+ return crypto.subtle.digest(ALG_SHA_256, data);
459
515
  }
460
516
 
461
517
  /** `SHA-256(serverKemPublicKey)` -- the key identity bound into the login
462
518
  * message, so the signature commits to which server key the client
463
519
  * encapsulated to. The client computes the same hash over its pinned key. */
464
520
  export function serverKemKeyId(): Uint8Array {
465
- return sha256(__serverKemPk);
521
+ return sha256(__resolveServerKemPk());
466
522
  }
467
523
 
468
524
  /** Domain separators for the session-key derivation and confirmation tag. */
@@ -24,6 +24,7 @@ import {
24
24
  HmacImportParams,
25
25
  HmacParams,
26
26
  ALG_SHA_256,
27
+ FMT_RAW,
27
28
  USAGE_SIGN,
28
29
  USAGE_VERIFY,
29
30
  } from 'crypto';
@@ -45,7 +46,7 @@ const TWOFA_VERSION: u8 = 1;
45
46
 
46
47
  function importHmac(key: Uint8Array): CryptoKey {
47
48
  return crypto.subtle.importKey(
48
- 'raw',
49
+ FMT_RAW,
49
50
  key,
50
51
  new HmacImportParams(ALG_SHA_256),
51
52
  false,
@@ -30,6 +30,7 @@ import {
30
30
  HmacParams,
31
31
  ALG_AES_GCM,
32
32
  ALG_SHA_256,
33
+ FMT_RAW,
33
34
  USAGE_SIGN,
34
35
  USAGE_VERIFY,
35
36
  USAGE_ENCRYPT,
@@ -104,7 +105,7 @@ export class SecureCookies {
104
105
 
105
106
  private importHmac(key: Uint8Array): CryptoKey {
106
107
  return crypto.subtle.importKey(
107
- 'raw',
108
+ FMT_RAW,
108
109
  key,
109
110
  new HmacImportParams(ALG_SHA_256),
110
111
  false,
@@ -114,7 +115,7 @@ export class SecureCookies {
114
115
 
115
116
  private importAes(key: Uint8Array): CryptoKey {
116
117
  return crypto.subtle.importKey(
117
- 'raw',
118
+ FMT_RAW,
118
119
  key,
119
120
  new AesKeyParams(),
120
121
  false,