toiljs 0.0.51 → 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 +9 -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/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/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
package/build/client/auth.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export declare const LOGIN_CONTEXT = "qauth:login:v1";
|
|
2
|
+
export declare const REGISTER_CONTEXT = "qauth:register:v1";
|
|
3
|
+
export declare const SERVER_CONFIRM_LABEL = "toil-server-confirm-v1";
|
|
4
|
+
export declare const KEM_PUBLIC_KEY_LEN = 1184;
|
|
5
|
+
export declare const KEM_CIPHERTEXT_LEN = 1088;
|
|
6
|
+
export declare const SHARED_SECRET_LEN = 32;
|
|
7
|
+
export declare const SERVER_KEM_PUBLIC_KEY: Uint8Array<ArrayBufferLike>;
|
|
2
8
|
export declare const PUBLIC_KEY_LEN = 1312;
|
|
3
9
|
export declare const SECRET_KEY_LEN = 2560;
|
|
4
10
|
export declare const SIGNATURE_LEN = 2420;
|
|
@@ -9,15 +15,9 @@ export interface KdfParams {
|
|
|
9
15
|
readonly parallelism: number;
|
|
10
16
|
readonly salt: Uint8Array;
|
|
11
17
|
}
|
|
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
18
|
export declare function buildLoginMessage(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: bigint, exp: bigint): Uint8Array;
|
|
19
|
+
export declare function buildLoginMessageV2(sub: string, aud: string, cid: Uint8Array, nonce: Uint8Array, iat: bigint, exp: bigint, ciphertext: Uint8Array): Uint8Array;
|
|
20
|
+
export declare function buildRegisterMessage(username: string, publicKey: Uint8Array): Uint8Array;
|
|
21
21
|
export interface AuthOptions {
|
|
22
22
|
readonly baseUrl?: string;
|
|
23
23
|
}
|
package/build/client/auth.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import { argon2id, sha256 } from 'hash-wasm';
|
|
1
|
+
import { argon2id, sha256, createSHA256 } 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 SERVER_CONFIRM_LABEL = 'toil-server-confirm-v1';
|
|
9
|
+
export const KEM_PUBLIC_KEY_LEN = 1184;
|
|
10
|
+
export const KEM_CIPHERTEXT_LEN = 1088;
|
|
11
|
+
export const SHARED_SECRET_LEN = 32;
|
|
12
|
+
function fromHex(hex) {
|
|
13
|
+
const out = new Uint8Array(hex.length / 2);
|
|
14
|
+
for (let i = 0; i < out.length; i++)
|
|
15
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
export const SERVER_KEM_PUBLIC_KEY = fromHex('29d765e8083182891302569b3712a856e564fdd484b0706b0c68568d5ab7edc742cf74459d64595455a60f267973aa55e43c5be61925a3822eafcca445e36dc4655636e31e6fc9bec338b253f94290008ef7f40dbddb49c15c690f6755a23a1b3c85cfd5207e71a607086a6fc6d74a05080f43276901a19cafdb8de7771d58ea07f0f1056b905127b22223d08e75173199f13ab13c5dcd3b51ac784f84e520484a262b845a897c41cf27324ab6ba545c78c9ccab361051e0bba53498af26240fa0d566d1572684f4b42e253e6d052c848650915063c35641e1121ef8d9cfd17b667b351103c56d195007c9376d0c08aa268396814490eab4c364175a94533267a1933862cc4c33bcf0a13d1fa2b9d6c5082eeca1480672f2526cbe013beff14dc908a386e0b633c8761023cbed760deac6709bc328d865ac82e12307b673d96711dbb27a4d939230d25b53d594169a318be0200fa33550e9418e2a3b30e9719edc09d5fc4306f1abfd021eab14637a8a72c5931d25dc9b56db0e6ab677522b10f25307dbb804a6774ce05b87b0976a4b227bfe6caf20a79e64004fbd27b1eea018b3ab8ffa629f2dc87f19278f95168e94e44660a3370c537795678eb2f056260609769740583b51b291862927a1938737c6a37f40b78f00671cccbcb88ac3427b37915ed58782998f84051647707d48995472baad3f64a7cca54e1c0734db08751c614a34f28b84f2c1b5a6817355ab61957c486b7acffbc092bc8a7b46387f33b53ed372f7168d31a71cd008539928b0cdf91e835aa97f6a2be6d327b87a6ae478701d75a59a25179cb14997bb2552853014724170a1c49b82c2bcebc3279024e1fa44c53c7afdc43f0bd22116490f3b74c90e7296be58b9a91168f2fa0c3d378a3bcac959f357825c9976a8c9ee944f29b45e96d7345d9b478431a20cf1c5d3a3227c717fd204619777636c0cb140db5c50d2a3302334461030bee34e4eb1a6f02b733f9ccda4290fa168bc039568373241542728d00030d1f251e83737cb215adbdc1de75978675a0cd0d75b12748abdda7a9852629c63697d145af2c69854b06e03f37c4b064e4c9a4c03f2ad4d081e70180e9547247921918118086b62b4f7727f46b24e3e79ba3f28209f32b5102035bf935856232f83642268c0292ec6bf8e9462382163d30a20b4bcb7b4439310ec9d0a148193907fc07697342967cf1a16c6b3c71558951fa915400736cf699262b54b723abb2ecc27b74b68ee494287595ef818388adb49e883c67bfa5c226c0eef037a0851a29d34675912c1ea1068310b6dfcd017c809c8fbfc2c3ae78dfef07299960eeefba182662a90fa422c1790f356a2ea909012b15623a9b9e450a282cb530589a68368b3583159d9010ac3e52cc974753c342e58279516339dfb691df94b13a223ad97eb6a09c21dafe6304a3642d6d2067b5238497661fe88ad1227ca3557be2a576b6e17c5a7f997ea07929e76407e376aba74c44cd8504804776f39bbb8327624188a63501e83b404d9438cade0b11dc3ac61856447fb072b91761c228878f01b2eb6b4b21ba664c2c75882431603b25a449ffeb8410b910558581777562aa9b2181fd9c04713ad9326462d3e842121c4997f9aa932417c67851625816de66e0d65637434629f39');
|
|
5
19
|
export const PUBLIC_KEY_LEN = 1312;
|
|
6
20
|
export const SECRET_KEY_LEN = 2560;
|
|
7
21
|
export const SIGNATURE_LEN = 2420;
|
|
@@ -10,9 +24,9 @@ function wipe(buf) {
|
|
|
10
24
|
crypto.getRandomValues(buf);
|
|
11
25
|
buf.fill(0);
|
|
12
26
|
}
|
|
13
|
-
async function deriveSeed(
|
|
27
|
+
async function deriveSeed(oprfOutput, kdf) {
|
|
14
28
|
return argon2id({
|
|
15
|
-
password:
|
|
29
|
+
password: oprfOutput,
|
|
16
30
|
salt: kdf.salt,
|
|
17
31
|
iterations: kdf.iterations,
|
|
18
32
|
parallelism: kdf.parallelism,
|
|
@@ -21,6 +35,33 @@ async function deriveSeed(password, kdf) {
|
|
|
21
35
|
outputType: 'binary',
|
|
22
36
|
});
|
|
23
37
|
}
|
|
38
|
+
const utf8 = (s) => new TextEncoder().encode(s);
|
|
39
|
+
async function sha256Bytes(data) {
|
|
40
|
+
const h = await createSHA256();
|
|
41
|
+
h.init();
|
|
42
|
+
h.update(data);
|
|
43
|
+
return h.digest('binary');
|
|
44
|
+
}
|
|
45
|
+
function concatBytes(...parts) {
|
|
46
|
+
let n = 0;
|
|
47
|
+
for (const p of parts)
|
|
48
|
+
n += p.length;
|
|
49
|
+
const out = new Uint8Array(n);
|
|
50
|
+
let off = 0;
|
|
51
|
+
for (const p of parts) {
|
|
52
|
+
out.set(p, off);
|
|
53
|
+
off += p.length;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function bytesEqual(a, b) {
|
|
58
|
+
if (a.length !== b.length)
|
|
59
|
+
return false;
|
|
60
|
+
let diff = 0;
|
|
61
|
+
for (let i = 0; i < a.length; i++)
|
|
62
|
+
diff |= a[i] ^ b[i];
|
|
63
|
+
return diff === 0;
|
|
64
|
+
}
|
|
24
65
|
export function buildLoginMessage(sub, aud, cid, nonce, iat, exp) {
|
|
25
66
|
return new DataWriter()
|
|
26
67
|
.writeU8(1)
|
|
@@ -32,6 +73,21 @@ export function buildLoginMessage(sub, aud, cid, nonce, iat, exp) {
|
|
|
32
73
|
.writeU64(exp)
|
|
33
74
|
.toBytes();
|
|
34
75
|
}
|
|
76
|
+
export function buildLoginMessageV2(sub, aud, cid, nonce, iat, exp, ciphertext) {
|
|
77
|
+
return new DataWriter()
|
|
78
|
+
.writeU8(2)
|
|
79
|
+
.writeString(sub)
|
|
80
|
+
.writeString(aud)
|
|
81
|
+
.writeBytes(cid)
|
|
82
|
+
.writeBytes(nonce)
|
|
83
|
+
.writeU64(iat)
|
|
84
|
+
.writeU64(exp)
|
|
85
|
+
.writeBytes(ciphertext)
|
|
86
|
+
.toBytes();
|
|
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,64 @@ 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
|
|
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
|
|
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
|
-
|
|
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());
|
|
142
|
+
const finish = await postBinary(baseUrl, '/register/finish', new DataWriter().writeString(username).writeBytes(publicKey).writeBytes(regProof).toBytes());
|
|
83
143
|
if (finish.readU8() !== 0)
|
|
84
144
|
throw new Error('auth: registration rejected');
|
|
85
145
|
}
|
|
86
146
|
export async function login(username, password, opts = {}) {
|
|
87
147
|
const baseUrl = opts.baseUrl ?? '/auth';
|
|
88
|
-
const
|
|
89
|
-
|
|
148
|
+
const oprf = ristretto255_oprf.oprf;
|
|
149
|
+
const pw = utf8(password.normalize('NFKC'));
|
|
150
|
+
const { blind, blinded } = oprf.blind(pw);
|
|
151
|
+
const r = await postBinary(baseUrl, '/login/start', new DataWriter().writeString(username).writeBytes(blinded).toBytes());
|
|
152
|
+
const cid = r.readBytes();
|
|
153
|
+
const aud = r.readString();
|
|
154
|
+
const kdf = decodeKdf(r);
|
|
155
|
+
const nonce = r.readBytes();
|
|
156
|
+
const iat = r.readU64();
|
|
157
|
+
const exp = r.readU64();
|
|
158
|
+
const evaluated = r.readBytes();
|
|
159
|
+
if (BigInt(Math.floor(Date.now() / 1000)) >= exp)
|
|
90
160
|
throw new Error('auth: challenge expired');
|
|
91
|
-
const
|
|
92
|
-
const seed = await deriveSeed(
|
|
161
|
+
const oprfOutput = oprf.finalize(pw, blind, evaluated);
|
|
162
|
+
const seed = await deriveSeed(oprfOutput, kdf);
|
|
163
|
+
const { cipherText, sharedSecret } = ml_kem768.encapsulate(SERVER_KEM_PUBLIC_KEY);
|
|
164
|
+
const message = buildLoginMessageV2(username, aud, cid, nonce, iat, exp, cipherText);
|
|
93
165
|
let signature;
|
|
94
166
|
try {
|
|
95
167
|
const kp = ml_dsa44.keygen(seed);
|
|
96
168
|
try {
|
|
97
|
-
signature = ml_dsa44.sign(message, kp.secretKey, { context:
|
|
169
|
+
signature = ml_dsa44.sign(message, kp.secretKey, { context: utf8(LOGIN_CONTEXT) });
|
|
98
170
|
}
|
|
99
171
|
finally {
|
|
100
172
|
wipe(kp.secretKey);
|
|
@@ -105,10 +177,19 @@ export async function login(username, password, opts = {}) {
|
|
|
105
177
|
}
|
|
106
178
|
if (signature.length !== SIGNATURE_LEN)
|
|
107
179
|
throw new Error('auth: bad signature length');
|
|
108
|
-
const res = await postBinary(baseUrl, '/login/finish', new DataWriter().writeBytes(
|
|
109
|
-
if (res.readU8() !== 0)
|
|
180
|
+
const res = await postBinary(baseUrl, '/login/finish', new DataWriter().writeBytes(cid).writeBytes(cipherText).writeBytes(signature).toBytes());
|
|
181
|
+
if (res.readU8() !== 0) {
|
|
182
|
+
wipe(sharedSecret);
|
|
110
183
|
throw new Error('auth: login failed');
|
|
111
|
-
|
|
184
|
+
}
|
|
185
|
+
const session = res.readBytes();
|
|
186
|
+
const serverConfirm = res.readBytes();
|
|
187
|
+
const transcriptHash = await sha256Bytes(message);
|
|
188
|
+
const expected = await sha256Bytes(concatBytes(utf8(SERVER_CONFIRM_LABEL), sharedSecret, transcriptHash));
|
|
189
|
+
wipe(sharedSecret);
|
|
190
|
+
if (!bytesEqual(expected, serverConfirm))
|
|
191
|
+
throw new Error('auth: server authentication failed');
|
|
192
|
+
return session;
|
|
112
193
|
}
|
|
113
194
|
function toHex(bytes) {
|
|
114
195
|
let s = '';
|
package/build/client/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { mount } from './routing/mount.js';
|
|
2
2
|
export { Router } from './routing/Router.js';
|
|
3
3
|
export { Auth, register as authRegister, login as authLogin, proveIdentity, buildLoginMessage, LOGIN_CONTEXT } from './auth.js';
|
|
4
|
-
export type { KdfParams,
|
|
4
|
+
export type { KdfParams, AuthOptions, IdentityProof } 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';
|