toiljs 0.0.51 → 0.0.53

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/TYPESCRIPT_LAW.md +12601 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +16 -1
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/auth.d.ts +9 -20
  7. package/build/client/auth.js +112 -95
  8. package/build/client/index.d.ts +2 -2
  9. package/build/client/index.js +1 -1
  10. package/build/compiler/.tsbuildinfo +1 -1
  11. package/build/compiler/generate.js +1 -1
  12. package/build/devserver/.tsbuildinfo +1 -1
  13. package/build/devserver/crypto.js +33 -0
  14. package/build/devserver/host.js +2 -0
  15. package/build/devserver/kv.d.ts +3 -0
  16. package/build/devserver/kv.js +53 -0
  17. package/build/devserver/module.js +2 -1
  18. package/docs/auth-todo.md +149 -0
  19. package/docs/auth.md +234 -173
  20. package/examples/basic/client/routes/pq.tsx +72 -103
  21. package/examples/basic/server/core/AppHandler.ts +24 -3
  22. package/examples/basic/server/main.ts +0 -1
  23. package/examples/basic/server/routes/Auth.ts +304 -99
  24. package/examples/basic/server/routes/Session.ts +5 -2
  25. package/package.json +2 -1
  26. package/server/globals/auth.ts +263 -10
  27. package/src/cli/diagnostics.ts +22 -0
  28. package/src/cli/doctor.ts +2 -0
  29. package/src/client/auth.ts +192 -174
  30. package/src/client/index.ts +2 -2
  31. package/src/compiler/generate.ts +1 -1
  32. package/src/devserver/crypto.ts +54 -0
  33. package/src/devserver/host.ts +6 -0
  34. package/src/devserver/kv.ts +93 -0
  35. package/src/devserver/module.ts +4 -1
  36. package/test/devserver-pqauth.test.ts +153 -0
  37. package/test/doctor.test.ts +22 -0
  38. package/test/pqauth-e2e.test.ts +207 -0
  39. package/examples/basic/server/routes/PqDemo.ts +0 -127
@@ -1,4 +1,11 @@
1
1
  export declare const LOGIN_CONTEXT = "qauth:login:v1";
2
+ export declare const REGISTER_CONTEXT = "qauth:register:v1";
3
+ export declare const SESSION_KEY_LABEL = "toil-session-key-v1";
4
+ export declare const SERVER_CONFIRM_LABEL = "toil-server-confirm-v1";
5
+ export declare const KEM_PUBLIC_KEY_LEN = 1184;
6
+ export declare const KEM_CIPHERTEXT_LEN = 1088;
7
+ export declare const SHARED_SECRET_LEN = 32;
8
+ export declare const SERVER_KEM_PUBLIC_KEY: Uint8Array<ArrayBufferLike>;
2
9
  export declare const PUBLIC_KEY_LEN = 1312;
3
10
  export declare const SECRET_KEY_LEN = 2560;
4
11
  export declare const SIGNATURE_LEN = 2420;
@@ -9,34 +16,16 @@ export interface KdfParams {
9
16
  readonly parallelism: number;
10
17
  readonly salt: Uint8Array;
11
18
  }
12
- export interface Challenge {
13
- readonly cid: Uint8Array;
14
- readonly aud: string;
15
- readonly kdf: KdfParams;
16
- readonly nonce: Uint8Array;
17
- readonly iat: bigint;
18
- readonly exp: bigint;
19
- }
20
- export declare function buildLoginMessage(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: bigint, exp: bigint): Uint8Array;
19
+ export declare function buildLoginMessage(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: bigint, exp: bigint, ciphertext: Uint8Array, memKiB: number, iterations: number, parallelism: number, serverKemKeyId: Uint8Array): Uint8Array;
20
+ export declare function buildRegisterMessage(username: string, publicKey: Uint8Array): Uint8Array;
21
21
  export interface AuthOptions {
22
22
  readonly baseUrl?: string;
23
23
  }
24
24
  export declare function register(username: string, password: string, opts?: AuthOptions): Promise<void>;
25
25
  export declare function login(username: string, password: string, opts?: AuthOptions): Promise<Uint8Array>;
