UNPKG

@kanadi/core

Version:

Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles

217 lines (189 loc) 5.62 kB
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 }; }