mirror of
https://github.com/xyzeva/k-id-age-verifier.git
synced 2026-09-20 23:50:44 +02:00
fix: add proxy & payload changes
This commit is contained in:
@@ -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=
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"imports": {
|
||||
"hono": "npm:hono@^4.11.9",
|
||||
"zod": "npm:zod",
|
||||
"@hono/zod-validator": "npm:@hono/zod-validator"
|
||||
}
|
||||
}
|
||||
Generated
+30
@@ -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@*"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> = {};
|
||||
for (const [key, value] of res.headers.entries()) {
|
||||
resHeaders[key] = value;
|
||||
}
|
||||
|
||||
return c.body(resBody, res.status as ContentfulStatusCode, resHeaders);
|
||||
}
|
||||
);
|
||||
|
||||
export default app;
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import QRCodeScanner from '$lib/components/QRCodeScanner.svelte';
|
||||
|
||||
const IS_PATCHED = true;
|
||||
const IS_PATCHED = false;
|
||||
</script>
|
||||
|
||||
<div class="mx-auto w-screen max-w-6xl items-center p-5 pb-16">
|
||||
|
||||
@@ -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 = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
async function fetchWithWorkerProxy(
|
||||
sessionId: string,
|
||||
url: string,
|
||||
init: RequestInit
|
||||
): Promise<Response> {
|
||||
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<typeof getRandomLocation>,
|
||||
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);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user