26
- export interface IdentityProof {
27
- readonly envelope: Uint8Array;
28
- readonly publicKeyHex: string;
29
- readonly nonceHex: string;
30
- readonly signatureLen: number;
31
- readonly deriveMs: number;
32
- }
33
- export declare function proveIdentity(username: string, password: string, opts?: {
34
- baseUrl?: string;
35
- }): Promise<IdentityProof>;
36
26
  export declare const Auth: {
37
27
  readonly register: typeof register;
38
28
  readonly login: typeof login;
39
- readonly proveIdentity: typeof proveIdentity;
40
29
  readonly buildLoginMessage: typeof buildLoginMessage;
41
30
  readonly LOGIN_CONTEXT: "qauth:login:v1";
42
31
  };
@@ -1,7 +1,22 @@
1
- import { argon2id, sha256 } from 'hash-wasm';
1
+ import { argon2id, createSHA256, createHMAC } from 'hash-wasm';
2
2
  import { ml_dsa44 } from '@dacely/noble-post-quantum/ml-dsa.js';
3
+ import { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem.js';
4
+ import { ristretto255_oprf } from '@noble/curves/ed25519.js';
3
5
  import { DataReader, DataWriter } from 'toiljs/io';
4
6
  export const LOGIN_CONTEXT = 'qauth:login:v1';
7
+ export const REGISTER_CONTEXT = 'qauth:register:v1';
8
+ export const SESSION_KEY_LABEL = 'toil-session-key-v1';
9
+ export const SERVER_CONFIRM_LABEL = 'toil-server-confirm-v1';
10
+ export const KEM_PUBLIC_KEY_LEN = 1184;
11
+ export const KEM_CIPHERTEXT_LEN = 1088;
12
+ export const SHARED_SECRET_LEN = 32;
13
+ function fromHex(hex) {
14
+ const out = new Uint8Array(hex.length / 2);
15
+ for (let i = 0; i < out.length; i++)
16
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
17
+ return out;
18
+ }
19
+ export const SERVER_KEM_PUBLIC_KEY = fromHex('29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39');
5
20
  export const PUBLIC_KEY_LEN = 1312;
6
21
  export const SECRET_KEY_LEN = 2560;
7
22
  export const SIGNATURE_LEN = 2420;
@@ -10,9 +25,9 @@ function wipe(buf) {
10
25
  crypto.getRandomValues(buf);
11
26
  buf.fill(0);
12
27
  }
13
- async function deriveSeed(password, kdf) {
28
+ async function deriveSeed(oprfOutput, kdf) {
14
29
  return argon2id({
15
- password: new TextEncoder().encode(password.normalize('NFKC')),
30
+ password: oprfOutput,
16
31
  salt: kdf.salt,
17
32
  iterations: kdf.iterations,
18
33
  parallelism: kdf.parallelism,
@@ -21,7 +36,40 @@ async function deriveSeed(password, kdf) {
21
36
  outputType: 'binary',
22
37
  });
23
38
  }
24
- export function buildLoginMessage(sub, aud, cid, nonce, iat, exp) {
39
+ const utf8 = (s) => new TextEncoder().encode(s);
40
+ async function sha256Bytes(data) {
41
+ const h = await createSHA256();
42
+ h.init();
43
+ h.update(data);
44
+ return h.digest('binary');
45
+ }
46
+ async function hmacSha256(key, msg) {
47
+ const h = await createHMAC(createSHA256(), key);
48
+ h.init();
49
+ h.update(msg);
50
+ return h.digest('binary');
51
+ }
52
+ function concatBytes(...parts) {
53
+ let n = 0;
54
+ for (const p of parts)
55
+ n += p.length;
56
+ const out = new Uint8Array(n);
57
+ let off = 0;
58
+ for (const p of parts) {
59
+ out.set(p, off);
60
+ off += p.length;
61
+ }
62
+ return out;
63
+ }
64
+ function bytesEqual(a, b) {
65
+ if (a.length !== b.length)
66
+ return false;
67
+ let diff = 0;
68
+ for (let i = 0; i < a.length; i++)
69
+ diff |= a[i] ^ b[i];
70
+ return diff === 0;
71
+ }
72
+ export function buildLoginMessage(sub, aud, cid, nonce, iat, exp, ciphertext, memKiB, iterations, parallelism, serverKemKeyId) {
25
73
  return new DataWriter()
26
74
  .writeU8(1)
27
75
  .writeString(sub)
@@ -30,8 +78,16 @@ export function buildLoginMessage(sub, aud, cid, nonce, iat, exp) {
30
78
  .writeBytes(nonce)
31
79
  .writeU64(iat)
32
80
  .writeU64(exp)
81
+ .writeBytes(ciphertext)
82
+ .writeU32(memKiB)
83
+ .writeU32(iterations)
84
+ .writeU32(parallelism)
85
+ .writeBytes(serverKemKeyId)
33
86
  .toBytes();
34
87
  }
88
+ export function buildRegisterMessage(username, publicKey) {
89
+ return new DataWriter().writeU8(1).writeString(username).writeBytes(publicKey).toBytes();
90
+ }
35
91
  function decodeKdf(r) {
36
92
  return {
37
93
  memKiB: r.readU32(),
@@ -40,15 +96,6 @@ function decodeKdf(r) {
40
96
  salt: r.readBytes(),
41
97
  };
42
98
  }
43
- function decodeChallenge(r) {
44
- const cid = r.readBytes();
45
- const aud = r.readString();
46
- const kdf = decodeKdf(r);
47
- const nonce = r.readBytes();
48
- const iat = r.readU64();
49
- const exp = r.readU64();
50
- return { cid, aud, kdf, nonce, iat, exp };
51
- }
52
99
  async function postBinary(baseUrl, path, body) {
53
100
  const res = await fetch(baseUrl + path, {
54
101
  method: 'POST',
@@ -62,39 +109,68 @@ async function postBinary(baseUrl, path, body) {
62
109
  }
63
110
  export async function register(username, password, opts = {}) {
64
111
  const baseUrl = opts.baseUrl ?? '/auth';
65
- const start = await postBinary(baseUrl, '/register/start', new DataWriter().writeString(username).toBytes());
112
+ const oprf = ristretto255_oprf.oprf;
113
+ const pw = utf8(password.normalize('NFKC'));
114
+ const { blind, blinded } = oprf.blind(pw);
115
+ const start = await postBinary(baseUrl, '/register/start', new DataWriter().writeString(username).writeBytes(blinded).toBytes());
66
116
  const status = start.readU8();
67
117
  if (status !== 0)
68
118
  throw new Error('auth: registration unavailable');
69
119
  const kdf = decodeKdf(start);
70
- const seed = await deriveSeed(password, kdf);
120
+ const evaluated = start.readBytes();
121
+ const oprfOutput = oprf.finalize(pw, blind, evaluated);
122
+ const seed = await deriveSeed(oprfOutput, kdf);
71
123
  let publicKey;
124
+ let regProof;
72
125
  try {
73
126
  const kp = ml_dsa44.keygen(seed);
74
127
  publicKey = kp.publicKey;
75
- wipe(kp.secretKey);
128
+ try {
129
+ regProof = ml_dsa44.sign(buildRegisterMessage(username, publicKey), kp.secretKey, {
130
+ context: utf8(REGISTER_CONTEXT),
131
+ });
132
+ }
133
+ finally {
134
+ wipe(kp.secretKey);
135
+ }
76
136
  }
77
137
  finally {
78
138
  wipe(seed);
79
139
  }
80
140
  if (publicKey.length !== PUBLIC_KEY_LEN)
81
141
  throw new Error('auth: bad public key length');
82
- const finish = await postBinary(baseUrl, '/register/finish', new DataWriter().writeString(username).writeBytes(publicKey).toBytes());
83
- if (finish.readU8() !== 0)
142
+ const finish = await postBinary(baseUrl, '/register/finish', new DataWriter().writeString(username).writeBytes(publicKey).writeBytes(regProof).toBytes());
143
+ const finishStatus = finish.readU8();
144
+ if (finishStatus === 1)
145
+ throw new Error('auth: username already registered (log in instead)');
146
+ if (finishStatus !== 0)
84
147
  throw new Error('auth: registration rejected');
85
148
  }
86
149
  export async function login(username, password, opts = {}) {
87
150
  const baseUrl = opts.baseUrl ?? '/auth';
88
- const ch = decodeChallenge(await postBinary(baseUrl, '/login/start', new DataWriter().writeString(username).toBytes()));
89
- if (BigInt(Math.floor(Date.now() / 1000)) >= ch.exp)
151
+ const oprf = ristretto255_oprf.oprf;
152
+ const pw = utf8(password.normalize('NFKC'));
153
+ const { blind, blinded } = oprf.blind(pw);
154
+ const r = await postBinary(baseUrl, '/login/start', new DataWriter().writeString(username).writeBytes(blinded).toBytes());
155
+ const cid = r.readBytes();
156
+ const aud = r.readString();
157
+ const kdf = decodeKdf(r);
158
+ const nonce = r.readBytes();
159
+ const iat = r.readU64();
160
+ const exp = r.readU64();
161
+ const evaluated = r.readBytes();
162
+ if (BigInt(Math.floor(Date.now() / 1000)) >= exp)
90
163
  throw new Error('auth: challenge expired');
91
- const message = buildLoginMessage(username, ch.aud, ch.cid, ch.nonce, ch.iat, ch.exp);
92
- const seed = await deriveSeed(password, ch.kdf);
164
+ const oprfOutput = oprf.finalize(pw, blind, evaluated);
165
+ const seed = await deriveSeed(oprfOutput, kdf);
166
+ const { cipherText, sharedSecret } = ml_kem768.encapsulate(SERVER_KEM_PUBLIC_KEY);
167
+ const serverKemKeyId = await sha256Bytes(SERVER_KEM_PUBLIC_KEY);
168
+ const message = buildLoginMessage(username, aud, cid, nonce, iat, exp, cipherText, kdf.memKiB, kdf.iterations, kdf.parallelism, serverKemKeyId);
93
169
  let signature;
94
170
  try {
95
171
  const kp = ml_dsa44.keygen(seed);
96
172
  try {
97
- signature = ml_dsa44.sign(message, kp.secretKey, { context: new TextEncoder().encode(LOGIN_CONTEXT) });
173
+ signature = ml_dsa44.sign(message, kp.secretKey, { context: utf8(LOGIN_CONTEXT) });
98
174
  }
99
175
  finally {
100
176
  wipe(kp.secretKey);
@@ -105,78 +181,19 @@ export async function login(username, password, opts = {}) {
105
181
  }
106
182
  if (signature.length !== SIGNATURE_LEN)
107
183
  throw new Error('auth: bad signature length');
108
- const res = await postBinary(baseUrl, '/login/finish', new DataWriter().writeBytes(ch.cid).writeBytes(signature).toBytes());
109
- if (res.readU8() !== 0)
184
+ const res = await postBinary(baseUrl, '/login/finish', new DataWriter().writeBytes(cid).writeBytes(cipherText).writeBytes(signature).toBytes());
185
+ if (res.readU8() !== 0) {
186
+ wipe(sharedSecret);
110
187
  throw new Error('auth: login failed');
111
- return res.readBytes();
112
- }
113
- function toHex(bytes) {
114
- let s = '';
115
- for (const b of bytes)
116
- s += b.toString(16).padStart(2, '0');
117
- return s;
118
- }
119
- async function demoSalt(username) {
120
- const hex = await sha256('pq-demo|' + username);
121
- const out = new Uint8Array(16);
122
- for (let i = 0; i < 16; i++)
123
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
124
- return out;
125
- }
126
- export async function proveIdentity(username, password, opts = {}) {
127
- const baseUrl = opts.baseUrl ?? '/pq';
128
- const cres = await fetch(baseUrl + '/challenge', { credentials: 'same-origin' });
129
- if (!cres.ok)
130
- throw new Error('pq: challenge request failed');
131
- const cr = new DataReader(new Uint8Array(await cres.arrayBuffer()));
132
- const aud = cr.readString();
133
- const cid = cr.readBytes();
134
- const nonce = cr.readBytes();
135
- const iat = cr.readU64();
136
- const exp = cr.readU64();
137
- const token = cr.readString();
138
- const salt = await demoSalt(username);
139
- const t0 = Date.now();
140
- const seed = await argon2id({
141
- password: new TextEncoder().encode(password.normalize('NFKC')),
142
- salt,
143
- iterations: 2,
144
- parallelism: 1,
145
- memorySize: 16 * 1024,
146
- hashLength: SEED_LEN,
147
- outputType: 'binary',
148
- });
149
- const deriveMs = Date.now() - t0;
150
- const message = buildLoginMessage(username, aud, cid, nonce, iat, exp);
151
- let publicKey;
152
- let signature;
153
- try {
154
- const kp = ml_dsa44.keygen(seed);
155
- publicKey = kp.publicKey;
156
- try {
157
- signature = ml_dsa44.sign(message, kp.secretKey, {
158
- context: new TextEncoder().encode(LOGIN_CONTEXT),
159
- });
160
- }
161
- finally {
162
- wipe(kp.secretKey);
163
- }
164
188
  }
165
- finally {
166
- wipe(seed);
167
- }
168
- const envelope = new DataWriter()
169
- .writeString(username)
170
- .writeString(token)
171
- .writeBytes(publicKey)
172
- .writeBytes(signature)
173
- .toBytes();
174
- return {
175
- envelope,
176
- publicKeyHex: toHex(publicKey.slice(0, 16)),
177
- nonceHex: toHex(nonce.slice(0, 16)),
178
- signatureLen: signature.length,
179
- deriveMs,
180
- };
189
+ const session = res.readBytes();
190
+ const serverConfirm = res.readBytes();
191
+ const transcriptHash = await sha256Bytes(message);
192
+ const sessionKey = await hmacSha256(sharedSecret, concatBytes(utf8(SESSION_KEY_LABEL), transcriptHash));
193
+ wipe(sharedSecret);
194
+ const expected = await hmacSha256(sessionKey, concatBytes(utf8(SERVER_CONFIRM_LABEL), transcriptHash));
195
+ if (!bytesEqual(expected, serverConfirm))
196
+ throw new Error('auth: server authentication failed');
197
+ return session;
181
198
  }
182
- export const Auth = { register, login, proveIdentity, buildLoginMessage, LOGIN_CONTEXT };
199
+ export const Auth = { register, login, buildLoginMessage, LOGIN_CONTEXT };
@@ -1,7 +1,7 @@
1
1
  export { mount } from './routing/mount.js';
2
2
  export { Router } from './routing/Router.js';
3
- export { Auth, register as authRegister, login as authLogin, proveIdentity, buildLoginMessage, LOGIN_CONTEXT } from './auth.js';
4
- export type { KdfParams, Challenge, AuthOptions, IdentityProof } from './auth.js';
3
+ export { Auth, register as authRegister, login as authLogin, buildLoginMessage, LOGIN_CONTEXT } from './auth.js';
4
+ export type { KdfParams, AuthOptions } from './auth.js';
5
5
  export { Link } from './navigation/Link.js';
6
6
  export type { LinkProps } from './navigation/Link.js';
7
7
  export { NavLink, matchActive } from './navigation/NavLink.js';
@@ -1,6 +1,6 @@
1
1
  export { mount } from './routing/mount.js';
2
2
  export { Router } from './routing/Router.js';
3
- export { Auth, register as authRegister, login as authLogin, proveIdentity, buildLoginMessage, LOGIN_CONTEXT } from './auth.js';
3
+ export { Auth, register as authRegister, login as authLogin, buildLoginMessage, LOGIN_CONTEXT } from './auth.js';
4
4
  export { Link } from './navigation/Link.js';
5
5
  export { NavLink, matchActive } from './navigation/NavLink.js';
6
6
  export { navigate, back, forward, refresh, setViewTransitions, setTransitions, href, } from './navigation/navigation.js';