mirror of
https://github.com/xyzeva/k-id-age-verifier.git
synced 2026-04-25 05:06:22 +02:00
796 lines
24 KiB
TypeScript
796 lines
24 KiB
TypeScript
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 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++) {
|
|
s = (s << 5) - s + a.charCodeAt(i);
|
|
s &= s;
|
|
}
|
|
return '' + s;
|
|
}
|
|
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(sub: string, sessionId: string) {
|
|
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 = generateId(randomHex(), sub, sessionId, '-');
|
|
|
|
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;
|
|
});
|
|
}
|
|
|
|
function calculateSpeedAndIntervals(measurements: number[], timestamps: number[]) {
|
|
const intervals = [];
|
|
const speeds = [];
|
|
|
|
for (let i = 1; i < measurements.length; i++) {
|
|
const distance = Math.abs(measurements[i] - measurements[i - 1]);
|
|
const timeInterval = (timestamps[i] - timestamps[i - 1]) / 1000;
|
|
intervals.push(timeInterval);
|
|
if (timeInterval > 0) {
|
|
const speed = distance / timeInterval;
|
|
speeds.push(speed);
|
|
} else {
|
|
speeds.push(0);
|
|
}
|
|
}
|
|
return { intervals, speeds };
|
|
}
|
|
|
|
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 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 = [];
|
|
let lastTime = randomInt(1, 5);
|
|
|
|
for (let i = 0; i < randomInt(1, 3); i++) {
|
|
const end = lastTime + randomInt(5, 20);
|
|
if (end < maxTime) {
|
|
entries.push([lastTime, end]);
|
|
lastTime = end + randomInt(20, 100);
|
|
}
|
|
}
|
|
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 noState = [
|
|
'VIDEO_PROCESSING',
|
|
'STAY_STILL',
|
|
'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',
|
|
'OPEN_YOUR_MOUTH'
|
|
];
|
|
const timelines: Record<string, number[][]> = {};
|
|
states.forEach((state) => (timelines[state] = generateTimeline(completionTime)));
|
|
noState.forEach((state) => {
|
|
timelines[state] = [];
|
|
});
|
|
|
|
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);
|
|
|
|
const gestureMeasurementTime = Date.now();
|
|
const recordedMeasurements: number[] = Array.from({ length: 5 }, () => randomFloat(0.1, 0.8, 17));
|
|
const recordedTimestamps: number[] = [
|
|
randomInt(500, Math.min(completionTime, 1000)) - gestureMeasurementTime,
|
|
randomInt(700, Math.min(completionTime, 1000)) - gestureMeasurementTime,
|
|
randomInt(1000, Math.min(completionTime, 1400)) - gestureMeasurementTime,
|
|
randomInt(1400, Math.min(completionTime, 1600)) - gestureMeasurementTime,
|
|
randomInt(1600, Math.min(completionTime, 1800)) - gestureMeasurementTime
|
|
];
|
|
const { speeds: recordedSpeeds, intervals: recordedIntervals } = calculateSpeedAndIntervals(
|
|
recordedMeasurements,
|
|
recordedTimestamps
|
|
);
|
|
|
|
const failedMeasurements: number[] = Array.from({ length: 5 }, () => randomFloat(0.1, 0.8, 17));
|
|
const failedTimestamps: number[] = [
|
|
randomInt(500, Math.min(completionTime, 1000)) - gestureMeasurementTime,
|
|
randomInt(700, Math.min(completionTime, 1000)) - gestureMeasurementTime,
|
|
randomInt(1000, Math.min(completionTime, 1400)) - gestureMeasurementTime,
|
|
randomInt(1400, Math.min(completionTime, 1600)) - gestureMeasurementTime
|
|
];
|
|
const { speeds: failedOpennessSpeeds, intervals: failedOpennessIntervals } =
|
|
calculateSpeedAndIntervals(failedMeasurements, failedTimestamps);
|
|
|
|
const stateTimelines = generateStateTimelines(completionTime);
|
|
const stateCompletionTimes: Record<string, number> = {};
|
|
for (const [key, timeline] of Object.entries(stateTimelines)) {
|
|
if (timeline.length < 1) continue;
|
|
let completionTime = 0;
|
|
for (const time of timeline) {
|
|
completionTime += time.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
|
|
}
|
|
|
|
stateCompletionTimes[key] = completionTime;
|
|
}
|
|
const mediaMetadata = generateMediaMetadata(jwtPayload.sub, sessionData.session_id);
|
|
|
|
const ageCheckSession = generateId(
|
|
mediaMetadata.find((m) => m.mediaKind === 'videoinput')!.mediaId,
|
|
jwtPayload.sub,
|
|
sessionData.session_id
|
|
);
|
|
|
|
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;
|
|
|
|
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: jwtPayload.sub,
|
|
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: ageCheckSession,
|
|
miscellaneous: {
|
|
recordedOpennessStreak: recordedMeasurements,
|
|
recordedSpeeds,
|
|
recordedIntervals,
|
|
failedOpennessReadings: [failedMeasurements],
|
|
failedOpennessSpeeds: [failedOpennessSpeeds],
|
|
failedOpennessIntervals: [failedOpennessIntervals],
|
|
numberOfGestureRetries: 1,
|
|
antiSpoofConfidences: [],
|
|
fp_scores: [],
|
|
laplacian_blur_scores: laplacianBlurScores,
|
|
laplacian_min_score: laplacianMinScore,
|
|
laplacian_max_score: laplacianMaxScore,
|
|
laplacian_avg_score: laplacianAvgScore,
|
|
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: [Array.from({ length: 2 }, generateBoundingBox)],
|
|
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: [Array.from({ length: 2 }, generateBoundingBox)],
|
|
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: jwtPayload.sub,
|
|
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 + Number(Math.random().toFixed(3)),
|
|
end_time_stamp: currentTime + completionTime / 1000,
|
|
device_timezone: location.timezone,
|
|
referring_page: `https://d3ogqhtsivkon3.cloudfront.net/index-v1.10.22.html#/?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: randomInt(150, 250),
|
|
coreInitTime: randomInt(800, 1000),
|
|
pageLoadTime: randomInt(250, 350) + Number(Math.random().toFixed(randomInt(7, 13))),
|
|
from_qr_scan: true,
|
|
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:
|
|
currentTime + randomInt(20, 80) + Number(Math.random().toFixed(randomInt(1, 3))),
|
|
instructionCompletionTime: randomInt(10000, 14000),
|
|
initialAdjustmentTime: randomInt(100, 500),
|
|
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,
|
|
stateTimelines,
|
|
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);
|