@darkauth/client 1.11.0 → 1.13.0

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/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ type Config = {
12
12
  discovery?: boolean;
13
13
  firstParty?: boolean;
14
14
  tokenStorage?: "memory" | "localStorage";
15
- drkStorage?: "memory" | "localStorage";
15
+ drkStorage?: "memory";
16
16
  refreshMode?: "cookie" | "token";
17
17
  credentials?: RequestCredentials;
18
18
  };
package/dist/index.js CHANGED
@@ -31,13 +31,13 @@ let cfg = {
31
31
  : "http://localhost:5173/callback"),
32
32
  zk: true,
33
33
  };
34
- const OBFUSCATION_KEY = "DarkAuth-Storage-Protection-2025";
35
34
  const EMPTY_DRK = new Uint8Array(0);
36
35
  const ID_TOKEN_KEY = "id_token";
37
36
  const ACCESS_TOKEN_KEY = "access_token";
38
37
  const REFRESH_TOKEN_KEY = "refresh_token";
39
38
  const DRK_STORAGE_KEY = "drk_protected";
40
39
  const OAUTH_STATE_KEY = "oauth_state";
40
+ const V2_KEY_JWE_MAX_TTL_SECONDS = 600;
41
41
  let memorySession = null;
42
42
  let memoryRefreshToken = null;
