mirror of
https://github.com/xyzeva/k-id-age-verifier.git
synced 2026-06-07 15:23:54 +02:00
meta: initial work
doesn't fully work yet
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
# Cloudflare Types
|
||||
/worker-configuration.d.ts
|
||||
@@ -0,0 +1,9 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tailwindStylesheet": "./src/routes/layout.css"
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# k-id age verifier
|
||||
|
||||
more information [on the website](https://age-verifier.eva.ac)
|
||||
@@ -0,0 +1,39 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import path from 'node:path';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
|
||||
|
||||
export default defineConfig(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||
rules: {
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "k-id-age-verifier",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "wrangler dev .svelte-kit/cloudflare/_worker.js --port 4173",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"gen": "wrangler types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/adapter-cloudflare": "^7.2.6",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^24",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.14.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.49.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"vite": "^7.3.1",
|
||||
"wrangler": "^4.63.0"
|
||||
}
|
||||
}
|
||||
Generated
+3026
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,747 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
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)];
|
||||
|
||||
const BASE_URL = 'https://eu-west-1.faceassure.com';
|
||||
|
||||
function generateUserAgent() {
|
||||
const agents = [
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/117.0',
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36'
|
||||
];
|
||||
return agents[Math.floor(Math.random() * agents.length)];
|
||||
}
|
||||
|
||||
function parseUserAgent(userAgent: string) {
|
||||
const isIOS = /iPhone|iPad/.test(userAgent);
|
||||
const isAndroid = /Android/.test(userAgent);
|
||||
const isSafari = /Safari/.test(userAgent) && !/Chrome/.test(userAgent);
|
||||
const isChrome = /Chrome/.test(userAgent);
|
||||
const isFirefox = /Firefox/.test(userAgent);
|
||||
|
||||
return {
|
||||
browser: {
|
||||
name: isSafari ? 'Safari' : isChrome ? 'Chrome' : isFirefox ? 'Firefox' : 'Safari',
|
||||
version: isIOS ? '17.0' : '117.0'
|
||||
},
|
||||
device: {
|
||||
type: 'mobile',
|
||||
vendor: isIOS ? 'Apple' : 'Samsung',
|
||||
model: isIOS ? 'iPhone' : 'Galaxy'
|
||||
},
|
||||
os: {
|
||||
name: isIOS ? 'iOS' : isAndroid ? 'Android' : 'iOS',
|
||||
version: isIOS ? '17.0' : '13'
|
||||
},
|
||||
engine: { name: isSafari || isIOS ? 'WebKit' : 'Blink' },
|
||||
cpu: { architecture: '64' }
|
||||
};
|
||||
}
|
||||
|
||||
function getRandomLocation() {
|
||||
const locations = [
|
||||
{
|
||||
country: 'United States',
|
||||
state: 'California',
|
||||
timezone: 'America/Los_Angeles',
|
||||
lang: 'en-US,en;q=0.9'
|
||||
},
|
||||
{
|
||||
country: 'United States',
|
||||
state: 'New York',
|
||||
timezone: 'America/New_York',
|
||||
lang: 'en-US,en;q=0.9'
|
||||
},
|
||||
{
|
||||
country: 'Canada',
|
||||
state: 'Ontario',
|
||||
timezone: 'America/Toronto',
|
||||
lang: 'en-CA,en;q=0.9,fr;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Australia',
|
||||
state: 'New South Wales',
|
||||
timezone: 'Australia/Sydney',
|
||||
lang: 'en-AU,en;q=0.9'
|
||||
},
|
||||
{
|
||||
country: 'Germany',
|
||||
state: 'Berlin',
|
||||
timezone: 'Europe/Berlin',
|
||||
lang: 'de-DE,de;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'France',
|
||||
state: 'Île-de-France',
|
||||
timezone: 'Europe/Paris',
|
||||
lang: 'fr-FR,fr;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Netherlands',
|
||||
state: 'North Holland',
|
||||
timezone: 'Europe/Amsterdam',
|
||||
lang: 'nl-NL,nl;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Sweden',
|
||||
state: 'Stockholm',
|
||||
timezone: 'Europe/Stockholm',
|
||||
lang: 'sv-SE,sv;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Norway',
|
||||
state: 'Oslo',
|
||||
timezone: 'Europe/Oslo',
|
||||
lang: 'nb-NO,nb;q=0.9,en;q=0.8'
|
||||
}
|
||||
];
|
||||
return locations[Math.floor(Math.random() * locations.length)];
|
||||
}
|
||||
|
||||
function generateMediaMetadata() {
|
||||
const randomHex = () =>
|
||||
Array.from({ length: 32 }, () =>
|
||||
Math.floor(Math.random() * 16)
|
||||
.toString(16)
|
||||
.toUpperCase()
|
||||
).join('');
|
||||
|
||||
const specs = [
|
||||
{
|
||||
width: 4032,
|
||||
height: 3024,
|
||||
frameRate: 60,
|
||||
zoom: 10,
|
||||
aspectRatio: 4032 / 3024
|
||||
},
|
||||
{
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
frameRate: 60,
|
||||
zoom: 8,
|
||||
aspectRatio: 3840 / 2160
|
||||
},
|
||||
{
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frameRate: 120,
|
||||
zoom: 5,
|
||||
aspectRatio: 1920 / 1080
|
||||
},
|
||||
{
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
frameRate: 60,
|
||||
zoom: 6,
|
||||
aspectRatio: 2560 / 1440
|
||||
}
|
||||
];
|
||||
|
||||
const spec = randomChoice(specs);
|
||||
const deviceId = randomHex();
|
||||
|
||||
return [
|
||||
{
|
||||
mediaKind: 'audioinput',
|
||||
mediaLabel: randomChoice(['', 'Built-in Microphone', 'Default']),
|
||||
mediaId: randomHex(),
|
||||
mediaCapabilities: {}
|
||||
},
|
||||
{
|
||||
mediaKind: 'videoinput',
|
||||
mediaLabel: 'Front Camera',
|
||||
mediaId: deviceId,
|
||||
mediaCapabilities: {
|
||||
aspectRatio: {
|
||||
max: spec.aspectRatio,
|
||||
min: randomFloat(0.0003, 0.001, 15)
|
||||
},
|
||||
backgroundBlur: [false],
|
||||
deviceId: deviceId,
|
||||
facingMode: ['user'],
|
||||
focusDistance: { min: randomFloat(0.1, 0.3) },
|
||||
frameRate: { max: spec.frameRate, min: 1 },
|
||||
groupId: randomHex(),
|
||||
height: { max: spec.height, min: 1 },
|
||||
powerEfficient: [false, true],
|
||||
whiteBalanceMode: randomChoice([
|
||||
['manual', 'continuous'],
|
||||
['auto', 'manual'],
|
||||
['continuous']
|
||||
]),
|
||||
width: { max: spec.width, min: 1 },
|
||||
zoom: { max: spec.zoom, min: 1 }
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const AMAP_MAP: Record<number, [number, number]> = {
|
||||
0: [0, 2],
|
||||
1: [2, 4],
|
||||
2: [4, 8],
|
||||
3: [8, 13],
|
||||
4: [13, 18],
|
||||
5: [18, 21],
|
||||
6: [21, 25],
|
||||
7: [25, 28],
|
||||
8: [28, 32],
|
||||
9: [32, 36],
|
||||
10: [36, 40],
|
||||
11: [40, 45],
|
||||
12: [45, 50],
|
||||
13: [50, 60],
|
||||
14: [60, 70],
|
||||
15: [70, 120]
|
||||
};
|
||||
|
||||
function amap(e: number) {
|
||||
const n = AMAP_MAP[~~e];
|
||||
const r = e % 1;
|
||||
return n[0] + r * (n[1] - n[0]);
|
||||
}
|
||||
|
||||
function removeOutliersWithZscore(arr: number[]) {
|
||||
const r =
|
||||
arr.reduce(function (e, t) {
|
||||
return e + t;
|
||||
}, 0) / arr.length;
|
||||
const a =
|
||||
arr.reduce(function (e, t) {
|
||||
return e + Math.pow(t - r, 2);
|
||||
}, 0) / arr.length;
|
||||
const s = Math.sqrt(a);
|
||||
return arr.filter(function (e) {
|
||||
return Math.abs((e - r) / s) <= 1;
|
||||
});
|
||||
}
|
||||
|
||||
async function encryptPayload(nonce: string, payload: any) {
|
||||
const getKey = async (nonce: string, timestamp: string, transactionId: string) => {
|
||||
const data = nonce + timestamp + transactionId;
|
||||
const dataEncoded = new TextEncoder().encode(data);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
dataEncoded,
|
||||
{
|
||||
name: 'HKDF'
|
||||
},
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
const derived = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: new Uint8Array(0),
|
||||
info: new TextEncoder().encode('payload-encryption')
|
||||
},
|
||||
key,
|
||||
32 * 8
|
||||
);
|
||||
|
||||
return await crypto.subtle.importKey(
|
||||
'raw',
|
||||
derived,
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
};
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const key = await getKey(nonce, timestamp, payload.transaction_id);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
new TextEncoder().encode(JSON.stringify(payload))
|
||||
);
|
||||
|
||||
const rawBuffer = Buffer.from(encryptedBuffer);
|
||||
const encryptedPayloadBuf = rawBuffer.subarray(0, rawBuffer.length - 16);
|
||||
const authTagBuf = rawBuffer.subarray(rawBuffer.length - 16);
|
||||
|
||||
return {
|
||||
encrypted_payload: encryptedPayloadBuf.toString('base64'),
|
||||
iv: Buffer.from(iv).toString('base64'),
|
||||
auth_tag: authTagBuf.toString('base64'),
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
const qrCodeUrl = Deno.args[0];
|
||||
|
||||
async function verify(qrCodeUrlStr: string) {
|
||||
const userAgent = generateUserAgent();
|
||||
const parsedUserAgent = parseUserAgent(userAgent);
|
||||
const location = getRandomLocation();
|
||||
const mediaMetadata = generateMediaMetadata();
|
||||
|
||||
const commonHeaders = {
|
||||
'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'
|
||||
};
|
||||
|
||||
const qrCodeUrl = new URL(qrCodeUrlStr);
|
||||
const shortlinkId = qrCodeUrl.searchParams.get('sl');
|
||||
if (!shortlinkId) throw `shortlink id not found in qr code url`;
|
||||
|
||||
const res = await fetch(`${BASE_URL}/shortlinks/${shortlinkId}`, {
|
||||
headers: commonHeaders
|
||||
});
|
||||
if (!res.ok)
|
||||
`failed to get shortlink (status=${res.status}, body=${JSON.stringify(await res.text())})`;
|
||||
|
||||
const data = await res.json();
|
||||
const originalUrl = new URL(data.Item.original_url.S.replace('#', ''));
|
||||
const token = originalUrl.searchParams.get('token');
|
||||
if (!token) throw `token not found in original url`;
|
||||
|
||||
const parts = token.split('.');
|
||||
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
|
||||
})
|
||||
});
|
||||
|
||||
if (!sessionRes.ok)
|
||||
throw `failed to generate new session (status=${sessionRes.status}, body=${JSON.stringify(await sessionRes.text())})`;
|
||||
|
||||
const sessionData = await sessionRes.json();
|
||||
const generateBoundingBox = () => {
|
||||
const topLeft = [randomFloat(140, 160), randomFloat(250, 270)];
|
||||
const width = randomFloat(170, 190);
|
||||
const height = randomFloat(220, 240);
|
||||
return {
|
||||
topLeft,
|
||||
bottomRight: [topLeft[0] + width, topLeft[1] + height],
|
||||
width,
|
||||
height
|
||||
};
|
||||
};
|
||||
|
||||
const generateTimeline = (maxTime: number) => {
|
||||
const entries = [];
|
||||
for (let i = 0; i < randomInt(2, 5); i++) {
|
||||
const start = randomInt(1, maxTime - 100);
|
||||
const end = start + randomInt(50, 500);
|
||||
if (end < maxTime) entries.push([start, end]);
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
|
||||
const generateStateTimelines = (completionTime: number) => {
|
||||
const states = [
|
||||
'TIME_UNTIL_CLICK_START',
|
||||
'GET_READY',
|
||||
'NO_FACE',
|
||||
'LOOK_STRAIGHT',
|
||||
'TURN_LEFT',
|
||||
'CENTRE_FACE',
|
||||
'KEEP_YOUR_MOUTH_OPEN',
|
||||
'CLOSE_YOUR_MOUTH',
|
||||
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
|
||||
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
|
||||
'TOO_DARK'
|
||||
];
|
||||
const timelines: Record<string, number[][]> = {};
|
||||
states.forEach((state) => (timelines[state] = generateTimeline(completionTime)));
|
||||
return timelines;
|
||||
};
|
||||
const baseAge = randomFloat(25.2, 26.0);
|
||||
const minAge = baseAge - randomFloat(0.1, 0.5);
|
||||
const maxAge = baseAge + randomFloat(0.1, 0.5);
|
||||
const averageAge = (minAge + maxAge) / 2;
|
||||
const currentTime = Date.now() / 1000;
|
||||
const completionTime = randomInt(8000, 15000);
|
||||
|
||||
const raws = Array.from({ length: 10 }, () => randomFloat(6.005, 7.007));
|
||||
const primaryOutputs = removeOutliersWithZscore(raws.map((r) => amap(r)));
|
||||
const outputs = removeOutliersWithZscore(primaryOutputs);
|
||||
|
||||
let payload = {
|
||||
request_type: 'complete_transaction',
|
||||
transaction_id: sessionData.transaction_id,
|
||||
api_key: sessionData.session_id,
|
||||
api_secret: sessionData.session_password,
|
||||
remote_pld: {},
|
||||
browser_response_data: {
|
||||
age: 'yes',
|
||||
age_confidence: 1,
|
||||
genuineness: Array.from({ length: 5 }, () => randomFloat(0.4, 0.98)),
|
||||
product: 'age',
|
||||
modality: 'image',
|
||||
unverifiedPayload: {
|
||||
iss: 'https://api.privately.swiss',
|
||||
sub: '1024',
|
||||
aud: 'https://api.k-id.com',
|
||||
exp: jwtPayload.exp,
|
||||
nbf: jwtPayload.nbf,
|
||||
iat: jwtPayload.iat,
|
||||
jti: jwtPayload.jti,
|
||||
age: jwtPayload.age,
|
||||
liv: true,
|
||||
rlt: {
|
||||
minAge,
|
||||
maxAge,
|
||||
score: 0,
|
||||
gate: 16
|
||||
},
|
||||
rsn: 'complete_transaction',
|
||||
rtf: 'interval',
|
||||
rtb: 'callback',
|
||||
vid: jwtPayload.vid,
|
||||
ver: 'v1.10.22',
|
||||
ufi: []
|
||||
},
|
||||
ageCheckSession: '-' + randomInt(1000000000, 9999999999),
|
||||
miscellaneous: {
|
||||
recordedOpennessStreak: Array.from({ length: 5 }, () => randomFloat(0.1, 0.8, 17)),
|
||||
recordedSpeeds: Array.from({ length: 5 }, () => randomFloat(0.2, 1.5, 17)),
|
||||
recordedIntervals: Array.from({ length: 5 }, () => randomFloat(0.1, 0.2, 3)),
|
||||
failedOpennessReadings: [],
|
||||
failedOpennessSpeeds: [],
|
||||
failedOpennessIntervals: [],
|
||||
numberOfGestureRetries: 1,
|
||||
antiSpoofConfidences: [],
|
||||
fp_scores: [],
|
||||
laplacian_blur_scores: Array.from({ length: 300 }, () => randomFloat(10, 300, 15)),
|
||||
laplacian_min_score: randomFloat(10, 50),
|
||||
laplacian_max_score: randomFloat(300, 350),
|
||||
laplacian_avg_score: randomFloat(50, 100),
|
||||
glare_ratios: Array.from({ length: 300 }, () => 0),
|
||||
allScreenDetectionDetails: {
|
||||
beforeClickingStart: {
|
||||
screenDetectionConfidence: [],
|
||||
screenFaceOverlap: [],
|
||||
screenBoundingBoxes: [],
|
||||
alternativeScore: []
|
||||
},
|
||||
positioning: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [[], []],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
liveness: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [[], []],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
distancing: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [
|
||||
[],
|
||||
[
|
||||
{
|
||||
x: 1,
|
||||
y: 60,
|
||||
width: 158,
|
||||
height: 335
|
||||
}
|
||||
]
|
||||
],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
closing: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [
|
||||
[
|
||||
{
|
||||
x: 370,
|
||||
y: 233,
|
||||
width: 58,
|
||||
height: 85
|
||||
}
|
||||
],
|
||||
[]
|
||||
],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
postChallenge: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [[], []],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
}
|
||||
},
|
||||
plScores: [],
|
||||
screenDetectionExecutionTimes: {
|
||||
beforeClickingStart: [],
|
||||
positioning: Array.from({ length: 2 }, () => randomFloat(5000, 6000)),
|
||||
liveness: Array.from({ length: 2 }, () => randomFloat(4000, 5000)),
|
||||
distancing: Array.from({ length: 2 }, () => randomFloat(3000, 4000)),
|
||||
closing: Array.from({ length: 2 }, () => randomFloat(2000, 3000)),
|
||||
postChallenge: Array.from({ length: 2 }, () => randomFloat(500, 1500))
|
||||
},
|
||||
landmarkDetectionExecutionTimes: {
|
||||
beforeClickingStart: [],
|
||||
positioning: Array.from({ length: 200 }, () => randomFloat(50, 150)),
|
||||
liveness: Array.from({ length: 50 }, () => randomFloat(50, 110)),
|
||||
distancing: Array.from({ length: 5 }, () => randomFloat(50, 110)),
|
||||
closing: Array.from({ length: 20 }, () => randomFloat(50, 110)),
|
||||
postChallenge: Array.from({ length: 7 }, () => randomFloat(50, 110))
|
||||
},
|
||||
screenAttackMeasure: 0,
|
||||
screenAttackBoundingBox: {},
|
||||
subclient: '1024',
|
||||
verificationID: jwtPayload.vid,
|
||||
version: 'v1.10.22',
|
||||
sdk_path: './face-capture-v1.10.22.js',
|
||||
model_version: 'v.2025.0',
|
||||
cropper_version: 'v.0.0.3',
|
||||
start_time_stamp: currentTime - randomFloat(20, 60),
|
||||
end_time_stamp: currentTime,
|
||||
device_timezone: location.timezone,
|
||||
referring_page: `https://d3ogqhtsivkon3.cloudfront.net/?token=${token}&shi=false&from_qr_scan=true`,
|
||||
parent_page: `https://d3ogqhtsivkon3.cloudfront.net/dynamic_index.html?sl=${jwtPayload.jti}®ion=eu-central-1`,
|
||||
face_confidence_limit: 0.975,
|
||||
multipleFacesDetected: false,
|
||||
targetGate: 18,
|
||||
targetConfidence: 0.9,
|
||||
averageAge,
|
||||
selecedLivenessStyle: 'open',
|
||||
selectedMediaLabel: 'Front Camera',
|
||||
rawImageWidth: 480,
|
||||
rawImageHeight: 640,
|
||||
boundingBoxesInPixels: Array.from({ length: randomInt(5, 10) }, generateBoundingBox),
|
||||
latestReportedState: 'AGE_CHECK_COMPLETE',
|
||||
challengeType: 'distance-open',
|
||||
authenticationCharacteristics: {
|
||||
session_id: sessionData.session_id,
|
||||
session_password: sessionData.session_password,
|
||||
token
|
||||
},
|
||||
deviceCharacteristics: {
|
||||
deviceBrowserModel: userAgent,
|
||||
isMobile: parsedUserAgent.device.type === 'mobile',
|
||||
browserName: parsedUserAgent.browser.name?.toLowerCase() || 'safari',
|
||||
isDeviceBrowserCompatible: true,
|
||||
deviceConnectionSpeedKbps: randomFloat(20000, 500000),
|
||||
deviceRegion: {
|
||||
country: location.country,
|
||||
state: location.state
|
||||
},
|
||||
mediaMetadata: mediaMetadata,
|
||||
platformDetails: {
|
||||
name: parsedUserAgent.browser.name || 'Safari',
|
||||
version: parsedUserAgent.browser.version || '15.0',
|
||||
layout: parsedUserAgent.engine.name || 'WebKit',
|
||||
os: {
|
||||
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
|
||||
family: parsedUserAgent.os.name || 'iOS',
|
||||
version: parsedUserAgent.os.version || '15.0'
|
||||
},
|
||||
description: `${parsedUserAgent.browser.name || 'Safari'} ${
|
||||
parsedUserAgent.browser.version || '15.0'
|
||||
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
|
||||
parsedUserAgent.os.version || '15.0'
|
||||
})`,
|
||||
product: parsedUserAgent.device.model || 'iPhone',
|
||||
manufacturer: parsedUserAgent.device.vendor || 'Apple'
|
||||
},
|
||||
userTriedLandscapeMode: 0,
|
||||
txFinishedInLandscapeMode: false
|
||||
},
|
||||
initializationCharacteristics: {
|
||||
cropperInitTime: 2718,
|
||||
coreInitTime: 6746,
|
||||
pageLoadTime: 602.0999999996275,
|
||||
from_qr_scan: false,
|
||||
blendShapesAvailable: true
|
||||
},
|
||||
executionCharacteristics: {
|
||||
experimentSetup: {
|
||||
experimentType: 'passive-liveness-override',
|
||||
experimentProbability: 1,
|
||||
deviceCoverage: 'all',
|
||||
deviceInfo: {
|
||||
name: parsedUserAgent.browser.name || 'Safari',
|
||||
version: parsedUserAgent.browser.version || '15.0',
|
||||
layout: parsedUserAgent.engine.name || 'WebKit',
|
||||
os: {
|
||||
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
|
||||
family: parsedUserAgent.os.name || 'iOS',
|
||||
version: parsedUserAgent.os.version || '15.0'
|
||||
},
|
||||
description: `${parsedUserAgent.browser.name || 'Safari'} ${
|
||||
parsedUserAgent.browser.version || '15.0'
|
||||
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
|
||||
parsedUserAgent.os.version || '15.0'
|
||||
})`,
|
||||
product: parsedUserAgent.device.model || 'iPhone',
|
||||
manufacturer: parsedUserAgent.device.vendor || 'Apple'
|
||||
},
|
||||
txMode: 'experiment',
|
||||
timestamp: new Date().getTime()
|
||||
},
|
||||
experimentConfigResult: {
|
||||
success: true,
|
||||
txMode: 'experiment',
|
||||
experimentType: 'passive-liveness-override'
|
||||
},
|
||||
isCameraPermissionGranted: true,
|
||||
completionTime,
|
||||
deferredComputationStartedAt: randomInt(10000, 14000),
|
||||
instructionCompletionTime: randomInt(10000, 14000),
|
||||
initialAdjustmentTime: randomInt(10000, 14000),
|
||||
completionState: 'COMPLETE',
|
||||
unfinishedInstructions: Object.fromEntries(
|
||||
[
|
||||
'NO_FACE',
|
||||
'VIDEO_PROCESSING',
|
||||
'STAY_STILL',
|
||||
'LOOK_STRAIGHT',
|
||||
'GET_READY',
|
||||
'TURN_LEFT',
|
||||
'TURN_RIGHT',
|
||||
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_UP',
|
||||
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_DOWN',
|
||||
'SLIGHTLY_TILT_YOUR_HEAD_LEFT',
|
||||
'SLIGHTLY_TILT_YOUR_HEAD_RIGHT',
|
||||
'CENTRE_FACE',
|
||||
'OPEN_YOUR_MOUTH',
|
||||
'KEEP_YOUR_MOUTH_OPEN',
|
||||
'CLOSE_YOUR_MOUTH',
|
||||
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
|
||||
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
|
||||
'TOO_DARK'
|
||||
].map((k) => [k, false])
|
||||
),
|
||||
// "stateCompletionTimes": {
|
||||
// "TIME_UNTIL_CLICK_START": 2342,
|
||||
// "GET_READY": 1130,
|
||||
// "NO_FACE": 7132,
|
||||
// "CENTRE_FACE": 551,
|
||||
// "TOO_DARK": 29998,
|
||||
// "TURN_LEFT": 30133,
|
||||
// "LOOK_STRAIGHT": 441,
|
||||
// "SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA": 0,
|
||||
// "SLOWLY_COME_CLOSER_TO_THE_CAMERA": 1546,
|
||||
// "CLOSE_YOUR_MOUTH": 1813,
|
||||
// "KEEP_YOUR_MOUTH_OPEN": 9686
|
||||
// },
|
||||
stateCompletionTimes: {
|
||||
TIME_UNTIL_CLICK_START: randomInt(800, 3200),
|
||||
GET_READY: randomInt(1200, 4000),
|
||||
NO_FACE: randomInt(2000, 5500),
|
||||
CENTRE_FACE: randomInt(8000, 18000),
|
||||
TOO_DARK: randomInt(1500, 3500),
|
||||
TURN_LEFT: randomInt(3000, 8500),
|
||||
LOOK_STRAIGHT: randomInt(100, 800),
|
||||
SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA: 0,
|
||||
SLOWLY_COME_CLOSER_TO_THE_CAMERA: randomInt(800, 3500),
|
||||
CLOSE_YOUR_MOUTH: randomInt(1800, 5200),
|
||||
KEEP_YOUR_MOUTH_OPEN: randomInt(3500, 9000)
|
||||
},
|
||||
stateTimelines: generateStateTimelines(completionTime),
|
||||
nonNeutralExpressionTimelines: Object.fromEntries(
|
||||
[
|
||||
'browDownLeft',
|
||||
'browDownRight',
|
||||
'mouthSmileLeft',
|
||||
'mouthSmileRight',
|
||||
'mouthPucker',
|
||||
'mouthDimpleLeft',
|
||||
'mouthDimpleRight',
|
||||
'mouthPressLeft',
|
||||
'mouthPressRight',
|
||||
'mouthShrugLower',
|
||||
'mouthShrugUpper',
|
||||
'eyeBlinkLeft',
|
||||
'eyeBlinkRight',
|
||||
'mouthFrownLeft',
|
||||
'mouthFrownRight'
|
||||
].map((k) => [k, {}])
|
||||
),
|
||||
handAnalysis: {
|
||||
faceHandSizeComparisons: []
|
||||
},
|
||||
predictions: {
|
||||
outputs,
|
||||
primaryOutputs,
|
||||
raws,
|
||||
secondaryOutputs: [],
|
||||
secondaryRaws: [],
|
||||
age: 'yes',
|
||||
horizontal_estimates: Array.from({ length: 6 }, () => randomFloat(3.1, 3.2)),
|
||||
vertical_estimates: Array.from({ length: 6 }, () => randomFloat(-1.6, -1.5)),
|
||||
horizontalratiotocenter_estimates: Array.from({ length: 6 }, () =>
|
||||
randomFloat(1.01, 1.03)
|
||||
),
|
||||
zy_estimates: Array.from({ length: 6 }, () => randomFloat(0.42, 0.44)),
|
||||
driftfromcenterx_estimates: Array.from({ length: 6 }, () => randomFloat(0.005, 0.007)),
|
||||
driftfromcentery_estimates: Array.from({ length: 6 }, () => randomFloat(-0.35, -0.37)),
|
||||
xScaledShiftAmt: 11.5,
|
||||
yScaledShiftAmt: -2
|
||||
}
|
||||
},
|
||||
errorCharacteristics: {
|
||||
systemErrors: [],
|
||||
userErrors: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
if (!completeRes.ok)
|
||||
throw `failed to complete transaction (status=${completeRes.status}, body=${JSON.stringify(await completeRes.text())})`;
|
||||
|
||||
console.log('tx completed', payload.transaction_id);
|
||||
}
|
||||
|
||||
await verify(qrCodeUrl);
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Platform {
|
||||
env: Env;
|
||||
ctx: ExecutionContext;
|
||||
caches: CacheStorage;
|
||||
cf?: IncomingRequestCfProperties;
|
||||
}
|
||||
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body class="bg-black font-mono text-white" data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
@@ -0,0 +1,153 @@
|
||||
<svelte:head>
|
||||
<title>discord age verifier</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto w-screen max-w-6xl items-center p-5 pb-16">
|
||||
<h1 class="mt-16 text-center text-3xl font-extrabold">discord age verifier</h1>
|
||||
<p class="text-center">age verifies your discord account automatically as an adult</p>
|
||||
<p class="mt-4 text-center text-white/50">
|
||||
made by <a class="underline" href="https://eva.ac" target="_blank">xyzeva</a> and
|
||||
<a class="underline" href="https://github.com/Dziurwa14" target="_blank">Dziurwa</a>, greetz to
|
||||
<a class="underline" href="https://amplitudes.me/" target="_blank">amplitudes</a> (for previous work)
|
||||
</p>
|
||||
|
||||
<p class="mt-8 text-left">
|
||||
it <span class="font-bold">doesn't matter</span> if you are in the UK or similar region that
|
||||
currently has access to this, this will verify your account for the future global rollout in
|
||||
march aswell as current. to use, simply paste this script into your discord console by going to
|
||||
<a
|
||||
class="font-bold underline"
|
||||
href="https://discord.com/app"
|
||||
target="_blank"
|
||||
rel="noreferer noopener">discord.com/app</a
|
||||
>, pressing <span class="font-bold">F12</span>, going to <span class="font-bold">Console</span>
|
||||
and copying and pasting and hitting enter on the following script and solving the captcha that pops
|
||||
up
|
||||
<span class="text-white/70">(typing "allow pasting" before if necessary)</span>:
|
||||
</p>
|
||||
<pre class="my-4 bg-white/10 p-5 text-wrap">// add a chunk to get all of the webpack chunks
|
||||
_mods = webpackChunkdiscord_app.push([[Symbol()],{},r=>r.c]);
|
||||
webpackChunkdiscord_app.pop(); // cleanup the chunk we added
|
||||
|
||||
// utitility to find a webpack chunk by property
|
||||
findByProps = (...props) => {
|
||||
for (let m of Object.values(_mods)) {
|
||||
try {
|
||||
if (!m.exports || m.exports === window) continue;
|
||||
if (props.every((x) => m.exports?.[x])) return m.exports;
|
||||
|
||||
for (let ex in m.exports) {
|
||||
if (props.every((x) => m.exports?.[ex]?.[x]) && m.exports[ex][Symbol.toStringTag] !== 'IntlMessagesProxy') return m.exports[ex];
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// find the discord api client
|
||||
api = findByProps('Bo','oh').Bo
|
||||
|
||||
// send a api request to discord /age-verification/verify and then redirect the page to our website
|
||||
window.location.href = `https://discord-verifier.eva.ac/webview?url=${encodeURIComponent((await api.post({ url: '/age-verification/verify', body: { method: 3 }})).body.verification_webview_url)}`</pre>
|
||||
<p class="text-center text-white/50">
|
||||
(feel free to read the code, we made it readable and we have nothing to hide)
|
||||
</p>
|
||||
<p class="mt-4 text-left">
|
||||
it should navigate to a link <span class="text-white/70"
|
||||
>(or give you a link to navigate to)</span
|
||||
>, from there, you can just wait until the page says success
|
||||
</p>
|
||||
<p class="mt-2 text-left">congrats! your discord account is now age verified.</p>
|
||||
|
||||
<h2 class="mt-12 text-2xl font-bold">how does this work</h2>
|
||||
<p>
|
||||
k-id, the age verification provider discord uses doesn't store or send your face to the server.
|
||||
instead, it sends a bunch of metadata about your face and general process details. while this is
|
||||
good for your privacy <span class="text-white/50"
|
||||
>(well, considering some other providers send actual videos of your face to their servers)</span
|
||||
>, its also bad for them, because we can just send legitimate looking metadata to their servers
|
||||
and they have no way to tell its not legitimate.
|
||||
<br />
|
||||
while this was easy in the past, k-id's partner for face verification (faceassure) has made this significantly
|
||||
harder to achieve after
|
||||
<a href="https://github.com/amplitudesxd/discord-k-id-verifier" class="underline"
|
||||
>amplitudes k-id verifier</a
|
||||
>
|
||||
was released,
|
||||
<span class="text-white/50">(which doesn't work anymore because of it.)</span>
|
||||
<br />
|
||||
<br />
|
||||
with discord's decision of making the age verification requirement global, we decided to look into
|
||||
it again to see if we can bypass the new checks.
|
||||
</p>
|
||||
<h3 class="mt-8 text-xl font-bold">step 1: encrypted_payload and auth_tag</h3>
|
||||
|
||||
<p>
|
||||
the first thing we noticed that the old implementation doesn't send when comparing a legitimate
|
||||
request payload with a generated one, is its missing <code class="bg-white/20 text-blue-400"
|
||||
>encrypted_payload</code
|
||||
>, <code class="bg-white/20 text-blue-400">auth_tag</code>,
|
||||
<code class="bg-white/20 text-blue-400">timestamp</code>
|
||||
and
|
||||
<code class="bg-white/20 text-blue-400">iv</code> in the body.
|
||||
<br />
|
||||
<br />
|
||||
looking at the code, this appears to be a simple AES-GCM cipher with the key being
|
||||
<code class="bg-white/20"
|
||||
><span class="text-orange-300">nonce</span> + <span class="text-orange-300">timestamp</span> +
|
||||
<span class="text-orange-300">transaction_id</span></code
|
||||
>, derived using HKDF (sha256). we can easily replicate this and also create the missing
|
||||
parameters in our generated output.
|
||||
</p>
|
||||
<h3 class="mt-8 text-xl font-bold">step 2: prediction data</h3>
|
||||
<p>
|
||||
heres where it kind of gets tricky, even after perfectly replicating the encryption, our
|
||||
verification attempt still doesn't succeed, so they must also be doing checks on the actual
|
||||
payload.
|
||||
<br />
|
||||
<br />
|
||||
after some trial and error, we narrowed the checked part to the prediction arrays, which are
|
||||
<code class="bg-white/20 text-blue-400">outputs</code>,
|
||||
<code class="bg-white/20 text-blue-400">primaryOutputs</code>
|
||||
and <code class="bg-white/20 text-blue-400">raws</code>.
|
||||
<br />
|
||||
<br />
|
||||
turns out, both <code class="bg-white/20 text-blue-400">outputs</code> and
|
||||
<code class="bg-white/20 text-blue-400">primaryOutputs</code>
|
||||
are generated from <code class="bg-white/20 text-blue-400">raws</code>. basically, the raw
|
||||
numbers are mapped to age outputs, and then the outliers get removed with z-score (once for
|
||||
<code class="bg-white/20 text-blue-400">primaryOutputs</code>
|
||||
and twice for <code class="bg-white/20 text-blue-400">outputs</code>).
|
||||
<br />
|
||||
<br />
|
||||
there is also some other differences:
|
||||
</p>
|
||||
<ul
|
||||
class="list-inside list-disc
|
||||
"
|
||||
>
|
||||
<li>
|
||||
<code class="bg-white/20 text-blue-400">xScaledShiftAmt</code> and
|
||||
<code class="bg-white/20 text-blue-400">yScaledShiftAmt</code> in predictions are not random but
|
||||
rather can be one of two values
|
||||
</li>
|
||||
<li>
|
||||
it is checked that the media name (camera) matches one of your media devices in the array of
|
||||
devices
|
||||
</li>
|
||||
<li>it is checked if the states completion times match the state timeline</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<br />
|
||||
with all of that done,
|
||||
<span class="font-extrabold">we can officially verify our age as an adult.</span> all of this
|
||||
code is open source and available
|
||||
<a
|
||||
href="https://github.com/xyzeva/discord-age-verifier"
|
||||
target="_blank"
|
||||
class="font-extrabold underline"
|
||||
rel="noreferer noopener">on github</a
|
||||
>, so you can actually see how we do this exactly.
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,794 @@
|
||||
import type { RequestEvent } from './$types';
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
const BASE_URL = 'https://eu-west-1.faceassure.com';
|
||||
const K_ID_DEPLOYMENT_ID = '20260210222654-016f063-production';
|
||||
const K_ID_TRACK_ACTION = '601d02be14a2f7e7e50a862f6a0585c9b64d928a84';
|
||||
const K_ID_UPGRADE_TOKEN_ACTION = '40f05c36f2421425f977611b87af923acca2feaaf7';
|
||||
const K_ID_PRIVATELY_ACTION_ID = '40dc500368168e3130ea4625c535d5a9bbbf0243f1';
|
||||
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 jsonResponse = (body: unknown, status: number = 200, extraHeaders: object = {}) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...extraHeaders
|
||||
}
|
||||
});
|
||||
|
||||
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
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)];
|
||||
|
||||
function generateUserAgent() {
|
||||
const agents = [
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/117.0',
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36'
|
||||
];
|
||||
return agents[Math.floor(Math.random() * agents.length)];
|
||||
}
|
||||
|
||||
function parseUserAgent(userAgent: string) {
|
||||
const isIOS = /iPhone|iPad/.test(userAgent);
|
||||
const isAndroid = /Android/.test(userAgent);
|
||||
const isSafari = /Safari/.test(userAgent) && !/Chrome/.test(userAgent);
|
||||
const isChrome = /Chrome/.test(userAgent);
|
||||
const isFirefox = /Firefox/.test(userAgent);
|
||||
|
||||
return {
|
||||
browser: {
|
||||
name: isSafari ? 'Safari' : isChrome ? 'Chrome' : isFirefox ? 'Firefox' : 'Safari',
|
||||
version: isIOS ? '17.0' : '117.0'
|
||||
},
|
||||
device: {
|
||||
type: 'mobile',
|
||||
vendor: isIOS ? 'Apple' : 'Samsung',
|
||||
model: isIOS ? 'iPhone' : 'Galaxy'
|
||||
},
|
||||
os: {
|
||||
name: isIOS ? 'iOS' : isAndroid ? 'Android' : 'iOS',
|
||||
version: isIOS ? '17.0' : '13'
|
||||
},
|
||||
engine: { name: isSafari || isIOS ? 'WebKit' : 'Blink' },
|
||||
cpu: { architecture: '64' }
|
||||
};
|
||||
}
|
||||
|
||||
function getRandomLocation() {
|
||||
const locations = [
|
||||
{
|
||||
country: 'United States',
|
||||
state: 'California',
|
||||
timezone: 'America/Los_Angeles',
|
||||
lang: 'en-US,en;q=0.9'
|
||||
},
|
||||
{
|
||||
country: 'United States',
|
||||
state: 'New York',
|
||||
timezone: 'America/New_York',
|
||||
lang: 'en-US,en;q=0.9'
|
||||
},
|
||||
{
|
||||
country: 'Canada',
|
||||
state: 'Ontario',
|
||||
timezone: 'America/Toronto',
|
||||
lang: 'en-CA,en;q=0.9,fr;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Australia',
|
||||
state: 'New South Wales',
|
||||
timezone: 'Australia/Sydney',
|
||||
lang: 'en-AU,en;q=0.9'
|
||||
},
|
||||
{
|
||||
country: 'Germany',
|
||||
state: 'Berlin',
|
||||
timezone: 'Europe/Berlin',
|
||||
lang: 'de-DE,de;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'France',
|
||||
state: 'Île-de-France',
|
||||
timezone: 'Europe/Paris',
|
||||
lang: 'fr-FR,fr;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Netherlands',
|
||||
state: 'North Holland',
|
||||
timezone: 'Europe/Amsterdam',
|
||||
lang: 'nl-NL,nl;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Sweden',
|
||||
state: 'Stockholm',
|
||||
timezone: 'Europe/Stockholm',
|
||||
lang: 'sv-SE,sv;q=0.9,en;q=0.8'
|
||||
},
|
||||
{
|
||||
country: 'Norway',
|
||||
state: 'Oslo',
|
||||
timezone: 'Europe/Oslo',
|
||||
lang: 'nb-NO,nb;q=0.9,en;q=0.8'
|
||||
}
|
||||
];
|
||||
return locations[Math.floor(Math.random() * locations.length)];
|
||||
}
|
||||
|
||||
function generateMediaMetadata() {
|
||||
const randomHex = () =>
|
||||
Array.from({ length: 32 }, () =>
|
||||
Math.floor(Math.random() * 16)
|
||||
.toString(16)
|
||||
.toUpperCase()
|
||||
).join('');
|
||||
|
||||
const specs = [
|
||||
{
|
||||
width: 4032,
|
||||
height: 3024,
|
||||
frameRate: 60,
|
||||
zoom: 10,
|
||||
aspectRatio: 4032 / 3024
|
||||
},
|
||||
{
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
frameRate: 60,
|
||||
zoom: 8,
|
||||
aspectRatio: 3840 / 2160
|
||||
},
|
||||
{
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frameRate: 120,
|
||||
zoom: 5,
|
||||
aspectRatio: 1920 / 1080
|
||||
},
|
||||
{
|
||||
width: 2560,
|
||||
height: 1440,
|
||||
frameRate: 60,
|
||||
zoom: 6,
|
||||
aspectRatio: 2560 / 1440
|
||||
}
|
||||
];
|
||||
|
||||
const spec = randomChoice(specs);
|
||||
const deviceId = randomHex();
|
||||
|
||||
return [
|
||||
{
|
||||
mediaKind: 'audioinput',
|
||||
mediaLabel: randomChoice(['', 'Built-in Microphone', 'Default']),
|
||||
mediaId: randomHex(),
|
||||
mediaCapabilities: {}
|
||||
},
|
||||
{
|
||||
mediaKind: 'videoinput',
|
||||
mediaLabel: 'Front Camera',
|
||||
mediaId: deviceId,
|
||||
mediaCapabilities: {
|
||||
aspectRatio: {
|
||||
max: spec.aspectRatio,
|
||||
min: randomFloat(0.0003, 0.001, 15)
|
||||
},
|
||||
backgroundBlur: [false],
|
||||
deviceId: deviceId,
|
||||
facingMode: ['user'],
|
||||
focusDistance: { min: randomFloat(0.1, 0.3) },
|
||||
frameRate: { max: spec.frameRate, min: 1 },
|
||||
groupId: randomHex(),
|
||||
height: { max: spec.height, min: 1 },
|
||||
powerEfficient: [false, true],
|
||||
whiteBalanceMode: randomChoice([
|
||||
['manual', 'continuous'],
|
||||
['auto', 'manual'],
|
||||
['continuous']
|
||||
]),
|
||||
width: { max: spec.width, min: 1 },
|
||||
zoom: { max: spec.zoom, min: 1 }
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const AMAP_MAP: Record<number, [number, number]> = {
|
||||
0: [0, 2],
|
||||
1: [2, 4],
|
||||
2: [4, 8],
|
||||
3: [8, 13],
|
||||
4: [13, 18],
|
||||
5: [18, 21],
|
||||
6: [21, 25],
|
||||
7: [25, 28],
|
||||
8: [28, 32],
|
||||
9: [32, 36],
|
||||
10: [36, 40],
|
||||
11: [40, 45],
|
||||
12: [45, 50],
|
||||
13: [50, 60],
|
||||
14: [60, 70],
|
||||
15: [70, 120]
|
||||
};
|
||||
|
||||
function amap(e: number) {
|
||||
const n = AMAP_MAP[~~e];
|
||||
const r = e % 1;
|
||||
return n[0] + r * (n[1] - n[0]);
|
||||
}
|
||||
|
||||
function removeOutliersWithZscore(arr: number[]) {
|
||||
const r =
|
||||
arr.reduce(function (e, t) {
|
||||
return e + t;
|
||||
}, 0) / arr.length;
|
||||
const a =
|
||||
arr.reduce(function (e, t) {
|
||||
return e + Math.pow(t - r, 2);
|
||||
}, 0) / arr.length;
|
||||
const s = Math.sqrt(a);
|
||||
return arr.filter(function (e) {
|
||||
return Math.abs((e - r) / s) <= 1;
|
||||
});
|
||||
}
|
||||
|
||||
async function encryptPayload(nonce: string, payload: any) {
|
||||
const getKey = async (nonce: string, timestamp: string, transactionId: string) => {
|
||||
const data = nonce + timestamp + transactionId;
|
||||
const dataEncoded = new TextEncoder().encode(data);
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
dataEncoded,
|
||||
{
|
||||
name: 'HKDF'
|
||||
},
|
||||
false,
|
||||
['deriveBits']
|
||||
);
|
||||
const derived = await crypto.subtle.deriveBits(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-256',
|
||||
salt: new Uint8Array(0),
|
||||
info: new TextEncoder().encode('payload-encryption')
|
||||
},
|
||||
key,
|
||||
32 * 8
|
||||
);
|
||||
|
||||
return await crypto.subtle.importKey(
|
||||
'raw',
|
||||
derived,
|
||||
{
|
||||
name: 'AES-GCM'
|
||||
},
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
};
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const key = await getKey(nonce, timestamp, payload.transaction_id);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
},
|
||||
key,
|
||||
new TextEncoder().encode(JSON.stringify(payload))
|
||||
);
|
||||
|
||||
const rawBuffer = Buffer.from(encryptedBuffer);
|
||||
const encryptedPayloadBuf = rawBuffer.subarray(0, rawBuffer.length - 16);
|
||||
const authTagBuf = rawBuffer.subarray(rawBuffer.length - 16);
|
||||
|
||||
return {
|
||||
encrypted_payload: encryptedPayloadBuf.toString('base64'),
|
||||
iv: Buffer.from(iv).toString('base64'),
|
||||
auth_tag: authTagBuf.toString('base64'),
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
async function verify(
|
||||
userAgent: string,
|
||||
location: ReturnType<typeof getRandomLocation>,
|
||||
token: string
|
||||
) {
|
||||
const parsedUserAgent = parseUserAgent(userAgent);
|
||||
const mediaMetadata = generateMediaMetadata();
|
||||
|
||||
const commonHeaders = {
|
||||
'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'
|
||||
};
|
||||
|
||||
/*
|
||||
const qrCodeUrl = new URL(qrCodeUrlStr);
|
||||
const shortlinkId = qrCodeUrl.searchParams.get('sl');
|
||||
if (!shortlinkId) throw `shortlink id not found in qr code url`;
|
||||
|
||||
const res = await fetch(`${BASE_URL}/shortlinks/${shortlinkId}`, {
|
||||
headers: commonHeaders
|
||||
});
|
||||
if (!res.ok)
|
||||
`failed to get shortlink (status=${res.status}, body=${JSON.stringify(await res.text())})`;
|
||||
|
||||
const data = await res.json();
|
||||
const originalUrl = new URL(data.Item.original_url.S.replace('#', ''));
|
||||
const token = originalUrl.searchParams.get('token');
|
||||
if (!token) throw `token not found in original url`;
|
||||
*/
|
||||
|
||||
const parts = token.split('.');
|
||||
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
|
||||
})
|
||||
});
|
||||
|
||||
if (!sessionRes.ok)
|
||||
throw `failed to generate new session (status=${sessionRes.status}, body=${JSON.stringify(await sessionRes.text())})`;
|
||||
|
||||
const sessionData = await sessionRes.json();
|
||||
const generateBoundingBox = () => {
|
||||
const topLeft = [randomFloat(140, 160), randomFloat(250, 270)];
|
||||
const width = randomFloat(170, 190);
|
||||
const height = randomFloat(220, 240);
|
||||
return {
|
||||
topLeft,
|
||||
bottomRight: [topLeft[0] + width, topLeft[1] + height],
|
||||
width,
|
||||
height
|
||||
};
|
||||
};
|
||||
|
||||
const generateTimeline = (maxTime: number) => {
|
||||
const entries = [];
|
||||
for (let i = 0; i < randomInt(2, 5); i++) {
|
||||
const start = randomInt(1, maxTime - 100);
|
||||
const end = start + randomInt(50, 500);
|
||||
if (end < maxTime) entries.push([start, end]);
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
|
||||
const generateStateTimelines = (completionTime: number) => {
|
||||
const states = [
|
||||
'TIME_UNTIL_CLICK_START',
|
||||
'GET_READY',
|
||||
'NO_FACE',
|
||||
'LOOK_STRAIGHT',
|
||||
'TURN_LEFT',
|
||||
'CENTRE_FACE',
|
||||
'KEEP_YOUR_MOUTH_OPEN',
|
||||
'CLOSE_YOUR_MOUTH',
|
||||
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
|
||||
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
|
||||
'TOO_DARK'
|
||||
];
|
||||
const timelines: Record<string, number[][]> = {};
|
||||
states.forEach((state) => (timelines[state] = generateTimeline(completionTime)));
|
||||
return timelines;
|
||||
};
|
||||
const baseAge = randomFloat(25.2, 26.0);
|
||||
const minAge = baseAge - randomFloat(0.1, 0.5);
|
||||
const maxAge = baseAge + randomFloat(0.1, 0.5);
|
||||
const averageAge = (minAge + maxAge) / 2;
|
||||
const currentTime = Date.now() / 1000;
|
||||
const completionTime = randomInt(8000, 15000);
|
||||
|
||||
const raws = Array.from({ length: 10 }, () => randomFloat(6.005, 7.007));
|
||||
const primaryOutputs = removeOutliersWithZscore(raws.map((r) => amap(r)));
|
||||
const outputs = removeOutliersWithZscore(primaryOutputs);
|
||||
|
||||
let payload = {
|
||||
request_type: 'complete_transaction',
|
||||
transaction_id: sessionData.transaction_id,
|
||||
api_key: sessionData.session_id,
|
||||
api_secret: sessionData.session_password,
|
||||
remote_pld: {},
|
||||
browser_response_data: {
|
||||
age: 'yes',
|
||||
age_confidence: 1,
|
||||
genuineness: Array.from({ length: 5 }, () => randomFloat(0.4, 0.98)),
|
||||
product: 'age',
|
||||
modality: 'image',
|
||||
unverifiedPayload: {
|
||||
iss: 'https://api.privately.swiss',
|
||||
sub: '1024',
|
||||
aud: 'https://api.k-id.com',
|
||||
exp: jwtPayload.exp,
|
||||
nbf: jwtPayload.nbf,
|
||||
iat: jwtPayload.iat,
|
||||
jti: jwtPayload.jti,
|
||||
age: jwtPayload.age,
|
||||
liv: true,
|
||||
rlt: {
|
||||
minAge,
|
||||
maxAge,
|
||||
score: 0,
|
||||
gate: 16
|
||||
},
|
||||
rsn: 'complete_transaction',
|
||||
rtf: 'interval',
|
||||
rtb: 'callback',
|
||||
vid: jwtPayload.vid,
|
||||
ver: 'v1.10.22',
|
||||
ufi: []
|
||||
},
|
||||
ageCheckSession: '-' + randomInt(1000000000, 9999999999),
|
||||
miscellaneous: {
|
||||
recordedOpennessStreak: Array.from({ length: 5 }, () => randomFloat(0.1, 0.8, 17)),
|
||||
recordedSpeeds: Array.from({ length: 5 }, () => randomFloat(0.2, 1.5, 17)),
|
||||
recordedIntervals: Array.from({ length: 5 }, () => randomFloat(0.1, 0.2, 3)),
|
||||
failedOpennessReadings: [],
|
||||
failedOpennessSpeeds: [],
|
||||
failedOpennessIntervals: [],
|
||||
numberOfGestureRetries: 1,
|
||||
antiSpoofConfidences: [],
|
||||
fp_scores: [],
|
||||
laplacian_blur_scores: Array.from({ length: 300 }, () => randomFloat(10, 300, 15)),
|
||||
laplacian_min_score: randomFloat(10, 50),
|
||||
laplacian_max_score: randomFloat(300, 350),
|
||||
laplacian_avg_score: randomFloat(50, 100),
|
||||
glare_ratios: Array.from({ length: 300 }, () => 0),
|
||||
allScreenDetectionDetails: {
|
||||
beforeClickingStart: {
|
||||
screenDetectionConfidence: [],
|
||||
screenFaceOverlap: [],
|
||||
screenBoundingBoxes: [],
|
||||
alternativeScore: []
|
||||
},
|
||||
positioning: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [[], []],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
liveness: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [[], []],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
distancing: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [
|
||||
[],
|
||||
[
|
||||
{
|
||||
x: 1,
|
||||
y: 60,
|
||||
width: 158,
|
||||
height: 335
|
||||
}
|
||||
]
|
||||
],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
closing: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [
|
||||
[
|
||||
{
|
||||
x: 370,
|
||||
y: 233,
|
||||
width: 58,
|
||||
height: 85
|
||||
}
|
||||
],
|
||||
[]
|
||||
],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
},
|
||||
postChallenge: {
|
||||
screenDetectionConfidence: Array.from({ length: 2 }, () => randomFloat(0.01, 0.03)),
|
||||
screenFaceOverlap: [0, 0],
|
||||
screenBoundingBoxes: [[], []],
|
||||
alternativeScore: Array.from({ length: 2 }, () => randomFloat(0.2, 0.9))
|
||||
}
|
||||
},
|
||||
plScores: [],
|
||||
screenDetectionExecutionTimes: {
|
||||
beforeClickingStart: [],
|
||||
positioning: Array.from({ length: 2 }, () => randomFloat(5000, 6000)),
|
||||
liveness: Array.from({ length: 2 }, () => randomFloat(4000, 5000)),
|
||||
distancing: Array.from({ length: 2 }, () => randomFloat(3000, 4000)),
|
||||
closing: Array.from({ length: 2 }, () => randomFloat(2000, 3000)),
|
||||
postChallenge: Array.from({ length: 2 }, () => randomFloat(500, 1500))
|
||||
},
|
||||
landmarkDetectionExecutionTimes: {
|
||||
beforeClickingStart: [],
|
||||
positioning: Array.from({ length: 200 }, () => randomFloat(50, 150)),
|
||||
liveness: Array.from({ length: 50 }, () => randomFloat(50, 110)),
|
||||
distancing: Array.from({ length: 5 }, () => randomFloat(50, 110)),
|
||||
closing: Array.from({ length: 20 }, () => randomFloat(50, 110)),
|
||||
postChallenge: Array.from({ length: 7 }, () => randomFloat(50, 110))
|
||||
},
|
||||
screenAttackMeasure: 0,
|
||||
screenAttackBoundingBox: {},
|
||||
subclient: '1024',
|
||||
verificationID: jwtPayload.vid,
|
||||
version: 'v1.10.22',
|
||||
sdk_path: './face-capture-v1.10.22.js',
|
||||
model_version: 'v.2025.0',
|
||||
cropper_version: 'v.0.0.3',
|
||||
start_time_stamp: currentTime - randomFloat(20, 60),
|
||||
end_time_stamp: currentTime,
|
||||
device_timezone: location.timezone,
|
||||
referring_page: `https://d3ogqhtsivkon3.cloudfront.net/?token=${token}&shi=false&from_qr_scan=true`,
|
||||
parent_page: `https://d3ogqhtsivkon3.cloudfront.net/dynamic_index.html?sl=${jwtPayload.jti}®ion=eu-central-1`,
|
||||
face_confidence_limit: 0.975,
|
||||
multipleFacesDetected: false,
|
||||
targetGate: 18,
|
||||
targetConfidence: 0.9,
|
||||
averageAge,
|
||||
selecedLivenessStyle: 'open',
|
||||
selectedMediaLabel: 'Front Camera',
|
||||
rawImageWidth: 480,
|
||||
rawImageHeight: 640,
|
||||
boundingBoxesInPixels: Array.from({ length: randomInt(5, 10) }, generateBoundingBox),
|
||||
latestReportedState: 'AGE_CHECK_COMPLETE',
|
||||
challengeType: 'distance-open',
|
||||
authenticationCharacteristics: {
|
||||
session_id: sessionData.session_id,
|
||||
session_password: sessionData.session_password,
|
||||
token
|
||||
},
|
||||
deviceCharacteristics: {
|
||||
deviceBrowserModel: userAgent,
|
||||
isMobile: parsedUserAgent.device.type === 'mobile',
|
||||
browserName: parsedUserAgent.browser.name?.toLowerCase() || 'safari',
|
||||
isDeviceBrowserCompatible: true,
|
||||
deviceConnectionSpeedKbps: randomFloat(20000, 500000),
|
||||
deviceRegion: {
|
||||
country: location.country,
|
||||
state: location.state
|
||||
},
|
||||
mediaMetadata: mediaMetadata,
|
||||
platformDetails: {
|
||||
name: parsedUserAgent.browser.name || 'Safari',
|
||||
version: parsedUserAgent.browser.version || '15.0',
|
||||
layout: parsedUserAgent.engine.name || 'WebKit',
|
||||
os: {
|
||||
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
|
||||
family: parsedUserAgent.os.name || 'iOS',
|
||||
version: parsedUserAgent.os.version || '15.0'
|
||||
},
|
||||
description: `${parsedUserAgent.browser.name || 'Safari'} ${
|
||||
parsedUserAgent.browser.version || '15.0'
|
||||
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
|
||||
parsedUserAgent.os.version || '15.0'
|
||||
})`,
|
||||
product: parsedUserAgent.device.model || 'iPhone',
|
||||
manufacturer: parsedUserAgent.device.vendor || 'Apple'
|
||||
},
|
||||
userTriedLandscapeMode: 0,
|
||||
txFinishedInLandscapeMode: false
|
||||
},
|
||||
initializationCharacteristics: {
|
||||
cropperInitTime: 2718,
|
||||
coreInitTime: 6746,
|
||||
pageLoadTime: 602.0999999996275,
|
||||
from_qr_scan: false,
|
||||
blendShapesAvailable: true
|
||||
},
|
||||
executionCharacteristics: {
|
||||
experimentSetup: {
|
||||
experimentType: 'passive-liveness-override',
|
||||
experimentProbability: 1,
|
||||
deviceCoverage: 'all',
|
||||
deviceInfo: {
|
||||
name: parsedUserAgent.browser.name || 'Safari',
|
||||
version: parsedUserAgent.browser.version || '15.0',
|
||||
layout: parsedUserAgent.engine.name || 'WebKit',
|
||||
os: {
|
||||
architecture: parseInt(parsedUserAgent.cpu.architecture) || 64,
|
||||
family: parsedUserAgent.os.name || 'iOS',
|
||||
version: parsedUserAgent.os.version || '15.0'
|
||||
},
|
||||
description: `${parsedUserAgent.browser.name || 'Safari'} ${
|
||||
parsedUserAgent.browser.version || '15.0'
|
||||
} on ${parsedUserAgent.device.vendor || 'Apple'} ${parsedUserAgent.device.model || 'iPhone'} (${parsedUserAgent.os.name || 'iOS'} ${
|
||||
parsedUserAgent.os.version || '15.0'
|
||||
})`,
|
||||
product: parsedUserAgent.device.model || 'iPhone',
|
||||
manufacturer: parsedUserAgent.device.vendor || 'Apple'
|
||||
},
|
||||
txMode: 'experiment',
|
||||
timestamp: new Date().getTime()
|
||||
},
|
||||
experimentConfigResult: {
|
||||
success: true,
|
||||
txMode: 'experiment',
|
||||
experimentType: 'passive-liveness-override'
|
||||
},
|
||||
isCameraPermissionGranted: true,
|
||||
completionTime,
|
||||
deferredComputationStartedAt: randomInt(10000, 14000),
|
||||
instructionCompletionTime: randomInt(10000, 14000),
|
||||
initialAdjustmentTime: randomInt(10000, 14000),
|
||||
completionState: 'COMPLETE',
|
||||
unfinishedInstructions: Object.fromEntries(
|
||||
[
|
||||
'NO_FACE',
|
||||
'VIDEO_PROCESSING',
|
||||
'STAY_STILL',
|
||||
'LOOK_STRAIGHT',
|
||||
'GET_READY',
|
||||
'TURN_LEFT',
|
||||
'TURN_RIGHT',
|
||||
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_UP',
|
||||
'ALIGN_YOUR_FACE_WITH_THE_CAMERA_DOWN',
|
||||
'SLIGHTLY_TILT_YOUR_HEAD_LEFT',
|
||||
'SLIGHTLY_TILT_YOUR_HEAD_RIGHT',
|
||||
'CENTRE_FACE',
|
||||
'OPEN_YOUR_MOUTH',
|
||||
'KEEP_YOUR_MOUTH_OPEN',
|
||||
'CLOSE_YOUR_MOUTH',
|
||||
'SLOWLY_COME_CLOSER_TO_THE_CAMERA',
|
||||
'SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA',
|
||||
'TOO_DARK'
|
||||
].map((k) => [k, false])
|
||||
),
|
||||
// "stateCompletionTimes": {
|
||||
// "TIME_UNTIL_CLICK_START": 2342,
|
||||
// "GET_READY": 1130,
|
||||
// "NO_FACE": 7132,
|
||||
// "CENTRE_FACE": 551,
|
||||
// "TOO_DARK": 29998,
|
||||
// "TURN_LEFT": 30133,
|
||||
// "LOOK_STRAIGHT": 441,
|
||||
// "SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA": 0,
|
||||
// "SLOWLY_COME_CLOSER_TO_THE_CAMERA": 1546,
|
||||
// "CLOSE_YOUR_MOUTH": 1813,
|
||||
// "KEEP_YOUR_MOUTH_OPEN": 9686
|
||||
// },
|
||||
stateCompletionTimes: {
|
||||
TIME_UNTIL_CLICK_START: randomInt(800, 3200),
|
||||
GET_READY: randomInt(1200, 4000),
|
||||
NO_FACE: randomInt(2000, 5500),
|
||||
CENTRE_FACE: randomInt(8000, 18000),
|
||||
TOO_DARK: randomInt(1500, 3500),
|
||||
TURN_LEFT: randomInt(3000, 8500),
|
||||
LOOK_STRAIGHT: randomInt(100, 800),
|
||||
SLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERA: 0,
|
||||
SLOWLY_COME_CLOSER_TO_THE_CAMERA: randomInt(800, 3500),
|
||||
CLOSE_YOUR_MOUTH: randomInt(1800, 5200),
|
||||
KEEP_YOUR_MOUTH_OPEN: randomInt(3500, 9000)
|
||||
},
|
||||
stateTimelines: generateStateTimelines(completionTime),
|
||||
nonNeutralExpressionTimelines: Object.fromEntries(
|
||||
[
|
||||
'browDownLeft',
|
||||
'browDownRight',
|
||||
'mouthSmileLeft',
|
||||
'mouthSmileRight',
|
||||
'mouthPucker',
|
||||
'mouthDimpleLeft',
|
||||
'mouthDimpleRight',
|
||||
'mouthPressLeft',
|
||||
'mouthPressRight',
|
||||
'mouthShrugLower',
|
||||
'mouthShrugUpper',
|
||||
'eyeBlinkLeft',
|
||||
'eyeBlinkRight',
|
||||
'mouthFrownLeft',
|
||||
'mouthFrownRight'
|
||||
].map((k) => [k, {}])
|
||||
),
|
||||
handAnalysis: {
|
||||
faceHandSizeComparisons: []
|
||||
},
|
||||
predictions: {
|
||||
outputs,
|
||||
primaryOutputs,
|
||||
raws,
|
||||
secondaryOutputs: [],
|
||||
secondaryRaws: [],
|
||||
age: 'yes',
|
||||
horizontal_estimates: Array.from({ length: 6 }, () => randomFloat(3.1, 3.2)),
|
||||
vertical_estimates: Array.from({ length: 6 }, () => randomFloat(-1.6, -1.5)),
|
||||
horizontalratiotocenter_estimates: Array.from({ length: 6 }, () =>
|
||||
randomFloat(1.01, 1.03)
|
||||
),
|
||||
zy_estimates: Array.from({ length: 6 }, () => randomFloat(0.42, 0.44)),
|
||||
driftfromcenterx_estimates: Array.from({ length: 6 }, () => randomFloat(0.005, 0.007)),
|
||||
driftfromcentery_estimates: Array.from({ length: 6 }, () => randomFloat(-0.35, -0.37)),
|
||||
xScaledShiftAmt: 11.5,
|
||||
yScaledShiftAmt: -2
|
||||
}
|
||||
},
|
||||
errorCharacteristics: {
|
||||
systemErrors: [],
|
||||
userErrors: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
if (!completeRes.ok)
|
||||
throw `failed to complete transaction (status=${completeRes.status}, body=${JSON.stringify(await completeRes.text())})`;
|
||||
|
||||
console.log('tx completed', payload.transaction_id);
|
||||
}
|
||||
|
||||
export const POST = async (event: RequestEvent) => {
|
||||
const userAgent = generateUserAgent();
|
||||
const location = getRandomLocation();
|
||||
|
||||
const { type, identifier }: { type: string; identifier: string } = await event.request.json();
|
||||
|
||||
if (type === 'webview') {
|
||||
const webviewUrl = new URL(identifier);
|
||||
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]));
|
||||
|
||||
// todo: implement getting a privately token from a k-id token
|
||||
return jsonResponse({ error: 'not implemented' }, 418);
|
||||
}
|
||||
|
||||
return jsonResponse({ error: 'unexpected type' }, 400);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
@import 'tailwindcss';
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const webviewUrl = page.url.searchParams.get('url');
|
||||
|
||||
let success = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
if (!webviewUrl) return;
|
||||
fetch('/api/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ type: 'webview', identifier: webviewUrl })
|
||||
})
|
||||
.then(async (r) => {
|
||||
if (!r.ok) {
|
||||
error = await r.text().catch(() => 'unexpected error');
|
||||
return;
|
||||
}
|
||||
|
||||
success = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('error sending verify request', e);
|
||||
error = 'unexpected error, please check your browser console.';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen w-screen items-center justify-center text-3xl">
|
||||
{#if webviewUrl}
|
||||
{#if success}
|
||||
<p class="text-green-500">your account is now age verified. enjoy</p>
|
||||
{:else if error}
|
||||
<p class="text-red-500">{error}</p>
|
||||
{:else}
|
||||
<p>automatically verifying your age</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>invalid url</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,6 @@
|
||||
import adapter from '@sveltejs/adapter-cloudflare';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = { kit: { adapter: adapter() } };
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["./worker-configuration.d.ts"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "discord-verifier",
|
||||
"compatibility_date": "2026-02-10",
|
||||
"compatibility_flags": ["nodejs_als"],
|
||||
"main": ".svelte-kit/cloudflare/_worker.js",
|
||||
"assets": {
|
||||
"binding": "ASSETS",
|
||||
"directory": ".svelte-kit/cloudflare"
|
||||
},
|
||||
"workers_dev": true,
|
||||
"preview_urls": true
|
||||
}
|
||||
Reference in New Issue
Block a user