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.
@@ -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
  }
@@ -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(password, kdf) {
27
+ async function deriveSeed(oprfOutput, kdf) {
14
28
  return argon2id({
15
- password: new TextEncoder().encode(password.normalize('NFKC')),
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 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());
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 ch = decodeChallenge(await postBinary(baseUrl, '/login/start', new DataWriter().writeString(username).toBytes()));
89
- if (BigInt(Math.floor(Date.now() / 1000)) >= ch.exp)
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 message = buildLoginMessage(username, ch.aud, ch.cid, ch.nonce, ch.iat, ch.exp);
92
- const seed = await deriveSeed(password, ch.kdf);
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: new TextEncoder().encode(LOGIN_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(ch.cid).writeBytes(signature).toBytes());
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
- return res.readBytes();
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 = '';
@@ -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, Challenge, AuthOptions, IdentityProof } from './auth.js';
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';