toiljs 0.0.53 → 0.0.54
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
CHANGED
|
@@ -7,24 +7,6 @@ import { Method, Request, Response, Rest, ToilHandler } from 'toiljs/server/runt
|
|
|
7
7
|
*/
|
|
8
8
|
export class AppHandler extends ToilHandler {
|
|
9
9
|
public handle(req: Request): Response {
|
|
10
|
-
// Session signing secret: set on EVERY request, BEFORE routing. The signed
|
|
11
|
-
// session cookie is minted in one route (auth login / session dev-login) but
|
|
12
|
-
// verified by the `@auth` gate in another, and each request runs in a FRESH
|
|
13
|
-
// wasm instance -- so a secret configured inside a single handler is absent on
|
|
14
|
-
// the instance that verifies `/session/me`, and the HMAC check fails (401).
|
|
15
|
-
// Setting it here, at the one entry point every request passes through, makes
|
|
16
|
-
// mint and verify always agree. Read from the env store with a clearly-insecure
|
|
17
|
-
// DEV fallback so the demo runs with zero config; set AUTH_SESSION_SECRET in
|
|
18
|
-
// `.env.secrets` for any non-throwaway use.
|
|
19
|
-
const sessionSecret = Environment.getSecure('AUTH_SESSION_SECRET');
|
|
20
|
-
AuthService.setSecret(
|
|
21
|
-
Uint8Array.wrap(
|
|
22
|
-
String.UTF8.encode(
|
|
23
|
-
sessionSecret != null ? sessionSecret : 'toil-demo-insecure-session-secret-change-me',
|
|
24
|
-
),
|
|
25
|
-
),
|
|
26
|
-
);
|
|
27
|
-
|
|
28
10
|
// Rest.dispatch returns the first matching route's Response, or null if nothing
|
|
29
11
|
// matched - then we fall through to our own logic. REST composes; it never takes
|
|
30
12
|
// over handle().
|
|
@@ -36,38 +36,10 @@ const DEMO_PAR: u32 = 1;
|
|
|
36
36
|
const CHALLENGE_TTL_SECS: u64 = 120;
|
|
37
37
|
const SESSION_TTL_SECS: u64 = 3600;
|
|
38
38
|
|
|
39
|
-
// DEV fallback for the server ML-KEM-768 secret (decapsulation) key, hex, used
|
|
40
|
-
// only when `AUTH_KEM_SK` is not set in `.env.secrets`. Matches the PINNED public
|
|
41
|
-
// key in `src/client/auth.ts`. A real deployment sets AUTH_KEM_SK (and pins its
|
|
42
|
-
// own public key in the client) and rotates it; never ship a real one here.
|
|
43
|
-
const DEMO_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
|
|
44
|
-
|
|
45
|
-
|
|
46
39
|
function utf8(s: string): Uint8Array {
|
|
47
40
|
return Uint8Array.wrap(String.UTF8.encode(s));
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
/** A secret from the env store (`Environment.getSecure`, backed by `.env.secrets`),
|
|
51
|
-
* falling back to a DEV default so the example runs with zero config. Set the
|
|
52
|
-
* real value in `.env.secrets` for any non-throwaway use. */
|
|
53
|
-
function envSecretOr(key: string, devDefault: string): string {
|
|
54
|
-
const v = Environment.getSecure(key);
|
|
55
|
-
return v != null ? v : devDefault;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function fromHex(s: string): Uint8Array {
|
|
59
|
-
const out = new Uint8Array(s.length >> 1);
|
|
60
|
-
for (let i = 0; i < out.length; i++) {
|
|
61
|
-
out[i] = <u8>(hexNibble(s.charCodeAt(i * 2)) * 16 + hexNibble(s.charCodeAt(i * 2 + 1)));
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
function hexNibble(c: i32): i32 {
|
|
66
|
-
if (c >= 48 && c <= 57) return c - 48; // 0-9
|
|
67
|
-
if (c >= 97 && c <= 102) return c - 87; // a-f
|
|
68
|
-
if (c >= 65 && c <= 70) return c - 55; // A-F
|
|
69
|
-
return 0;
|
|
70
|
-
}
|
|
71
43
|
function toHex(b: Uint8Array): string {
|
|
72
44
|
let s = '';
|
|
73
45
|
for (let i = 0; i < b.length; i++) {
|
|
@@ -105,34 +77,6 @@ function deriveSalt(username: string): Uint8Array {
|
|
|
105
77
|
}
|
|
106
78
|
|
|
107
79
|
|
|
108
|
-
let __configured = false;
|
|
109
|
-
/** Lazily configure the auth-route crypto material (OPRF seed + server ML-KEM
|
|
110
|
-
* key) on first use; only the register/login handlers below read these.
|
|
111
|
-
*
|
|
112
|
-
* The session HMAC secret is deliberately NOT set here. The session is verified
|
|
113
|
-
* by the `@auth` gate in `routes/Session` (a DIFFERENT route -> a different fresh
|
|
114
|
-
* wasm instance per request), so it must be configured for EVERY request, not
|
|
115
|
-
* just auth ones. That happens once, before routing, in `core/AppHandler`. */
|
|
116
|
-
function ensureConfigured(): void {
|
|
117
|
-
if (__configured) return;
|
|
118
|
-
// Both secrets come from the env store (`.env.secrets`), with DEV fallbacks
|
|
119
|
-
// so the example runs with zero config. Set the real values in `.env.secrets`
|
|
120
|
-
// (see `.env.secrets.example`) for any non-throwaway use.
|
|
121
|
-
|
|
122
|
-
// OPRF master seed: hashed to a 32-byte (RFC 9497 Ns) seed so any env value
|
|
123
|
-
// works. Per-user OPRF keys derive from this + the username.
|
|
124
|
-
AuthService.setOprfSeed(crypto.sha256Text(envSecretOr('AUTH_OPRF_SEED', 'toil-demo-oprf-seed-v1')));
|
|
125
|
-
// Server static ML-KEM secret key (must match the client's pinned public key).
|
|
126
|
-
const sk = fromHex(envSecretOr('AUTH_KEM_SK', DEMO_KEM_SK_HEX));
|
|
127
|
-
AuthService.setServerKemSecretKey(sk);
|
|
128
|
-
// The ML-KEM-768 public key (ek) is embedded in the decapsulation key at
|
|
129
|
-
// bytes [1152, 2336) (FIPS 203 dk layout); use it for the key id the login
|
|
130
|
-
// message binds. Identical to the public key the client pins.
|
|
131
|
-
AuthService.setServerKemPublicKey(sk.slice(1152, 2336));
|
|
132
|
-
__configured = true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
80
|
// @ts-ignore: decorator
|
|
137
81
|
@external('env', 'kv.put')
|
|
138
82
|
declare function __kvPut(keyPtr: usize, keyLen: i32, valPtr: usize, valLen: i32): void;
|
|
@@ -240,7 +184,6 @@ class Auth {
|
|
|
240
184
|
* No taken-oracle: always succeeds; register/finish rejects a duplicate. */
|
|
241
185
|
@post('/register/start')
|
|
242
186
|
public registerStart(ctx: RouteContext): Response {
|
|
243
|
-
ensureConfigured();
|
|
244
187
|
const r = new DataReader(ctx.request.body);
|
|
245
188
|
const username = r.readString();
|
|
246
189
|
const blinded = r.readBytes();
|
|
@@ -263,7 +206,6 @@ class Auth {
|
|
|
263
206
|
* proof-of-possession before storing the key. */
|
|
264
207
|
@post('/register/finish')
|
|
265
208
|
public registerFinish(ctx: RouteContext): Response {
|
|
266
|
-
ensureConfigured();
|
|
267
209
|
const r = new DataReader(ctx.request.body);
|
|
268
210
|
const username = r.readString();
|
|
269
211
|
const pk = r.readBytes();
|
|
@@ -300,7 +242,6 @@ class Auth {
|
|
|
300
242
|
* and a fresh challenge -- a known and an unknown user are indistinguishable. */
|
|
301
243
|
@post('/login/start')
|
|
302
244
|
public loginStart(ctx: RouteContext): Response {
|
|
303
|
-
ensureConfigured();
|
|
304
245
|
const r = new DataReader(ctx.request.body);
|
|
305
246
|
const username = r.readString();
|
|
306
247
|
const blinded = r.readBytes();
|
|
@@ -344,7 +285,6 @@ class Auth {
|
|
|
344
285
|
* resp: u8(status) [+ bytes(sessionToken) bytes(serverConfirm)] + Set-Cookie */
|
|
345
286
|
@post('/login/finish')
|
|
346
287
|
public loginFinish(ctx: RouteContext): Response {
|
|
347
|
-
ensureConfigured();
|
|
348
288
|
const r = new DataReader(ctx.request.body);
|
|
349
289
|
const cid = r.readBytes();
|
|
350
290
|
const ct = r.readBytes();
|
|
@@ -16,11 +16,11 @@ import { DataReader, DataWriter } from 'data';
|
|
|
16
16
|
* this `/session/dev-login` mints one for a caller-named demo user so the flow is
|
|
17
17
|
* runnable without the external account store the login example stubs out.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* No secret wiring is needed: `AuthService` reads `AUTH_SESSION_SECRET` from the
|
|
20
|
+
* env store automatically (with a clearly-insecure DEV fallback), so the cookie
|
|
21
|
+
* minted on login and re-verified here by the `@auth` gate always agree, even
|
|
22
|
+
* though each request runs in its own fresh wasm instance. A real deployment just
|
|
23
|
+
* sets `AUTH_SESSION_SECRET` in `.env.secrets`.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
// @user: the authenticated-user shape. Exactly one per program.
|
package/package.json
CHANGED
package/server/globals/auth.ts
CHANGED
|
@@ -70,39 +70,90 @@ declare function __toilVoprfEvaluate(
|
|
|
70
70
|
outPtr: usize,
|
|
71
71
|
): i32;
|
|
72
72
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
@@ -165,9 +216,10 @@ export namespace AuthService {
|
|
|
165
216
|
export const DEFAULT_SESSION_TTL_SECS: u64 = 86400; // 24h
|
|
166
217
|
|
|
167
218
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
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
|
-
*
|
|
397
|
-
* bytes).
|
|
398
|
-
*
|
|
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
|
-
*
|
|
406
|
-
* compute {@link serverKemKeyId}.
|
|
407
|
-
* `ek` embedded
|
|
408
|
-
*
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
505
|
+
sk.dataStart,
|
|
506
|
+
sk.length,
|
|
451
507
|
out.dataStart,
|
|
452
508
|
);
|
|
453
509
|
return rc == 0 ? out : new Uint8Array(0);
|
|
@@ -462,7 +518,7 @@ export namespace AuthService {
|
|
|
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(
|
|
521
|
+
return sha256(__resolveServerKemPk());
|
|
466
522
|
}
|
|
467
523
|
|
|
468
524
|
/** Domain separators for the session-key derivation and confirmation tag. */
|