43
43
  export function setConfig(next) {
@@ -66,9 +66,6 @@ function clearStoredAccessToken() {
66
66
  function tokenStorageMode() {
67
67
  return cfg.tokenStorage || (cfg.firstParty === false ? "localStorage" : "memory");
68
68
  }
69
- function drkStorageMode() {
70
- return cfg.drkStorage || (cfg.firstParty === false ? "localStorage" : "memory");
71
- }
72
69
  function refreshMode() {
73
70
  return cfg.refreshMode || (cfg.firstParty === false ? "token" : "cookie");
74
71
  }
@@ -150,42 +147,44 @@ function parseFragmentParams(hash) {
150
147
  }
151
148
  return res;
152
149
  }
153
- function obfuscateKey(drk) {
154
- const obfKey = new TextEncoder().encode(OBFUSCATION_KEY);
155
- const out = new Uint8Array(drk.length);
156
- for (let i = 0; i < drk.length; i++) {
157
- const a = drk[i] ?? 0;
158
- const b = obfKey[i % obfKey.length] ?? 0;
159
- out[i] = a ^ b;
160
- }
161
- return out;
150
+ async function sha256Base64Url(value) {
151
+ return bytesToBase64Url(await sha256(new TextEncoder().encode(value)));
152
+ }
153
+ function isStringArray(value) {
154
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
155
+ }
156
+ function audienceMatches(value, expected) {
157
+ if (typeof value === "string")
158
+ return value === expected;
159
+ if (isStringArray(value))
160
+ return value.includes(expected);
161
+ return false;
162
162
  }
163
- function deobfuscateKey(obfuscated) {
164
- return obfuscateKey(obfuscated);
163
+ function requireString(value, name) {
164
+ if (typeof value !== "string" || value.length === 0)
165
+ throw new Error(`Invalid ${name}`);
166
+ return value;
167
+ }
168
+ function requireNumber(value, name) {
169
+ if (typeof value !== "number" || !Number.isFinite(value))
170
+ throw new Error(`Invalid ${name}`);
171
+ return value;
172
+ }
173
+ function parseJsonPayload(bytes) {
174
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
175
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
176
+ throw new Error("Invalid key payload");
177
+ return parsed;
165
178
  }
166
179
  function clearStoredDrk() {
167
180
  localStorage.removeItem(DRK_STORAGE_KEY);
168
181
  }
169
182
  function getStoredDrk() {
170
- if (drkStorageMode() !== "localStorage") {
171
- clearStoredDrk();
172
- return null;
173
- }
174
- const obfuscatedDrkBase64 = localStorage.getItem(DRK_STORAGE_KEY);
175
- if (!obfuscatedDrkBase64)
176
- return null;
177
- try {
178
- const obfuscatedDrk = base64UrlToBytes(obfuscatedDrkBase64);
179
- return deobfuscateKey(obfuscatedDrk);
180
- }
181
- catch {
182
- clearStoredDrk();
183
- return null;
184
- }
183
+ clearStoredDrk();
184
+ return null;
185
185
  }
186
186
  function storeSession(session) {
187
187
  const tokenMode = tokenStorageMode();
188
- const drkMode = drkStorageMode();
189
188
  const currentRefreshMode = refreshMode();
190
189
  const storedSession = {
191
190
  idToken: session.idToken,
@@ -205,13 +204,7 @@ function storeSession(session) {
205
204
  clearStoredIdToken();
206
205
  clearStoredAccessToken();
207
206
  }
208
- if (drkMode === "localStorage" && session.drk.length > 0) {
209
- const obfuscatedDrk = obfuscateKey(session.drk);
210
- localStorage.setItem(DRK_STORAGE_KEY, bytesToBase64Url(obfuscatedDrk));
211
- }
212
- else {
213
- clearStoredDrk();
214
- }
207
+ clearStoredDrk();
215
208
  if (currentRefreshMode === "token") {
216
209
  memoryRefreshToken = session.refreshToken || memoryRefreshToken;
217
210
  if (tokenMode === "localStorage" && session.refreshToken) {
@@ -229,12 +222,13 @@ function clearCallbackStorage() {
229
222
  sessionStorage.removeItem(OAUTH_STATE_KEY);
230
223
  sessionStorage.removeItem("pkce_verifier");
231
224
  }
232
- function stripDrkJweFragment() {
233
- if (!location.hash.includes("drk_jwe="))
225
+ function stripKeyJweFragment() {
226
+ if (!location.hash.includes("drk_jwe=") && !location.hash.includes("darkauth_key_jwe="))
234
227
  return;
235
228
  const hash = location.hash.startsWith("#") ? location.hash.slice(1) : location.hash;
236
229
  const params = new URLSearchParams(hash);
237
230
  params.delete("drk_jwe");
231
+ params.delete("darkauth_key_jwe");
238
232
  const nextHash = params.toString();
239
233
  const nextUrl = `${location.origin}${location.pathname}${location.search || ""}${nextHash ? `#${nextHash}` : ""}`;
240
234
  try {
@@ -307,8 +301,9 @@ export async function handleCallback() {
307
301
  return null;
308
302
  const fragmentParams = parseFragmentParams(location.hash || "");
309
303
  const drkJwe = fragmentParams.drk_jwe;
310
- if (drkJwe)
311
- stripDrkJweFragment();
304
+ const darkauthKeyJwe = fragmentParams.darkauth_key_jwe;
305
+ if (drkJwe || darkauthKeyJwe)
306
+ stripKeyJweFragment();
312
307
  const expectedState = sessionStorage.getItem(OAUTH_STATE_KEY);
313
308
  const returnedState = params.get("state");
314
309
  if (!expectedState)
@@ -342,6 +337,15 @@ export async function handleCallback() {
342
337
  const zkDrkHash = typeof tokenResponse.zk_drk_hash === "string"
343
338
  ? tokenResponse.zk_drk_hash
344
339
  : null;
340
+ const zkKeyHash = typeof tokenResponse.zk_key_hash === "string"
341
+ ? tokenResponse.zk_key_hash
342
+ : null;
343
+ const zkKeyKind = typeof tokenResponse.zk_key_kind === "string"
344
+ ? tokenResponse.zk_key_kind
345
+ : null;
346
+ const zkKeyVersion = typeof tokenResponse.zk_key_version === "string"
347
+ ? tokenResponse.zk_key_version
348
+ : null;
345
349
  const idToken = tokenResponse.id_token;
346
350
  const accessToken = typeof tokenResponse.access_token === "string"
347
351
  ? tokenResponse.access_token
@@ -350,25 +354,87 @@ export async function handleCallback() {
350
354
  const refreshToken = tokenRefreshMode === "token"
351
355
  ? tokenResponse.refresh_token
352
356
  : undefined;
353
- const hasZkArtifacts = !!drkJwe || !!zkDrkHash;
357
+ const hasV2Artifacts = !!darkauthKeyJwe || !!zkKeyHash || !!zkKeyKind || !!zkKeyVersion;
358
+ const hasLegacyArtifacts = !!drkJwe || !!zkDrkHash;
359
+ const hasZkArtifacts = hasV2Artifacts || hasLegacyArtifacts;
354
360
  if (!hasZkArtifacts) {
355
361
  clearCallbackUrl();
356
362
  return storeSession({ idToken, accessToken, drk: EMPTY_DRK, refreshToken });
357
363
  }
358
- if (!drkJwe || typeof drkJwe !== "string")
359
- throw new Error("Missing DRK JWE from URL fragment");
360
- if (zkDrkHash) {
361
- const hash = bytesToBase64Url(await sha256(new TextEncoder().encode(drkJwe)));
362
- if (zkDrkHash !== hash)
363
- throw new Error("DRK hash mismatch");
364
- }
365
364
  const privateJwkString = sessionStorage.getItem("zk_eph_priv_jwk");
366
365
  if (!privateJwkString)
367
366
  throw new Error("Missing ZK private key for callback");
368
367
  sessionStorage.removeItem("zk_eph_priv_jwk");
369
368
  const privateKey = await crypto.subtle.importKey("jwk", JSON.parse(privateJwkString), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits", "deriveKey"]);
370
- const { plaintext } = await compactDecrypt(drkJwe, privateKey);
371
- const drk = new Uint8Array(plaintext);
369
+ let drk;
370
+ if (hasV2Artifacts) {
371
+ if (hasLegacyArtifacts)
372
+ throw new Error("Mixed key delivery metadata");
373
+ if (!darkauthKeyJwe || typeof darkauthKeyJwe !== "string")
374
+ throw new Error("Missing client key JWE from URL fragment");
375
+ if (!zkKeyHash)
376
+ throw new Error("Missing client key hash");
377
+ if (zkKeyKind !== "client_app_key")
378
+ throw new Error("Invalid client key kind");
379
+ if (zkKeyVersion !== "v2")
380
+ throw new Error("Invalid client key version");
381
+ const hash = await sha256Base64Url(darkauthKeyJwe);
382
+ if (zkKeyHash !== hash)
383
+ throw new Error("Client key hash mismatch");
384
+ const { plaintext, protectedHeader } = await compactDecrypt(darkauthKeyJwe, privateKey);
385
+ if (protectedHeader.alg !== "ECDH-ES" || protectedHeader.enc !== "A256GCM")
386
+ throw new Error("Invalid client key JWE header");
387
+ const payload = parseJsonPayload(new Uint8Array(plaintext));
388
+ if (payload.typ !== "DarkAuth-Client-Key")
389
+ throw new Error("Invalid client key type");
390
+ if (payload.version !== "v2" && payload.version !== "v2-client-key")
391
+ throw new Error("Invalid client key payload version");
392
+ if (payload.key_kind !== "client_app_key")
393
+ throw new Error("Invalid client key payload kind");
394
+ if (payload.client_id !== cfg.clientId)
395
+ throw new Error("Invalid client key client");
396
+ if (!audienceMatches(payload.aud, cfg.clientId))
397
+ throw new Error("Invalid client key audience");
398
+ const idTokenSubject = parseJwt(idToken)?.sub;
399
+ if (!idTokenSubject || payload.sub !== idTokenSubject)
400
+ throw new Error("Invalid client key subject");
401
+ if (payload.state_hash !== (await sha256Base64Url(expectedState)))
402
+ throw new Error("Invalid client key state");
403
+ if (payload.redirect_uri_hash !== (await sha256Base64Url(cfg.redirectUri)))
404
+ throw new Error("Invalid client key redirect URI");
405
+ requireString(payload.key_id, "client key id");
406
+ const exp = requireNumber(payload.exp, "client key expiry");
407
+ const now = Date.now() / 1000;
408
+ if (exp <= now)
409
+ throw new Error("Client key JWE expired");
410
+ if (typeof payload.iat === "number") {
411
+ if (!Number.isFinite(payload.iat))
412
+ throw new Error("Invalid client key issued-at");
413
+ if (payload.iat > now + 60)
414
+ throw new Error("Invalid client key issued-at");
415
+ if (exp - payload.iat > V2_KEY_JWE_MAX_TTL_SECONDS)
416
+ throw new Error("Client key JWE lifetime too long");
417
+ }
418
+ else if (exp > now + V2_KEY_JWE_MAX_TTL_SECONDS) {
419
+ throw new Error("Client key JWE lifetime too long");
420
+ }
421
+ drk = base64UrlToBytes(requireString(payload.cak, "client app key"));
422
+ if (drk.length === 0)
423
+ throw new Error("Invalid client app key");
424
+ }
425
+ else {
426
+ if (!drkJwe || typeof drkJwe !== "string")
427
+ throw new Error("Missing DRK JWE from URL fragment");
428
+ if (zkDrkHash) {
429
+ const hash = await sha256Base64Url(drkJwe);
430
+ if (zkDrkHash !== hash)
431
+ throw new Error("DRK hash mismatch");
432
+ }
433
+ const { plaintext, protectedHeader } = await compactDecrypt(drkJwe, privateKey);
434
+ if (protectedHeader.alg !== "ECDH-ES" || protectedHeader.enc !== "A256GCM")
435
+ throw new Error("Invalid DRK JWE header");
436
+ drk = new Uint8Array(plaintext);
437
+ }
372
438
  clearCallbackUrl();
373
439
  return storeSession({ idToken, accessToken, drk, refreshToken });
374
440
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkauth/client",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "directory": "packages/darkauth-client",