toiljs 0.0.50 → 0.0.52
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 +18 -0
- package/TYPESCRIPT_LAW.md +12601 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +16 -1
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +8 -8
- package/build/client/auth.js +105 -24
- package/build/client/index.d.ts +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/generate.js +1 -1
- package/build/compiler/index.js +7 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/crypto.js +33 -0
- package/build/devserver/host.js +2 -0
- package/build/devserver/kv.d.ts +3 -0
- package/build/devserver/kv.js +53 -0
- package/build/devserver/module.js +2 -1
- package/examples/basic/client/routes/pq.tsx +67 -104
- package/examples/basic/server/routes/Auth.ts +274 -100
- package/package.json +2 -1
- package/server/globals/auth.ts +215 -0
- package/src/cli/diagnostics.ts +22 -0
- package/src/cli/doctor.ts +2 -0
- package/src/client/auth.ts +179 -51
- package/src/client/index.ts +1 -1
- package/src/compiler/generate.ts +1 -1
- package/src/compiler/index.ts +14 -2
- package/src/devserver/crypto.ts +54 -0
- package/src/devserver/host.ts +6 -0
- package/src/devserver/kv.ts +96 -0
- package/src/devserver/module.ts +4 -1
- package/test/devserver-pqauth.test.ts +153 -0
- package/test/doctor.test.ts +22 -0
- package/test/pqauth-e2e.test.ts +162 -0
|
@@ -1,68 +1,72 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
2
2
|
import { DataReader, DataWriter } from 'data';
|
|
3
3
|
|
|
4
|
+
import { encodeSessionUser } from './Session';
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* OPAQUE-style post-quantum auth, end-to-end and runnable under `toiljs dev`.
|
|
8
|
+
*
|
|
9
|
+
* The password never leaves the browser. The client blinds it through the
|
|
10
|
+
* server-keyed OPRF (precomputation-resistant keyed salt), stretches the OPRF
|
|
11
|
+
* output with Argon2id into an ML-DSA-44 keypair, and registers only the public
|
|
12
|
+
* key (+ a proof-of-possession). Login is a challenge-response that also runs an
|
|
13
|
+
* ML-KEM-768 key encapsulation: the server proves its identity by returning a
|
|
14
|
+
* confirmation tag only derivable from the decapsulated shared secret (mutual
|
|
15
|
+
* auth). See `server/globals/auth.ts` (the `AuthService` global) and the client
|
|
16
|
+
* half in `toiljs/client` (`Auth.register` / `Auth.login`).
|
|
9
17
|
*
|
|
10
|
-
* STORAGE
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* all instances (Redis `GETDEL`, or SQL `DELETE ... RETURNING`) -- never a
|
|
16
|
-
* read-then-delete, or a sniffed signature replays across a race. The stubs
|
|
17
|
-
* below throw to make that explicit; swap them for your store + a host KV/db
|
|
18
|
-
* binding. The crypto and encoding (`AuthService`) are production-ready; the
|
|
19
|
-
* orchestration here is a template.
|
|
18
|
+
* STORAGE: backed by the DEV-ONLY `kv.*` host imports (see
|
|
19
|
+
* `src/devserver/kv.ts`) so the register -> login chain spans requests under
|
|
20
|
+
* `toiljs dev`. ============ REMOVE KV LATER ============ this is a stand-in;
|
|
21
|
+
* once toildb is implemented, the Accounts/Challenges stores move onto it and
|
|
22
|
+
* this goes away. `kv.*` is not on the production edge.
|
|
20
23
|
*
|
|
21
24
|
* Wire: every body/response is binary (`DataWriter`/`DataReader`), never JSON.
|
|
22
|
-
* The client half lives in `toiljs/client` (`Auth.register` / `Auth.login`).
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
27
|
const AUD = 'toil-demo'; // this service's audience id (server config; never client-echoed)
|
|
26
|
-
const MIN_MEM_KIB = 256 * 1024; // 256 MiB floor (KDF-params-as-credential)
|
|
27
|
-
const MIN_ITERATIONS = 3;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
parallelism: u32 = 0;
|
|
36
|
-
}
|
|
29
|
+
// Demo-light Argon2id params (responsive in a browser tab). A real deployment
|
|
30
|
+
// uses >= 256 MiB / >= 3 iterations. The client derives against whatever it is
|
|
31
|
+
// handed, so this is the single source of truth.
|
|
32
|
+
const DEMO_MEM_KIB: u32 = 32768; // 32 MiB
|
|
33
|
+
const DEMO_ITERS: u32 = 2;
|
|
34
|
+
const DEMO_PAR: u32 = 1;
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
const CHALLENGE_TTL_SECS: u64 = 120;
|
|
37
|
+
const SESSION_TTL_SECS: u64 = 3600;
|
|
38
|
+
|
|
39
|
+
// DEV server ML-KEM-768 secret (decapsulation) key, hex. Matches the PINNED
|
|
40
|
+
// public key in `src/client/auth.ts`. DEV ONLY: a real deployment loads this
|
|
41
|
+
// from the secure env store and rotates it; never commit a real one.
|
|
42
|
+
const SERVER_KEM_SK_HEX: string = '3156a8eb11c62bdb4af9fc57bef470f880ae340373bcc61662748a9742a639b9ad6bc55a77a82e0caa99ede237b4783ce70ab08ecc5802a9478c4ca3de67acd7a2147db43fdba408e9765443f37e9e90cc09f836d53879b890126bd6c33d55a6d97636a28ba10e18ac919aa9d37c2e4d07b6c930a5cb3238c8338fbb1abe7dac124c93462ebc5ae81cb132947993a74f9602610eab68b7fc9407b58e958aca054443246240c484c650962408168632c303cfc738d3b918ee04a37c2436b6f7300b8c6e7bd528bc5c229673c3a1bc4ae4265772f654ed8377b285626c67a4ef715a5a04a56804c3fae93ca5e3219cd68649622ee0d77bcb664a68e377260a3a38c2739b81c3c9ec510b66acde5041f3b52922a17019dc9afaec71c3e3c3102686ceb019da138b22463ad7f452640526d1d8b21c9111ca844149d1391c937b84287f1a228342c06ccb87c31cb14227e175007c5c4497c11e8647377234a84ab2640aa8ee7acb54954f99155cf7d768446b104ac149f59ca1d0029401570db9341c93db0041d52fbbd62726a75f9ab177e4ea5176e675d28a1f9852c28b38074c91cec8064b6ba116db8b59c0434fbd1b207cd921fbf29b06740b53c7304b17b253652ad469b2cb10bf7ed3bcc5b1b6168c2d30a889f67a01ae79455100ac582ba2f764a4a4b134b9115d7c548032d55d4916ce25c0ce7c42160e446298fb10f747302e781a70b2b7962b0b54f3c0e3a4677e99cc02e41e66b0861d02d072b94ce3f8a04fd20d2ec220cea3737922808f00080186421e60b7d1076e5ca40099d54da33033021349e31bb65e12aa259b37bc975582aa6441ab2fabdc9cee0aab0c11c7e3489b93bab26e13bf399ab8a37949baba3c2f8a94fd97a9a551c96d582b5c1ba97b4547701656ee02567dd6a8362c1043c5874760c7d1133292f05c9d3689beccb903d4bd65f09e3e3255d0229daf9050ebaa107e51371fc9248393239575466a9c45b4a239e1b29b07d9701cf1bb488a95a004a98fcb1f6d548cc8554a3eb25a5fc90892618e5d33b04938567e748ab9ba79b0d39d611864b2140666c1791e79c5c0943a03038f7306551db3b271b08dec32443ae14674e16d6c42956ef36499348e7424bbc4883c37675a4f8bb28cd68f30b532ba80104e7214b9a4886045a152d161821a006ae03ae3742e36f63d997c858b850119e1004f4022a04a9533749d993641763a83dce5256f3826ae9b0584c72d69c77d6784444737a0192789e0d63a2f2808ce88b07c33383e588f68b13b892ac6998c9f2db14ba3e10eee4b9717761efc298e026974231a143b89009a724a7121292bb9292662b87502beadb9cbea3cc89de1997b376575f466b6693e18eb70630ba1823cae5f03698ae662190207156ca8d1a4a3cb926d20c92b524180c0804f057491c292024641bf9b21b52214bf2a2b42d16596e22935317bc712e64f64c143b257ca6f663223a1a2b6537b55746a2a739b2adbbfa004354a1555cc8b8215aa06413b27b7fa8c860386c13876b8d55b743860a13c0005dc4ac5e003cd3431c7a29edcc73c50b991e56a12423ac1f2842ed2999b7b31b6e01aaa83c01af658bae959b2cb256f1e7bba29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39d157cc40cbafccc4429c35caeda482299013baf565d0f38b8f2886b9641ae6bea5b2bfccd9e6f3000d1a2734414e5b6875828f9ca9b6c3d0ddeaf704111e2b38';
|
|
43
|
+
|
|
44
|
+
// --- small helpers ----------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function utf8(s: string): Uint8Array {
|
|
47
|
+
return Uint8Array.wrap(String.UTF8.encode(s));
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
export function exists(_username: string): bool {
|
|
52
|
-
throw new Error('wire Accounts to your store');
|
|
53
|
-
}
|
|
54
|
-
export function put(_a: AccountRecord): void {
|
|
55
|
-
throw new Error('wire Accounts to your store');
|
|
50
|
+
function fromHex(s: string): Uint8Array {
|
|
51
|
+
const out = new Uint8Array(s.length >> 1);
|
|
52
|
+
for (let i = 0; i < out.length; i++) {
|
|
53
|
+
out[i] = <u8>(hexNibble(s.charCodeAt(i * 2)) * 16 + hexNibble(s.charCodeAt(i * 2 + 1)));
|
|
56
54
|
}
|
|
55
|
+
return out;
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
function hexNibble(c: i32): i32 {
|
|
58
|
+
if (c >= 48 && c <= 57) return c - 48; // 0-9
|
|
59
|
+
if (c >= 97 && c <= 102) return c - 87; // a-f
|
|
60
|
+
if (c >= 65 && c <= 70) return c - 55; // A-F
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
function toHex(b: Uint8Array): string {
|
|
64
|
+
let s = '';
|
|
65
|
+
for (let i = 0; i < b.length; i++) {
|
|
66
|
+
const v = b[i];
|
|
67
|
+
s += (v < 16 ? '0' : '') + (<u32>v).toString(16);
|
|
65
68
|
}
|
|
69
|
+
return s;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
function randomBytes(n: i32): Uint8Array {
|
|
@@ -71,114 +75,284 @@ function randomBytes(n: i32): Uint8Array {
|
|
|
71
75
|
return b;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function nowSecs(): u64 {
|
|
79
|
+
return <u64>(Date.now() / 1000);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One generic error on every failure path (anti-enumeration, anti-oracle). */
|
|
74
83
|
function fail(): Response {
|
|
75
|
-
// One generic error on every failure path (anti-enumeration, anti-oracle).
|
|
76
84
|
return Response.text('auth: request failed\n', 401);
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Deterministic per-user Argon2id salt (16 bytes). With the OPRF providing
|
|
89
|
+
* precomputation resistance, a public/deterministic salt is fine: it only needs
|
|
90
|
+
* to be unique per user (the OPRF output already differs per user). Making it
|
|
91
|
+
* deterministic means register and login agree with NO stored salt, and an
|
|
92
|
+
* unknown user yields the SAME stable salt as a known one would -- no
|
|
93
|
+
* enumeration oracle.
|
|
94
|
+
*/
|
|
95
|
+
function deriveSalt(username: string): Uint8Array {
|
|
96
|
+
return crypto.sha256Text('toil-demo-salt-v1:' + username).slice(0, 16);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- startup config (idempotent; re-applied per fresh instance) -------------
|
|
100
|
+
|
|
101
|
+
let __configured = false;
|
|
102
|
+
function ensureConfigured(): void {
|
|
103
|
+
if (__configured) return;
|
|
104
|
+
// OPRF master seed: 32 bytes from a fixed label (a real deployment uses a
|
|
105
|
+
// secret from the env store). Per-user OPRF keys derive from this + username.
|
|
106
|
+
AuthService.setOprfSeed(crypto.sha256Text('toil-demo-oprf-seed-v1'));
|
|
107
|
+
// Server static ML-KEM secret key (matches the client's pinned public key).
|
|
108
|
+
AuthService.setServerKemSecretKey(fromHex(SERVER_KEM_SK_HEX));
|
|
109
|
+
__configured = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- KV-backed storage (DEV-ONLY host imports; see src/devserver/kv.ts) ------
|
|
113
|
+
|
|
114
|
+
// @ts-ignore: decorator
|
|
115
|
+
@external('env', 'kv.put')
|
|
116
|
+
declare function __kvPut(keyPtr: usize, keyLen: i32, valPtr: usize, valLen: i32): void;
|
|
117
|
+
// @ts-ignore: decorator
|
|
118
|
+
@external('env', 'kv.get')
|
|
119
|
+
declare function __kvGet(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
|
|
120
|
+
// @ts-ignore: decorator
|
|
121
|
+
@external('env', 'kv.getdel')
|
|
122
|
+
declare function __kvGetDel(keyPtr: usize, keyLen: i32, outPtr: usize, outCap: i32): i32;
|
|
123
|
+
|
|
124
|
+
const KV_CAP: i32 = 8192; // bounds account (~1.5 KiB) + challenge (~100 B) records
|
|
125
|
+
|
|
126
|
+
function kvPut(key: Uint8Array, val: Uint8Array): void {
|
|
127
|
+
__kvPut(key.dataStart, key.length, val.dataStart, val.length);
|
|
128
|
+
}
|
|
129
|
+
function kvGet(key: Uint8Array): Uint8Array | null {
|
|
130
|
+
const out = new Uint8Array(KV_CAP);
|
|
131
|
+
const n = __kvGet(key.dataStart, key.length, out.dataStart, KV_CAP);
|
|
132
|
+
if (n < 0) return null;
|
|
133
|
+
return out.slice(0, n);
|
|
134
|
+
}
|
|
135
|
+
/** Atomic fetch-and-delete: consumes a login challenge exactly once. */
|
|
136
|
+
function kvGetDel(key: Uint8Array): Uint8Array | null {
|
|
137
|
+
const out = new Uint8Array(KV_CAP);
|
|
138
|
+
const n = __kvGetDel(key.dataStart, key.length, out.dataStart, KV_CAP);
|
|
139
|
+
if (n < 0) return null;
|
|
140
|
+
return out.slice(0, n);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function acctKey(username: string): Uint8Array {
|
|
144
|
+
return utf8('acct:' + username);
|
|
145
|
+
}
|
|
146
|
+
function chalKey(cid: Uint8Array): Uint8Array {
|
|
147
|
+
return utf8('chal:' + toHex(cid));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- records ----------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
class Account {
|
|
153
|
+
username: string = '';
|
|
154
|
+
salt: Uint8Array = new Uint8Array(0);
|
|
155
|
+
publicKey: Uint8Array = new Uint8Array(0);
|
|
156
|
+
memKiB: u32 = 0;
|
|
157
|
+
iterations: u32 = 0;
|
|
158
|
+
parallelism: u32 = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function putAccount(a: Account): void {
|
|
162
|
+
const w = new DataWriter();
|
|
163
|
+
w.writeString(a.username);
|
|
164
|
+
w.writeBytes(a.salt);
|
|
165
|
+
w.writeBytes(a.publicKey);
|
|
166
|
+
w.writeU32(a.memKiB);
|
|
167
|
+
w.writeU32(a.iterations);
|
|
168
|
+
w.writeU32(a.parallelism);
|
|
169
|
+
kvPut(acctKey(a.username), w.toBytes());
|
|
170
|
+
}
|
|
171
|
+
function getAccount(username: string): Account | null {
|
|
172
|
+
const raw = kvGet(acctKey(username));
|
|
173
|
+
if (raw == null) return null;
|
|
174
|
+
const r = new DataReader(raw);
|
|
175
|
+
const a = new Account();
|
|
176
|
+
a.username = r.readString();
|
|
177
|
+
a.salt = r.readBytes();
|
|
178
|
+
a.publicKey = r.readBytes();
|
|
179
|
+
a.memKiB = r.readU32();
|
|
180
|
+
a.iterations = r.readU32();
|
|
181
|
+
a.parallelism = r.readU32();
|
|
182
|
+
return r.ok ? a : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class Challenge {
|
|
186
|
+
cid: Uint8Array = new Uint8Array(0);
|
|
187
|
+
username: string = '';
|
|
188
|
+
nonce: Uint8Array = new Uint8Array(0);
|
|
189
|
+
iat: u64 = 0;
|
|
190
|
+
exp: u64 = 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function putChallenge(c: Challenge): void {
|
|
194
|
+
const w = new DataWriter();
|
|
195
|
+
w.writeBytes(c.cid);
|
|
196
|
+
w.writeString(c.username);
|
|
197
|
+
w.writeBytes(c.nonce);
|
|
198
|
+
w.writeU64(c.iat);
|
|
199
|
+
w.writeU64(c.exp);
|
|
200
|
+
kvPut(chalKey(c.cid), w.toBytes());
|
|
201
|
+
}
|
|
202
|
+
function consumeChallenge(cid: Uint8Array): Challenge | null {
|
|
203
|
+
const raw = kvGetDel(chalKey(cid));
|
|
204
|
+
if (raw == null) return null;
|
|
205
|
+
const r = new DataReader(raw);
|
|
206
|
+
const c = new Challenge();
|
|
207
|
+
c.cid = r.readBytes();
|
|
208
|
+
c.username = r.readString();
|
|
209
|
+
c.nonce = r.readBytes();
|
|
210
|
+
c.iat = r.readU64();
|
|
211
|
+
c.exp = r.readU64();
|
|
212
|
+
return r.ok ? c : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
79
215
|
@rest('auth')
|
|
80
216
|
class Auth {
|
|
81
|
-
/** POST /auth/register/start body: str(username)
|
|
82
|
-
* resp: u8(status=0)
|
|
217
|
+
/** POST /auth/register/start body: str(username) bytes(blinded)
|
|
218
|
+
* resp: u8(status=0) u32(mem) u32(iters) u32(par) bytes(salt) bytes(evaluated)
|
|
219
|
+
* No taken-oracle: always succeeds; register/finish rejects a duplicate. */
|
|
83
220
|
@post('/register/start')
|
|
84
221
|
public registerStart(ctx: RouteContext): Response {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
222
|
+
ensureConfigured();
|
|
223
|
+
const r = new DataReader(ctx.request.body);
|
|
224
|
+
const username = r.readString();
|
|
225
|
+
const blinded = r.readBytes();
|
|
226
|
+
if (!r.ok) return fail();
|
|
227
|
+
const evaluated = AuthService.oprfEvaluate(username, blinded);
|
|
228
|
+
if (evaluated.length != AuthService.OPRF_ELEMENT_LEN) return fail();
|
|
229
|
+
|
|
90
230
|
const w = new DataWriter();
|
|
91
231
|
w.writeU8(0);
|
|
92
|
-
w.writeU32(
|
|
93
|
-
w.writeU32(
|
|
94
|
-
w.writeU32(
|
|
95
|
-
w.writeBytes(
|
|
96
|
-
|
|
97
|
-
// registerFinish stores the same one; omitted here (no store).
|
|
232
|
+
w.writeU32(DEMO_MEM_KIB);
|
|
233
|
+
w.writeU32(DEMO_ITERS);
|
|
234
|
+
w.writeU32(DEMO_PAR);
|
|
235
|
+
w.writeBytes(deriveSalt(username));
|
|
236
|
+
w.writeBytes(evaluated);
|
|
98
237
|
return Response.bytes(w.toBytes());
|
|
99
238
|
}
|
|
100
239
|
|
|
101
|
-
/** POST /auth/register/finish body: str(username) bytes(pk)
|
|
240
|
+
/** POST /auth/register/finish body: str(username) bytes(pk) bytes(regProof)
|
|
241
|
+
* resp: u8(status). Verifies proof-of-possession before storing the key. */
|
|
102
242
|
@post('/register/finish')
|
|
103
243
|
public registerFinish(ctx: RouteContext): Response {
|
|
244
|
+
ensureConfigured();
|
|
104
245
|
const r = new DataReader(ctx.request.body);
|
|
105
246
|
const username = r.readString();
|
|
106
247
|
const pk = r.readBytes();
|
|
107
|
-
|
|
108
|
-
|
|
248
|
+
const proof = r.readBytes();
|
|
249
|
+
if (!r.ok) return fail();
|
|
250
|
+
if (pk.length != AuthService.PUBLIC_KEY_LEN) return fail();
|
|
251
|
+
if (getAccount(username) != null) return fail(); // already registered
|
|
252
|
+
|
|
253
|
+
// Proof-of-possession: the client signed buildRegisterMessage with the
|
|
254
|
+
// matching secret key, so we confirm it actually holds it.
|
|
255
|
+
const regMsg = AuthService.buildRegisterMessage(username, pk);
|
|
256
|
+
if (!AuthService.verifyRegister(pk, regMsg, proof)) return fail();
|
|
257
|
+
|
|
258
|
+
const a = new Account();
|
|
109
259
|
a.username = username;
|
|
260
|
+
a.salt = deriveSalt(username);
|
|
110
261
|
a.publicKey = pk;
|
|
111
|
-
a.memKiB =
|
|
112
|
-
a.iterations =
|
|
113
|
-
a.parallelism =
|
|
114
|
-
|
|
115
|
-
Accounts.put(a);
|
|
262
|
+
a.memKiB = DEMO_MEM_KIB;
|
|
263
|
+
a.iterations = DEMO_ITERS;
|
|
264
|
+
a.parallelism = DEMO_PAR;
|
|
265
|
+
putAccount(a);
|
|
116
266
|
return Response.bytes(new DataWriter().writeU8(0).toBytes());
|
|
117
267
|
}
|
|
118
268
|
|
|
119
|
-
/** POST /auth/login/start body: str(username)
|
|
120
|
-
* resp: bytes(cid) str(aud) u32(mem) u32(iters) u32(par) bytes(salt)
|
|
269
|
+
/** POST /auth/login/start body: str(username) bytes(blinded)
|
|
270
|
+
* resp: bytes(cid) str(aud) u32(mem) u32(iters) u32(par) bytes(salt)
|
|
271
|
+
* bytes(nonce) u64(iat) u64(exp) bytes(evaluated)
|
|
272
|
+
* Anti-enumeration: ALWAYS OPRF-evaluates (real or decoy key from the same
|
|
273
|
+
* seed+username), returns a deterministic per-user salt + constant params,
|
|
274
|
+
* and a fresh challenge -- a known and an unknown user are indistinguishable. */
|
|
121
275
|
@post('/login/start')
|
|
122
276
|
public loginStart(ctx: RouteContext): Response {
|
|
123
|
-
|
|
124
|
-
const
|
|
277
|
+
ensureConfigured();
|
|
278
|
+
const r = new DataReader(ctx.request.body);
|
|
279
|
+
const username = r.readString();
|
|
280
|
+
const blinded = r.readBytes();
|
|
281
|
+
if (!r.ok) return fail();
|
|
282
|
+
const evaluated = AuthService.oprfEvaluate(username, blinded);
|
|
283
|
+
if (evaluated.length != AuthService.OPRF_ELEMENT_LEN) return fail();
|
|
125
284
|
|
|
285
|
+
const acct = getAccount(username);
|
|
126
286
|
const cid = randomBytes(16);
|
|
127
287
|
const nonce = randomBytes(32);
|
|
128
|
-
const iat =
|
|
129
|
-
const exp = iat +
|
|
130
|
-
|
|
131
|
-
// Anti-enumeration: unknown user still gets a fully-formed challenge with
|
|
132
|
-
// a throwaway salt; the eventual verify just fails.
|
|
133
|
-
const salt = acct != null ? acct.salt : randomBytes(16);
|
|
134
|
-
const mem = acct != null ? acct.memKiB : <u32>MIN_MEM_KIB;
|
|
135
|
-
const iters = acct != null ? acct.iterations : <u32>MIN_ITERATIONS;
|
|
136
|
-
const par = acct != null ? acct.parallelism : 1;
|
|
288
|
+
const iat = nowSecs();
|
|
289
|
+
const exp = iat + CHALLENGE_TTL_SECS;
|
|
137
290
|
|
|
291
|
+
// Persist only for a real account; the response is identical either way,
|
|
292
|
+
// and login/finish for an unknown user fails generically at consume.
|
|
138
293
|
if (acct != null) {
|
|
139
|
-
const c = new
|
|
294
|
+
const c = new Challenge();
|
|
140
295
|
c.cid = cid;
|
|
141
296
|
c.username = username;
|
|
142
297
|
c.nonce = nonce;
|
|
143
298
|
c.iat = iat;
|
|
144
299
|
c.exp = exp;
|
|
145
|
-
|
|
300
|
+
putChallenge(c);
|
|
146
301
|
}
|
|
147
302
|
|
|
148
303
|
const w = new DataWriter();
|
|
149
304
|
w.writeBytes(cid);
|
|
150
305
|
w.writeString(AUD);
|
|
151
|
-
w.writeU32(
|
|
152
|
-
w.writeU32(
|
|
153
|
-
w.writeU32(
|
|
154
|
-
w.writeBytes(
|
|
306
|
+
w.writeU32(DEMO_MEM_KIB);
|
|
307
|
+
w.writeU32(DEMO_ITERS);
|
|
308
|
+
w.writeU32(DEMO_PAR);
|
|
309
|
+
w.writeBytes(deriveSalt(username));
|
|
155
310
|
w.writeBytes(nonce);
|
|
156
311
|
w.writeU64(iat);
|
|
157
312
|
w.writeU64(exp);
|
|
313
|
+
w.writeBytes(evaluated);
|
|
158
314
|
return Response.bytes(w.toBytes());
|
|
159
315
|
}
|
|
160
316
|
|
|
161
|
-
/** POST /auth/login/finish body: bytes(cid) bytes(
|
|
317
|
+
/** POST /auth/login/finish body: bytes(cid) bytes(ct) bytes(sig)
|
|
318
|
+
* resp: u8(status) [+ bytes(sessionToken) bytes(serverConfirm)] + Set-Cookie */
|
|
162
319
|
@post('/login/finish')
|
|
163
320
|
public loginFinish(ctx: RouteContext): Response {
|
|
321
|
+
ensureConfigured();
|
|
164
322
|
const r = new DataReader(ctx.request.body);
|
|
165
323
|
const cid = r.readBytes();
|
|
324
|
+
const ct = r.readBytes();
|
|
166
325
|
const sig = r.readBytes();
|
|
326
|
+
if (!r.ok) return fail();
|
|
167
327
|
|
|
168
328
|
// 1. CONSUME FIRST: atomic fetch-and-delete. Unknown/used/expired => fail.
|
|
169
|
-
const ch =
|
|
329
|
+
const ch = consumeChallenge(cid);
|
|
170
330
|
if (ch == null) return fail();
|
|
171
|
-
|
|
172
|
-
if (now >= ch.exp) return fail();
|
|
331
|
+
if (nowSecs() >= ch.exp) return fail();
|
|
173
332
|
|
|
174
|
-
// 2. Rebuild the message from OUR stored values
|
|
175
|
-
// load the account
|
|
176
|
-
const acct =
|
|
333
|
+
// 2. Rebuild the v2 message from OUR stored values + the client's ct,
|
|
334
|
+
// load the account key, verify the login signature.
|
|
335
|
+
const acct = getAccount(ch.username);
|
|
177
336
|
if (acct == null) return fail();
|
|
178
|
-
const message = AuthService.
|
|
337
|
+
const message = AuthService.buildLoginMessageV2(ch.username, AUD, cid, ch.nonce, ch.iat, ch.exp, ct);
|
|
179
338
|
if (!AuthService.verifyLogin(acct.publicKey, message, sig)) return fail();
|
|
180
339
|
|
|
181
|
-
// 3.
|
|
182
|
-
|
|
340
|
+
// 3. Decapsulate the client's ciphertext (proves WE hold the KEM key) and
|
|
341
|
+
// build the mutual-auth confirmation tag the client will verify.
|
|
342
|
+
const sharedSecret = AuthService.mlkemDecapsulate(ct);
|
|
343
|
+
if (sharedSecret.length != AuthService.SHARED_SECRET_LEN) return fail();
|
|
344
|
+
const transcriptHash = AuthService.sha256(message);
|
|
345
|
+
const confirm = AuthService.serverConfirmTag(sharedSecret, transcriptHash);
|
|
346
|
+
|
|
347
|
+
// 4. Success: mint the session and return {0, sessionToken, confirm}.
|
|
348
|
+
const userData = encodeSessionUser(ch.username);
|
|
349
|
+
const w = new DataWriter();
|
|
350
|
+
w.writeU8(0);
|
|
351
|
+
w.writeBytes(userData); // opaque session token (the readable user payload)
|
|
352
|
+
w.writeBytes(confirm);
|
|
353
|
+
const resp = Response.bytes(w.toBytes());
|
|
354
|
+
resp.setCookie(AuthService.mintSession(userData, SESSION_TTL_SECS));
|
|
355
|
+
resp.setCookie(AuthService.userCookie(userData, SESSION_TTL_SECS));
|
|
356
|
+
return resp;
|
|
183
357
|
}
|
|
184
358
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toiljs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.52",
|
|
5
5
|
"author": "Dacely",
|
|
6
6
|
"description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
|
|
7
7
|
"repository": {
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"dependencies": {
|
|
119
119
|
"@dacely/hyper-express": "6.17.4",
|
|
120
120
|
"@dacely/noble-post-quantum": "^0.6.1",
|
|
121
|
+
"@noble/curves": "^2.2.0",
|
|
121
122
|
"@dacely/toilscript-loader": "^0.1.0",
|
|
122
123
|
"@eslint-react/eslint-plugin": "^5.8.8",
|
|
123
124
|
"@eslint/js": "^10.0.1",
|