From ba28b3771703792cb145031896d4b5d560f7af22 Mon Sep 17 00:00:00 2001 From: eva Date: Fri, 13 Feb 2026 04:37:25 +0000 Subject: [PATCH] fix: add proxy & payload changes --- .env.example | 4 + proxy/deno.json | 7 + proxy/deno.lock | 30 ++++ proxy/mod.ts | 84 +++++++++++ scripts/standalone-verify.ts | 7 +- src/routes/+page.svelte | 2 +- src/routes/api/verify/+server.ts | 243 +++++++++++-------------------- 7 files changed, 218 insertions(+), 159 deletions(-) create mode 100644 .env.example create mode 100644 proxy/deno.json create mode 100644 proxy/deno.lock create mode 100644 proxy/mod.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a325f1e --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# if your running on cloudflare workers, you will need to supply a http proxy +ENABLE_WORKER_PROXY=false +WORKER_PROXY_TOKEN= +WORKER_PROXY_URL= diff --git a/proxy/deno.json b/proxy/deno.json new file mode 100644 index 0000000..9268e1f --- /dev/null +++ b/proxy/deno.json @@ -0,0 +1,7 @@ +{ + "imports": { + "hono": "npm:hono@^4.11.9", + "zod": "npm:zod", + "@hono/zod-validator": "npm:@hono/zod-validator" + } +} diff --git a/proxy/deno.lock b/proxy/deno.lock new file mode 100644 index 0000000..523a5fe --- /dev/null +++ b/proxy/deno.lock @@ -0,0 +1,30 @@ +{ + "version": "5", + "specifiers": { + "npm:@hono/zod-validator@*": "0.7.6_hono@4.11.9_zod@4.3.6", + "npm:hono@^4.11.9": "4.11.9", + "npm:zod@*": "4.3.6" + }, + "npm": { + "@hono/zod-validator@0.7.6_hono@4.11.9_zod@4.3.6": { + "integrity": "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw==", + "dependencies": [ + "hono", + "zod" + ] + }, + "hono@4.11.9": { + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==" + }, + "zod@4.3.6": { + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==" + } + }, + "workspace": { + "dependencies": [ + "npm:@hono/zod-validator@*", + "npm:hono@^4.11.9", + "npm:zod@*" + ] + } +} diff --git a/proxy/mod.ts b/proxy/mod.ts new file mode 100644 index 0000000..4342463 --- /dev/null +++ b/proxy/mod.ts @@ -0,0 +1,84 @@ +import { Hono } from 'hono'; +import { ContentfulStatusCode } from 'hono/utils/http-status'; +import crypto from 'node:crypto'; +import { zValidator } from '@hono/zod-validator'; +import z from 'zod'; + +const app = new Hono(); + +const proxyToken = Deno.env.get('PROXY_TOKEN'); +const proxyUrlTemplate = Deno.env.get('PROXY_URL'); + +if (!proxyToken) { + console.error('failed to find PROXY_TOKEN environment variable'); + Deno.exit(1); +} + +if (!proxyUrlTemplate) { + console.error('failed to find PROXY_URL environment variable'); + Deno.exit(1); +} + +function timingSafeEquals(a: string, b: string) { + const aBuf = new TextEncoder().encode(a); + const bBuf = new TextEncoder().encode(b); + + if (aBuf.length !== bBuf.length) { + return false; + } + + return crypto.timingSafeEqual(aBuf, bBuf); +} + +app.post( + '/proxy', + zValidator( + 'json', + z.object({ + method: z.enum(['GET', 'POST', 'PATCH', 'PUT', 'DELETE']), + url: z.url(), + headers: z.record(z.string(), z.string()), + body: z.string().nullable(), + sessionId: z.string() + }) + ), + async (c) => { + const header = c.req.header('x-proxy-token'); + if (!header || !timingSafeEquals(proxyToken, header)) { + return c.json( + { + code: 20017, + message: "The Maze isn't meant for you 👽👽👽" + }, + 403 + ); + } + + const body = c.req.valid('json'); + + const client = Deno.createHttpClient({ + proxy: { + url: proxyUrlTemplate.replaceAll('{sessionId}', body.sessionId) + } + }); + console.log(proxyUrlTemplate.replaceAll('{sessionId}', body.sessionId)); + const res = await fetch(body.url, { + method: body.method, + headers: { + ...body.headers + }, + body: body.body, + client + }); + + const resBody = await res.arrayBuffer(); + const resHeaders: Record = {}; + for (const [key, value] of res.headers.entries()) { + resHeaders[key] = value; + } + + return c.body(resBody, res.status as ContentfulStatusCode, resHeaders); + } +); + +export default app; diff --git a/scripts/standalone-verify.ts b/scripts/standalone-verify.ts index d4669fb..c97c275 100644 --- a/scripts/standalone-verify.ts +++ b/scripts/standalone-verify.ts @@ -476,7 +476,8 @@ async function verify(qrCodeUrlStr: string) { const laplacianBlurScores = Array.from({ length: 300 }, () => randomFloat(10, 300, 15)); const laplacianMinScore = Math.min(...laplacianBlurScores); const laplacianMaxScore = Math.max(...laplacianBlurScores); - const laplacianAvgScore = laplacianBlurScores.reduce((sum, score) => sum + score) / laplacianBlurScores.length; + const laplacianAvgScore = + laplacianBlurScores.reduce((sum, score) => sum + score) / laplacianBlurScores.length; let payload = { request_type: 'complete_transaction', @@ -685,9 +686,9 @@ async function verify(qrCodeUrlStr: string) { isCameraPermissionGranted: true, completionTime, deferredComputationStartedAt: - randomInt(10000, 14000) + Number(Math.random().toFixed(randomInt(1, 3))), + currentTime + randomInt(20, 80) + Number(Math.random().toFixed(randomInt(1, 3))), instructionCompletionTime: randomInt(10000, 14000), - initialAdjustmentTime: randomInt(10000, 14000), + initialAdjustmentTime: randomInt(100, 500), completionState: 'COMPLETE', unfinishedInstructions: Object.fromEntries( [ diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e1e700f..691d668 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,7 @@
diff --git a/src/routes/api/verify/+server.ts b/src/routes/api/verify/+server.ts index 8f20224..26e8de2 100644 --- a/src/routes/api/verify/+server.ts +++ b/src/routes/api/verify/+server.ts @@ -1,12 +1,8 @@ +import { ENABLE_WORKER_PROXY, WORKER_PROXY_TOKEN, WORKER_PROXY_URL } from '$env/static/private'; import type { RequestEvent } from './$types'; import { Buffer } from 'node:buffer'; const BASE_URL = 'https://eu-west-1.faceassure.com'; -const K_ID_DEPLOYMENT_ID = '20260212204349-d873b84-production'; -const K_ID_PRIVATELY_ACTION_ID = '40b20a9df4b9c190d3b926d1c6b859c42ca73cb038'; -const K_ID_NEXT_ROUTER_TREE = - '%5B%22%22%2C%7B%22children%22%3A%5B%22verify%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D'; -const PRIVATELY_URL_REGEX = /(https:\/\/[a-z0-9]+\.cloudfront\.net\/.*)(?=:\{)/; const jsonResponse = (body: unknown, status: number = 200, extraHeaders: object = {}) => new Response(JSON.stringify(body), { @@ -22,6 +18,31 @@ const randomFloat = (min: number, max: number, decimals = 15) => parseFloat((Math.random() * (max - min) + min).toFixed(decimals)); const randomChoice = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; +async function fetchWithWorkerProxy( + sessionId: string, + url: string, + init: RequestInit +): Promise { + if (ENABLE_WORKER_PROXY !== 'true') { + return await fetch(url, init); + } + + return await fetch(WORKER_PROXY_URL, { + method: 'POST', + headers: { + 'x-proxy-token': WORKER_PROXY_TOKEN, + 'content-type': 'application/json' + }, + body: JSON.stringify({ + method: init.method ?? 'GET', + url: url, + headers: init.headers, + body: init.body ?? null, + sessionId + }) + }); +} + function generateId(id: string, sub: string, sessionId: string, delimiter = '|') { let s = 0; for (let a = '' + id + delimiter + sub + delimiter + sessionId, i = 0; i < a.length; i++) { @@ -334,6 +355,7 @@ async function generateKIDProof(grant: string, attemptId: string) { } async function verify( + sessionId: string, userAgent: string, location: ReturnType, token: string @@ -372,22 +394,26 @@ async function verify( if (parts.length !== 3) throw `token is an invalid jwt `; const jwtPayload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); - const sessionRes = await fetch(`${BASE_URL}/age-services/d-privately-age-services`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...commonHeaders - }, - body: JSON.stringify({ - request_type: 'generate_new_session', - transaction_id: jwtPayload.jti, - api_key: null, - api_secret: null, - token, - longURL: null, - userAgent: userAgent - }) - }); + const sessionRes = await fetchWithWorkerProxy( + sessionId, + `${BASE_URL}/age-services/d-privately-age-services`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...commonHeaders + }, + body: JSON.stringify({ + request_type: 'generate_new_session', + transaction_id: jwtPayload.jti, + api_key: null, + api_secret: null, + token, + longURL: null, + userAgent: userAgent + }) + } + ); if (!sessionRes.ok) throw `failed to generate new session (status=${sessionRes.status}, body=${JSON.stringify(await sessionRes.text())})`; @@ -490,7 +516,8 @@ async function verify( const laplacianBlurScores = Array.from({ length: 300 }, () => randomFloat(10, 300, 15)); const laplacianMinScore = Math.min(...laplacianBlurScores); const laplacianMaxScore = Math.max(...laplacianBlurScores); - const laplacianAvgScore = laplacianBlurScores.reduce((sum, score) => sum + score) / laplacianBlurScores.length; + const laplacianAvgScore = + laplacianBlurScores.reduce((sum, score) => sum + score) / laplacianBlurScores.length; let payload = { request_type: 'complete_transaction', @@ -699,9 +726,9 @@ async function verify( isCameraPermissionGranted: true, completionTime, deferredComputationStartedAt: - randomInt(10000, 14000) + Number(Math.random().toFixed(randomInt(1, 3))), + currentTime + randomInt(20, 80) + Number(Math.random().toFixed(randomInt(1, 3))), instructionCompletionTime: randomInt(10000, 14000), - initialAdjustmentTime: randomInt(10000, 14000), + initialAdjustmentTime: randomInt(100, 500), completionState: 'COMPLETE', unfinishedInstructions: Object.fromEntries( [ @@ -779,21 +806,25 @@ async function verify( const encryptionData = await encryptPayload(sessionData.nonce, payload); payload = Object.assign(payload, encryptionData); - const completeRes = await fetch(`${BASE_URL}/age-services/d-privately-age-services`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'User-Agent': userAgent, - accept: '*/*', - 'accept-language': location.lang, - 'access-control-allow-origin': '*', - priority: 'u=1, i', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'cross-site' - }, - body: JSON.stringify(payload) - }); + const completeRes = await fetchWithWorkerProxy( + sessionId, + `${BASE_URL}/age-services/d-privately-age-services`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': userAgent, + accept: '*/*', + 'accept-language': location.lang, + 'access-control-allow-origin': '*', + priority: 'u=1, i', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site' + }, + body: JSON.stringify(payload) + } + ); if (!completeRes.ok) throw `failed to complete transaction (status=${completeRes.status}, body=${JSON.stringify(await completeRes.text())})`; @@ -804,112 +835,10 @@ async function verify( export const POST = async (event: RequestEvent) => { const userAgent = generateUserAgent(); const location = getRandomLocation(); + const sessionId = String(randomInt(1000000000, 1999999999)); const { type, identifier }: { type: string; identifier: string } = await event.request.json(); - if (type === 'webview') { - let webviewUrl: URL; - try { - webviewUrl = new URL(identifier); - } catch (e) { - return jsonResponse({ error: 'error parsing webview url' }, 400); - } - - if (webviewUrl.host !== 'family.k-id.com' || webviewUrl.pathname !== '/verify') { - return jsonResponse({ error: 'unexpected webview url' }, 400); - } - - const kIdToken = webviewUrl.searchParams.get('token'); - if (!kIdToken) { - return jsonResponse({ error: 'no k-id token in webview url' }, 400); - } - - const parts = kIdToken.split('.'); - if (parts.length !== 3) { - return jsonResponse({ error: 'invalid k-id jwt' }, 400); - } - - const payload = JSON.parse(atob(parts[1])); - - const attemptId = crypto.randomUUID(); - - // fetch the webview first - const res = await fetch(webviewUrl, { - headers: { - 'User-Agent': userAgent, - accept: - 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'accept-language': location.lang, - priority: 'u=1, i', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'cross-site', - Origin: 'https://family.k-id.com', - Referer: identifier - } - }); - - const privatelySsrGrantMatch = (await res.text()).match( - /privatelySsrGrant\\?":\\?"(eyJ[\w-]+\.[\w-]+\.[\w-]+)/ - ); - - if (!privatelySsrGrantMatch) { - return jsonResponse({ error: 'failed to match privately ssr grant' }, 500); - } - - const grant = privatelySsrGrantMatch[1]; - const proof = await generateKIDProof(grant, attemptId); - - const privatelyActionRes = await fetch(webviewUrl, { - method: 'POST', - headers: { - 'User-Agent': userAgent, - accept: '*/*', - 'accept-language': location.lang, - priority: 'u=1, i', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'cross-site', - 'Next-Action': K_ID_PRIVATELY_ACTION_ID, - 'Next-Router-State-Tree': K_ID_NEXT_ROUTER_TREE, - 'X-Deployment-Id': K_ID_DEPLOYMENT_ID, - 'Content-Type': 'application/json', - Origin: 'https://family.k-id.com', - Referer: identifier - }, - body: JSON.stringify([ - { verificationId: payload.jti, useBranding: true, attemptId, proof, grant } - ]) - }); - - if (!privatelyActionRes.ok) { - return jsonResponse( - { - error: `failed to execute k-id privately action (status=${privatelyActionRes.status})` - }, - 500 - ); - } - - const privatelyActionBody = await privatelyActionRes.text(); - const match = privatelyActionBody.match(PRIVATELY_URL_REGEX); - - if (!match) { - return jsonResponse({ error: 'no privately url found in response' }, 500); - } - - const privatelyUrl = new URL(match[1]); - const privatelyToken = privatelyUrl.searchParams.get('token'); - - if (!privatelyToken) { - return jsonResponse({ error: 'no privately token' }, 500); - } - - await verify(userAgent, location, privatelyToken); - - return jsonResponse({ success: true }); - } - if (type === 'qr_link') { let qrCodeUrl: URL; try { @@ -923,19 +852,23 @@ export const POST = async (event: RequestEvent) => { return jsonResponse({ error: 'failed to get shortlink id from qr code url' }, 400); } - const res = await fetch(`${BASE_URL}/shortlinks/${encodeURIComponent(shortlinkId)}`, { - headers: { - 'User-Agent': userAgent, - accept: '*/*', - 'accept-language': location.lang, - priority: 'u=1, i', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'cross-site' + const res = await fetchWithWorkerProxy( + sessionId, + `${BASE_URL}/shortlinks/${encodeURIComponent(shortlinkId)}`, + { + headers: { + 'User-Agent': userAgent, + accept: '*/*', + 'accept-language': location.lang, + priority: 'u=1, i', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site' + } } - }); + ); if (!res.ok) { - return jsonResponse(`failed to get shortlink (status=${res.status})`, 400); + return jsonResponse({ error: `failed to get shortlink (status=${res.status})` }, 400); } const data = await res.json(); @@ -945,10 +878,10 @@ export const POST = async (event: RequestEvent) => { return jsonResponse({ error: 'token not found in original url' }, 500); } - await verify(userAgent, location, token); + await verify(sessionId, userAgent, location, token); return jsonResponse({ success: true }); } return jsonResponse({ error: 'unexpected type' }, 400); -}; \ No newline at end of file +};