@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
217 lines (189 loc) • 5.62 kB
text/typescript
export interface ClientFingerprintData {
canvas: string;
webgl: string;
fonts: string[];
audio: string;
screen: {
width: number;
height: number;
colorDepth: number;
pixelRatio: number;
};
navigator: {
userAgent: string;
language: string;
languages: readonly string[];
platform: string;
hardwareConcurrency: number;
deviceMemory?: number;
maxTouchPoints: number;
};
timezone: {
offset: number;
name: string;
};
features: {
cookies: boolean;
localStorage: boolean;
sessionStorage: boolean;
indexedDB: boolean;
webgl: boolean;
canvas: boolean;
};
}
async function sha256(message: string): Promise<string> {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
function getCanvasFingerprint(): string {
try {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return "";
canvas.width = 200;
canvas.height = 50;
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";
ctx.fillStyle = "#f60";
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = "#069";
ctx.fillText("Kanadi Fingerprint", 2, 15);
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
ctx.fillText("Canvas FP Test", 4, 17);
return canvas.toDataURL();
} catch {
return "";
}
}
function getWebGLFingerprint(): string {
try {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (!gl) return "";
const debugInfo = (gl as any).getExtension("WEBGL_debug_renderer_info");
if (!debugInfo) return "";
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
return `${vendor}~${renderer}`;
} catch {
return "";
}
}
function detectFonts(): string[] {
const baseFonts = ["monospace", "sans-serif", "serif"];
const testFonts = [
"Arial", "Verdana", "Times New Roman", "Courier New", "Georgia",
"Palatino", "Garamond", "Bookman", "Comic Sans MS", "Trebuchet MS",
"Impact", "Lucida Console", "Tahoma", "Lucida Sans Unicode",
"MS Sans Serif", "MS Serif", "Helvetica", "Calibri", "Cambria",
];
const testString = "mmmmmmmmmmlli";
const testSize = "72px";
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return [];
const baselines: { [key: string]: number } = {};
for (const baseFont of baseFonts) {
ctx.font = `${testSize} ${baseFont}`;
baselines[baseFont] = ctx.measureText(testString).width;
}
const detected: string[] = [];
for (const font of testFonts) {
let isDetected = false;
for (const baseFont of baseFonts) {
ctx.font = `${testSize} '${font}', ${baseFont}`;
const width = ctx.measureText(testString).width;
if (width !== baselines[baseFont]) {
isDetected = true;
break;
}
}
if (isDetected) {
detected.push(font);
}
}
return detected;
}
function getAudioFingerprint(): string {
try {
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const analyser = audioContext.createAnalyser();
const gainNode = audioContext.createGain();
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
gainNode.gain.value = 0;
oscillator.type = "triangle";
oscillator.connect(analyser);
analyser.connect(scriptProcessor);
scriptProcessor.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start(0);
let fingerprint = "";
scriptProcessor.onaudioprocess = (event) => {
const output = event.outputBuffer.getChannelData(0);
fingerprint = Array.from(output.slice(0, 30))
.map((v) => v.toFixed(10))
.join("");
oscillator.stop();
audioContext.close();
};
return fingerprint || "audio-fp";
} catch {
return "";
}
}
function getScreenFingerprint() {
return {
width: window.screen.width,
height: window.screen.height,
colorDepth: window.screen.colorDepth,
pixelRatio: window.devicePixelRatio || 1,
};
}
function getNavigatorFingerprint() {
return {
userAgent: navigator.userAgent,
language: navigator.language,
languages: navigator.languages,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency || 0,
deviceMemory: (navigator as any).deviceMemory,
maxTouchPoints: navigator.maxTouchPoints || 0,
};
}
function getTimezoneFingerprint() {
return {
offset: new Date().getTimezoneOffset(),
name: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}
function getFeatures() {
return {
cookies: navigator.cookieEnabled,
localStorage: !!window.localStorage,
sessionStorage: !!window.sessionStorage,
indexedDB: !!window.indexedDB,
webgl: !!document.createElement("canvas").getContext("webgl"),
canvas: !!document.createElement("canvas").getContext("2d"),
};
}
export async function generateClientFingerprint(): Promise<{
fingerprint: string;
data: ClientFingerprintData;
}> {
const data: ClientFingerprintData = {
canvas: getCanvasFingerprint(),
webgl: getWebGLFingerprint(),
fonts: detectFonts(),
audio: getAudioFingerprint(),
screen: getScreenFingerprint(),
navigator: getNavigatorFingerprint(),
timezone: getTimezoneFingerprint(),
features: getFeatures(),
};
const fingerprintString = JSON.stringify(data);
const fingerprint = await sha256(fingerprintString);
return { fingerprint, data };
}