(function () { "use strict" var LANG = document.documentElement && document.documentElement.lang === "en" ? "en" : "fr" var TEXTS = { fr: { run: "Lancer l'analyse", running: "Analyse en cours…", again: "Relancer l'analyse", intro: "Tout se déroule localement, dans votre navigateur. Aucun résultat n'est envoyé sur Internet, rien n'est stocké sur ce serveur, et aucune limite : relancez l'analyse autant de fois que vous voulez. Deux identifiants sont calculés : l'un propre à ce navigateur, l'autre propre à votre appareil (stable quand vous changez de navigateur).", idLabel: "Empreinte navigateur (Ray ID)", idHow: "Calculé à partir des signaux dépendants du moteur (User-Agent, canvas, audio, moteur JS). Il change si vous changez de navigateur ou de réglages d'anti-empreinte.", deviceLabel: "Empreinte d'appareil", deviceHow: "Calculé uniquement à partir du noyau matériel qui résiste à l'anti-empreinte : profondeur de couleur, canaux audio, codecs VP9/AV1/Opus, largeurs d'emoji (le font-spoofing ne touche pas les glyphes emoji) et écart à l'horloge du serveur (tronqué en paliers de 1 s). La dérive d'horloge locale est exclue : RFP la quantifie grossièrement et la rend aléatoire à chaque chargement. Le ZPD reste identique entre LibreWolf et Brave sur la même machine.", entropyLabel: "Entropie totale", entropyNote: "L'entropie est exprimée en bits. Plus elle est élevée, plus votre navigateur est unique et donc identifiable.", deviceEntropyLabel: "Entropie appareil", browserEntropyLabel: "Entropie navigateur", population: "Population théorique partageant cette empreinte", level: "Niveau d'unicité", deviceLevelLabel: "Unicité de l'appareil", levels: { low: "Faible", moderate: "Modérée", high: "Élevée", extreme: "Extrême" }, scoreLabel: "Probabilité d'unicité", stabilityLabel: "Signaux stables", signalsTitle: "Les signaux analysés", signalsHow: "Comment ça marche", signalsProtect: "Comment s'en protéger", stable: "stable", volatile: "volatile", device: "appareil", browser: "navigateur", core: "noyau", notAvailable: "non disponible", value: "Valeur", bits: "bits", entropy: "entropie", estimated: "estimation", estimatedNote: "Estimations d'entropie issues des travaux publics de recherche sur l'empreinte navigateur (Panopticlick, AmIUnique, FingerprintJS).", privacyNote: "Confidentialité : cette page ne collecte rien. Toutes les mesures sont faites dans votre navigateur ; aucune donnée n'est envoyée ni enregistrée. C'est une démonstration pédagogique du fingerprinting.", whatIsTitle: "Qu'est-ce que le fingerprinting ?", whatIs: [ "Le fingerprinting (empreinte numérique) consiste à collecter les caractéristiques techniques d'un navigateur pour créer un identifiant unique et persistant, sans cookie ni compte.", "Chaque navigateur combine différemment son moteur de rendu, sa carte graphique, ses polices installées, sa langue, son fuseau horaire, son écran et bien d'autres détails. Cette combinaison est presque toujours unique.", "Deux couches coexistent : l'empreinte du navigateur (moteur JS, rendu canvas, traitement audio) qui change dès qu'on change de navigateur, et l'empreinte de l'appareil (écran, GPU, polices, OS) qui reste stable. C'est cette seconde couche qui permet à un site de vous reconnaître même si vous utilisez un navigateur différent.", "C'est la technique de suivi la plus difficile à bloquer : il n'y a pas de cookie à supprimer. C'est pourquoi de nombreux sites, régies publicitaires et services antifraude l'utilisent — y compris certains fournisseurs qui l'annoncent comme un « Ray ID ».", "L'objectif de cette page est de vous montrer ce que les sites peuvent savoir de vous, de vous donner un score, puis de vous expliquer comment vous protéger." ], error: "Le test a échoué sur ce navigateur. Essayez de relancer.", webglExt: "extensions", resolution: "résolution", fontsCount: "polices détectées", stableCount: "de vos signaux sont stables dans le temps", deviceStableCount: "de vos signaux d'appareil sont stables entre navigateurs", spoofTitle: "Anti-empreinte actif", spoofText: "Des signaux matériels sont incohérents (par exemple une plateforme Windows avec des polices Linux) : un anti-fingerprinting (RFP, extensions, lettreboxing…) masque ou spoofe votre appareil. Le ZPD reflète alors le matériel SPOOFÉ et peut changer d'un navigateur à l'autre — c'est le but de la protection. Pour comparer, utilisez deux navigateurs non durcis sur la même machine.", run3: "Lancer l'analyse", tourN: "Passage {n}/{t}", tourRunning: "Analyse {n}/{t} en cours…", tourDone: "Passage {n}/{t} terminé — on réanalyse…", finalRun: "Dernier passage — calcul final en cours…", multiTitle: "Votre niveau d'exposition au suivi", multiIntro: "L'analyse s'est déroulée sur plusieurs instants d'affilée, avec un rechargement de la page entre chaque passage. Ce qui suit compare les empreintes d'un passage à l'autre et évalue à quel point il est possible de vous suivre.", secScore: "Niveau d'exposition", stabScore: "Stabilité", protStatus: "Statut", type: "Type", zfpSub: "Empreinte navigateur", zpdSub: "Empreinte d'appareil", zfpDesc: "Fingerprinting basique — Cloudflare Ray ID, FingerprintJS, 95 %+ des sites. Identifie le navigateur, change quand vous changez de navigateur ou de profil.", zpdDesc: "Fingerprinting avancé — rare, souvent partagé avec un tiers, extrêmement plus difficile à contourner. Identifie la machine elle-même, stable quelle que soit la configuration logicielle. Utilisé par des services comme Shugoi.", protected: "Protégé", partial: "Partiellement protégé", vulnerable: "Vulnérable", exposed: "Exposé", instable: "Instable entre les passages", stableSig: "Stable entre les passages", recoTitle: "Recommandations", restart: "Relancer l'analyse", whatProtected: "Ce que vous protégez", whatExposed: "Ce qui vous identifie" }, en: { run: "Run the analysis", running: "Analyzing…", again: "Run again", intro: "Everything runs locally, in your browser. No result is sent over the Internet, nothing is stored on this server, and there is no limit: re-run the analysis as many times as you like. Two identifiers are computed: one specific to this browser, one specific to your device (stable when you switch browsers).", idLabel: "Browser fingerprint (Ray ID)", idHow: "Computed from engine-dependent signals (User-Agent, canvas, audio, JS engine). It changes if you switch browsers or anti-fingerprinting settings.", deviceLabel: "Device fingerprint", deviceHow: "Computed only from the hardware core that resists anti-fingerprinting: color depth, audio channels, VP9/AV1/Opus codecs, emoji widths (font spoofing does not touch emoji glyphs) and server clock offset (truncated to 1 s steps). The local clock drift is excluded: RFP quantizes it coarsely and makes it random on every load. The ZPD stays identical between LibreWolf and Brave on the same machine.", entropyLabel: "Total entropy", entropyNote: "Entropy is expressed in bits. The higher it is, the more unique — and trackable — your browser is.", deviceEntropyLabel: "Device entropy", browserEntropyLabel: "Browser entropy", population: "Theoretical population sharing this fingerprint", level: "Uniqueness level", deviceLevelLabel: "Device uniqueness", levels: { low: "Low", moderate: "Moderate", high: "High", extreme: "Extreme" }, scoreLabel: "Uniqueness probability", stabilityLabel: "Stable signals", signalsTitle: "Signals analyzed", signalsHow: "How it works", signalsProtect: "How to protect yourself", stable: "stable", volatile: "volatile", device: "device", browser: "browser", core: "core", notAvailable: "not available", value: "Value", bits: "bits", entropy: "entropy", estimated: "estimate", estimatedNote: "Entropy estimates based on public fingerprinting research (Panopticlick, AmIUnique, FingerprintJS).", privacyNote: "Privacy: this page collects nothing. Every measurement happens in your browser; no data is sent or stored. This is a pedagogical demonstration of fingerprinting.", whatIsTitle: "What is fingerprinting?", whatIs: [ "Fingerprinting is the collection of a browser's technical characteristics to build a unique, persistent identifier — without cookies or accounts.", "Each browser combines its rendering engine, GPU, installed fonts, language, timezone, screen and many other details differently. This combination is almost always unique.", "Two layers coexist: the browser fingerprint (JS engine, canvas rendering, audio processing) which changes as soon as you switch browsers, and the device fingerprint (screen, GPU, fonts, OS) which stays stable. This second layer is what lets a website recognize you even with a different browser.", "It is the hardest tracking technique to block: there is no cookie to delete. That is why many websites, ad networks and anti-fraud services use it — including providers that brand it as a “Ray ID”.", "The goal of this page is to show you what websites can learn about you, give you a score, and explain how to protect yourself." ], error: "The test failed in this browser. Try again.", webglExt: "extensions", resolution: "resolution", fontsCount: "fonts detected", stableCount: "of your signals are stable over time", deviceStableCount: "of your device signals are stable across browsers", spoofTitle: "Anti-fingerprinting detected", spoofText: "Hardware signals are inconsistent (e.g. a Windows platform reporting Linux fonts): an anti-fingerprinting tool (RFP, extensions, letterboxing…) is hiding or spoofing your device. The ZPD then reflects the SPOOFED hardware and may change from one browser to another — that is the point of the protection. To compare, use two non-hardened browsers on the same machine.", run3: "Run the analysis", tourN: "Pass {n}/{t}", tourRunning: "Analyzing pass {n}/{t}…", tourDone: "Pass {n}/{t} done — running again…", finalRun: "Last pass — computing the final result…", multiTitle: "How trackable you are", multiIntro: "The analysis ran several times in a row, reloading the page between each pass. What follows compares the fingerprints across passes and gauges how easily you can be tracked.", secScore: "Exposure level", stabScore: "Stability", protStatus: "Status", type: "Type", zfpSub: "Browser fingerprint", zpdSub: "Device fingerprint", zfpDesc: "Basic fingerprinting — Cloudflare Ray ID, FingerprintJS, 95%+ of sites. Identifies the browser, changes when you switch browser or profile.", zpdDesc: "Advanced fingerprinting — rare, often shared with a third party, much harder to bypass. Identifies the machine itself, stable regardless of software configuration. Used by services like Shugoi.", protected: "Protected", partial: "Partially protected", vulnerable: "Vulnerable", exposed: "Exposed", instable: "Unstable across passes", stableSig: "Stable across passes", recoTitle: "Recommendations", restart: "Run the analysis again", whatProtected: "What you're protecting", whatExposed: "What identifies you" } } function t(key) { var chain = key.split(".") var o = TEXTS[LANG] for (var i = 0; i < chain.length && o; i++) o = o[chain[i]] return typeof o === "string" ? o : key } function $id(id) { return document.getElementById(id) } function el(tag, className, text) { var n = document.createElement(tag) if (className) n.className = className if (text !== undefined && text !== null) n.textContent = text return n } function hashCode(str) { var h = 2166136261 for (var i = 0; i < str.length; i++) { h ^= str.charCodeAt(i) h = (h * 16777619) >>> 0 } return ("00000000" + h.toString(16)).slice(-8) } function sha256Hex(str) { if (!window.crypto || !crypto.subtle || !crypto.subtle.digest) { return Promise.resolve(hashCode(str) + hashCode(str.split("").reverse().join(""))) } var data = new TextEncoder().encode(str) return crypto.subtle .digest("SHA-256", data) .then(function (buf) { var bytes = new Uint8Array(buf) var hex = "" for (var i = 0; i < bytes.length; i++) hex += ("0" + bytes[i].toString(16)).slice(-2) return hex }) .catch(function () { return hashCode(str) + hashCode(str.split("").reverse().join("")) }) } function stableHash(str) { return hashCode("stable:" + str) + hashCode("stable:" + str.split("").reverse().join("")) } function formatBits(b) { if (b >= 100) return b.toFixed(0) return b.toFixed(1) } function formatPopulation(bits) { if (bits >= 128) return "≈ 2^" + Math.floor(bits) return "≈ " + Math.round(Math.pow(2, bits)).toLocaleString(LANG === "fr" ? "fr-FR" : "en-US") } function wait(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms) }) } function timeout(promise, ms) { return Promise.race([promise, wait(ms).then(function () { return null })]) } function stableString(value) { try { return JSON.stringify(value) } catch (e) { return String(value) } } function collectUA() { var parts = [navigator.userAgent || ""] if (navigator.userAgentData) { try { var ud = navigator.userAgentData if (ud.brands) { parts.push("brands=" + ud.brands.map(function (b) { return b.brand + "@" + b.version }).join(",")) } parts.push("mobile=" + !!ud.mobile) if (ud.getHighEntropyValues) { return ud .getHighEntropyValues(["platform", "platformVersion", "architecture", "model", "uaFullVersion", "fullVersionList"]) .then(function (h) { return parts .concat([ "arch=" + (h.architecture || "?"), "model=" + (h.model || "?"), "platVer=" + (h.platformVersion || "?"), "uaFull=" + (h.uaFullVersion || "?"), "fullBrands=" + ((h.fullVersionList || []).map(function (b) { return b.brand + "@" + b.version }).join(",") || "?") ]) .join(" || ") }) .catch(function () { return parts.join(" || ") }) } } catch (e) {} } return parts.join(" || ") } function collectPlatform() { var o = { platform: navigator.platform || "?" } try { if (navigator.userAgentData) { o.hintPlatform = navigator.userAgentData.platform || "?" o.mobile = !!navigator.userAgentData.mobile if (navigator.userAgentData.getHighEntropyValues) { return navigator.userAgentData .getHighEntropyValues(["architecture", "model", "bitness"]) .then(function (h) { o.arch = h.architecture || "?" o.model = h.model || "?" o.bitness = h.bitness || "?" return o }) .catch(function () { return o }) } } } catch (e) {} return o } function collectLanguages() { var langs = navigator.languages || [] if (!langs.length && navigator.language) langs = [navigator.language] var extra = {} try { extra.locale = Intl.DateTimeFormat().resolvedOptions().locale } catch (e) {} try { extra.number = new Intl.NumberFormat().resolvedOptions().numberingSystem || "?" } catch (e) {} return { langs: langs.join(","), extra: extra } } function collectTimezone() { var tz = "?" try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "?" } catch (e) {} var off = new Date().getTimezoneOffset() var offStr = (off === 0 ? "0" : (off < 0 ? "+" : "-") + Math.abs(Math.round(off / 60))) var jan = new Date(new Date().getFullYear(), 0, 1).getTimezoneOffset() var jul = new Date(new Date().getFullYear(), 6, 1).getTimezoneOffset() var dst = jan !== jul ? (off === Math.max(jan, jul) ? "hiver" : "été") : "pas de DST" return { tz: tz, offset: offStr, dst: dst } } function collectScreen() { var s = window.screen || {} var o = s.orientation var res = { w: s.width, h: s.height, availW: s.availWidth, availH: s.availHeight, colorDepth: s.colorDepth, pixelDepth: s.pixelDepth, dpr: window.devicePixelRatio || 1, orientation: o && o.type ? o.type : (s.orientationType || "n/a") } return getWorkerData().then(function (d) { if (d && d.colorDepth) { res.colorDepth = d.colorDepth if (d.orientation) res.orientation = d.orientation res.worker = true } return res }) } function canvasPass(draw) { try { var c = document.createElement("canvas") c.width = 300 c.height = 150 var ctx = c.getContext("2d") if (!ctx) return null ctx.textBaseline = "top" draw(ctx) return stableHash(c.toDataURL()) } catch (e) { return null } } function collectCanvas() { var hashes = [] var p1 = canvasPass(function (ctx) { ctx.fillStyle = "#f60" ctx.fillRect(125, 1, 62, 20) var g = ctx.createLinearGradient(0, 0, 300, 150) g.addColorStop(0, "#f60") g.addColorStop(0.5, "#069") g.addColorStop(1, "#0a0") ctx.fillStyle = g ctx.fillRect(0, 0, 300, 40) ctx.font = "15px Arial" ctx.fillStyle = "#069" ctx.fillText("Zektyc-Fingerprint-ææåå", 2, 15) ctx.font = "italic bold 20px Georgia" ctx.fillText("zfp::test::0123456789", 4, 45) }) var p2 = canvasPass(function (ctx) { ctx.save() ctx.translate(150, 75) ctx.rotate(0.5) ctx.scale(1.1, 0.9) ctx.fillStyle = "rgba(102, 204, 0, 0.7)" ctx.beginPath() ctx.arc(0, 0, 40, 0, Math.PI * 2) ctx.closePath() ctx.fill() ctx.fillStyle = "#fff" ctx.beginPath() ctx.arc(30, 20, 15, 0, Math.PI * 2, true) ctx.closePath() ctx.fill() ctx.strokeStyle = "#000" ctx.lineWidth = 2 ctx.beginPath() ctx.moveTo(-40, -40) ctx.lineTo(40, 40) ctx.stroke() ctx.restore() }) var p3 = canvasPass(function (ctx) { ctx.globalCompositeOperation = "multiply" ctx.fillStyle = "#0f0" ctx.fillRect(0, 0, 100, 20) ctx.fillStyle = "#f00" ctx.fillRect(50, 0, 100, 20) ctx.globalCompositeOperation = "screen" ctx.fillStyle = "#00f" ctx.fillRect(25, 10, 100, 20) ctx.globalCompositeOperation = "source-over" ctx.shadowColor = "rgba(0,0,0,0.5)" ctx.shadowBlur = 5 ctx.shadowOffsetX = 3 ctx.shadowOffsetY = 3 ctx.fillStyle = "#888" ctx.fillRect(120, 80, 60, 30) }) var p4 = canvasPass(function (ctx) { ctx.font = "28px 'Arial'" ctx.fillStyle = "#000" ctx.fillText("mmmmmmmmmmlli", 5, 20) ctx.font = "28px 'Courier New'" ctx.fillText("Ii1l|!000O", 5, 50) ctx.font = "28px 'Times New Roman'" ctx.fillText("avawARcennWW", 5, 80) }) var p5 = canvasPass(function (ctx) { ctx.fillStyle = "#123" ctx.fillRect(0, 0, 300, 150) ctx.fillStyle = "#fff" ctx.font = "34px monospace" ctx.fillText("😀😍🚀💀❤️", 10, 50) ctx.font = "24px sans-serif" ctx.fillText("café naïve æther ünïc", 10, 100) }) if (p1) hashes.push(p1) if (p2) hashes.push(p2) if (p3) hashes.push(p3) if (p4) hashes.push(p4) if (p5) hashes.push(p5) if (!hashes.length) return null return { hash: stableHash(hashes.join("|")), passes: hashes.length } } function webglRenderHash(gl) { try { var w = 16 var h = 16 gl.viewport(0, 0, w, h) var vsSrc = "attribute vec2 p;void main(){gl_Position=vec4(p,0.0,1.0);}" var fsSrc = "precision mediump float;uniform float u;void main(){vec2 r=gl_FragCoord.xy/16.0;float v=fract(sin(dot(r+u,vec2(12.9898,78.233)))*43758.5453);gl_FragColor=vec4(r,v,1.0);}" var vs = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vs, vsSrc) gl.compileShader(vs) var fs = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(fs, fsSrc) gl.compileShader(fs) if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) return null var prog = gl.createProgram() gl.attachShader(prog, vs) gl.attachShader(prog, fs) gl.linkProgram(prog) if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null gl.useProgram(prog) var buf = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, buf) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW) var loc = gl.getAttribLocation(prog, "p") gl.enableVertexAttribArray(loc) gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0) var uloc = gl.getUniformLocation(prog, "u") gl.uniform1f(uloc, 0.001) gl.clearColor(0, 0, 0, 1) gl.clear(gl.COLOR_BUFFER_BIT) gl.drawArrays(gl.TRIANGLES, 0, 3) var px = new Uint8Array(w * h * 4) gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px) var s = "" for (var i = 0; i < px.length; i += 7) s += px[i].toString(16) return stableHash(s) } catch (e) { return null } } function collectWebGL() { try { var c = document.createElement("canvas") var gl = c.getContext("webgl") || c.getContext("experimental-webgl") if (!gl) return null var dbg = gl.getExtension("WEBGL_debug_renderer_info") var vendor = dbg ? String(gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL)) : String(gl.getParameter(gl.VENDOR)) var renderer = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : String(gl.getParameter(gl.RENDERER)) var version = String(gl.getParameter(gl.VERSION)) var glsl = String(gl.getParameter(gl.SHADING_LANGUAGE_VERSION)) var ext = (gl.getSupportedExtensions() || []).join(",") var params = {} var pnames = [ "MAX_TEXTURE_SIZE", "MAX_CUBE_MAP_TEXTURE_SIZE", "MAX_RENDERBUFFER_SIZE", "MAX_VIEWPORT_DIMS", "MAX_VERTEX_ATTRIBS", "MAX_VERTEX_UNIFORM_VECTORS", "MAX_VARYING_VECTORS", "MAX_FRAGMENT_UNIFORM_VECTORS", "MAX_COMBINED_TEXTURE_IMAGE_UNITS" ] for (var i = 0; i < pnames.length; i++) { try { params[pnames[i]] = JSON.stringify(gl.getParameter(gl[pnames[i]])) } catch (e) {} } var precision = {} var stypes = ["VERTEX_SHADER", "FRAGMENT_SHADER"] for (var j = 0; j < stypes.length; j++) { try { var f = gl.getShaderPrecisionFormat(gl[stypes[j]], gl.HIGH_FLOAT) if (f) precision[stypes[j]] = f.precision + "/" + f.rangeMin + "/" + f.rangeMax } catch (e) {} } var hasWebGL2 = false try { var c2 = document.createElement("canvas") hasWebGL2 = !!c2.getContext("webgl2") } catch (e) {} return { vendor: vendor, renderer: renderer, version: version, glsl: glsl, webgl2: hasWebGL2, extHash: stableHash(ext), paramsHash: stableHash(JSON.stringify(params) + JSON.stringify(precision)), renderHash: webglRenderHash(gl) } } catch (e) { return null } } function collectWebGPU() { return new Promise(function (resolve) { if (!navigator.gpu || !navigator.gpu.requestAdapter) { resolve(null) return } navigator.gpu .requestAdapter() .then(function (adapter) { if (!adapter) { resolve(null) return } var info = {} try { var a = adapter.info info.vendor = a.vendor || "?" info.architecture = a.architecture || "?" info.device = a.device || "?" info.description = a.description || "?" } catch (e) {} var features = [] try { adapter.features.forEach(function (f) { features.push(String(f)) }) } catch (e) {} var limits = {} try { var ls = ["maxTextureDimension2D", "maxTextureArrayLayers", "maxBufferSize", "maxBindGroups", "maxVertexBuffers", "maxColorAttachments", "maxComputeWorkgroupSizeX"] for (var i = 0; i < ls.length; i++) { if (adapter.limits && adapter.limits[ls[i]] !== undefined) limits[ls[i]] = adapter.limits[ls[i]] } } catch (e) {} resolve({ info: info, featuresHash: stableHash(features.join(",")), limitsHash: stableHash(JSON.stringify(limits)) }) }) .catch(function () { resolve(null) }) }) } function collectAudio() { return new Promise(function (resolve) { var AC = window.OfflineAudioContext || window.webkitOfflineAudioContext if (!AC) { resolve(null) return } try { var ctx = new AC(1, 44100, 44100) var osc = ctx.createOscillator() osc.type = "triangle" osc.frequency.value = 10000 var comp = ctx.createDynamicsCompressor() comp.threshold.value = -50 comp.knee.value = 40 comp.ratio.value = 12 comp.attack.value = 0 comp.release.value = 0.25 osc.connect(comp) comp.connect(ctx.destination) osc.start(0) osc.stop(0.1) var finish = function (buffer) { var data = buffer.getChannelData(0) var chunks = [] var sum = 0 for (var i = 0; i < data.length; i++) { sum += Math.abs(data[i]) if (i % 5000 === 4999) { chunks.push(hashCode(String(sum))) sum = 0 } } chunks.push(hashCode(String(sum))) resolve({ hash: stableHash(chunks.join("|")) }) } if (ctx.startRendering && ctx.oncomplete !== undefined) { ctx.oncomplete = function (e) { finish(e.renderedBuffer) } ctx.startRendering() } else if (ctx.startRendering) { ctx.startRendering().then(finish).catch(function () { resolve(null) }) } else { resolve(null) } } catch (e) { resolve(null) } }) } function collectAudioContext() { try { var AC = window.AudioContext || window.webkitAudioContext if (!AC) return null var ctx = new AC() var res = { sampleRate: ctx.sampleRate, channels: ctx.destination && typeof ctx.destination.channelCount === "number" ? ctx.destination.channelCount : 0, baseLatency: typeof ctx.baseLatency === "number" ? ctx.baseLatency.toFixed(4) : "?", outputLatency: typeof ctx.outputLatency === "number" ? ctx.outputLatency.toFixed(4) : "?" } setTimeout(function () { try { ctx.close() } catch (e) {} }, 0) return res } catch (e) { return null } } var FONT_LIST = [ "Arial", "Arial Black", "Arial Narrow", "Arial Unicode MS", "Bahnschrift", "Baskerville", "Calibri", "Cambria", "Cambria Math", "Candara", "Century Gothic", "Charter", "Cochin", "Comic Sans MS", "Consolas", "Constantia", "Corbel", "Courier", "Courier New", "DejaVu Sans", "DejaVu Sans Mono", "DejaVu Serif", "Didot", "Franklin Gothic Medium", "Futura", "Geneva", "Georgia", "Gill Sans", "Helvetica", "Helvetica Neue", "Impact", "Iowan Old Style", "Lato", "Liberation Sans", "Liberation Serif", "Lucida Console", "Malgun Gothic", "Menlo", "Microsoft JhengHei", "Microsoft Sans Serif", "Microsoft YaHei", "Monaco", "Noto Sans", "Noto Sans Mono", "Noto Serif", "Open Sans", "Optima", "Palatino", "Palatino Linotype", "Roboto", "Segoe UI", "Segoe UI Emoji", "Sitka", "Symbol", "Tahoma", "Times New Roman", "Trebuchet MS", "Ubuntu", "Ubuntu Mono", "Verdana", "Webdings", "Wingdings", "Zapfino" ] function collectFonts() { try { var c = document.createElement("canvas") c.width = 400 c.height = 80 var ctx = c.getContext("2d") if (!ctx) return null var test = "mmmmmmmmmmlli" ctx.font = "72px monospace" var base = ctx.measureText(test).width var found = [] for (var i = 0; i < FONT_LIST.length; i++) { ctx.font = "72px '" + FONT_LIST[i] + "', monospace" var w = ctx.measureText(test).width if (Math.abs(w - base) > 0.5) found.push(FONT_LIST[i]) } if (!found.length) return null return { count: found.length, list: found.join(","), hash: stableHash(found.join(",")) } } catch (e) { return null } } function collectEmoji() { try { var c = document.createElement("canvas") c.width = 250 c.height = 60 var ctx = c.getContext("2d") if (!ctx) return null var probes = ["😀", "😍", "🚀", "💀", "❤️", "🇫🇷", "🎉", "🧪", "🌈", "🦄", "👽", "💾", "🖥️", "⌛", "✈️", "⚡", "⭐", "❄️", "🍕", "💡", "🎯", "♻️", "☢️", "⛔", "🫠", "🪿", "🛜", "🤌", "🫶", "🥸"] var widths = [] var renderHashes = [] var base = ctx.measureText("M").width for (var i = 0; i < probes.length; i++) { var w = ctx.measureText(probes[i]).width widths.push(w > base * 1.5 ? "t" : "f") ctx.clearRect(0, 0, 250, 60) ctx.fillStyle = "#000" ctx.fillRect(0, 0, 250, 60) ctx.fillStyle = "#fff" ctx.font = "48px monospace" ctx.fillText(probes[i], 5, 50) renderHashes.push(hashCode(c.toDataURL())) } return { widths: widths.join(""), hash: stableHash(widths.join("") + "|" + renderHashes.join("|")) } } catch (e) { return null } } function collectHardware() { var o = {} if (navigator.hardwareConcurrency) o.cores = navigator.hardwareConcurrency if (navigator.deviceMemory) o.memory = navigator.deviceMemory + "GB" return Object.keys(o).length ? o : null } function collectTouch() { var v = navigator.maxTouchPoints || 0 var hasTouch = "ontouchstart" in window || v > 0 var coarse = false try { coarse = window.matchMedia("(pointer: coarse)").matches } catch (e) {} return { maxTouchPoints: v, touchEvents: hasTouch, coarsePointer: coarse } } function collectMedia() { var q = [ ["dark", "(prefers-color-scheme: dark)"], ["light", "(prefers-color-scheme: light)"], ["reduced-motion", "(prefers-reduced-motion: reduce)"], ["reduced-transparency", "(prefers-reduced-transparency: reduce)"], ["contrast-more", "(prefers-contrast: more)"], ["contrast-custom", "(prefers-contrast: custom)"], ["forced-colors", "(forced-colors: active)"], ["inverted-colors", "(inverted-colors: inverted)"], ["hover", "(hover: hover)"], ["any-hover", "(any-hover: hover)"], ["pointer-fine", "(pointer: fine)"], ["pointer-coarse", "(pointer: coarse)"], ["any-pointer-fine", "(any-pointer: fine)"], ["color-gamut-p3", "(color-gamut: p3)"], ["color-gamut-rec2020", "(color-gamut: rec2020)"], ["video-dynamic-p3", "(dynamic-range: p3)"], ["reduced-data", "(prefers-reduced-data: reduce)"], ["update-fast", "(update: fast)"], ["display-standalone", "(display-mode: standalone)"], ["scripting", "(scripting: enabled)"] ] var out = {} for (var i = 0; i < q.length; i++) { try { out[q[i][0]] = window.matchMedia(q[i][1]).matches } catch (e) {} } return out } function collectScrollbar() { try { var outer = document.createElement("div") outer.style.cssText = "position:absolute;top:-10000px;left:-10000px;width:100px;height:100px;overflow:scroll;visibility:hidden" var inner = document.createElement("div") inner.style.width = "100%" inner.style.height = "100%" outer.appendChild(inner) document.body.appendChild(outer) var w = outer.offsetWidth - inner.offsetWidth document.body.removeChild(outer) return { width: w + "px" } } catch (e) { return null } } function collectStorage() { var out = {} try { var k = "zfp-test-" + Date.now() window.localStorage.setItem(k, "1") out.localStorage = window.localStorage.getItem(k) === "1" window.localStorage.removeItem(k) } catch (e) { out.localStorage = false } try { var k2 = "zfp-test-" + Date.now() window.sessionStorage.setItem(k2, "1") out.sessionStorage = window.sessionStorage.getItem(k2) === "1" window.sessionStorage.removeItem(k2) } catch (e) { out.sessionStorage = false } out.indexedDB = !!window.indexedDB return out } function collectStorageQuota() { return new Promise(function (resolve) { if (!navigator.storage || !navigator.storage.estimate) { resolve(null) return } navigator.storage .estimate() .then(function (e) { resolve({ quota: e.quota || 0, usage: e.usage || 0 }) }) .catch(function () { resolve(null) }) }) } function mathRaw() { var ops = ["sin", "cos", "tan", "log", "exp", "sqrt", "asin", "acos", "atan2", "sinh", "cosh", "tanh", "cbrt"] var out = [] for (var i = 0; i < ops.length; i++) { try { out.push(Math[ops[i]](i * 0.123456789 + 1).toPrecision(17)) } catch (e) {} } try { out.push(Math.pow(1.0000001, 1000000).toPrecision(17)) } catch (e) {} return out.join(":") } function collectMath() { var s = mathRaw() if (!s) return null return { hash: stableHash(s) } } function collectTiming() { var t0 = performance.now() var min = Infinity for (var i = 0; i < 1000; i++) { var t = performance.now() var d = t - t0 if (d > 0 && d < min) min = d t0 = t } var d0 = Date.now() var dmin = Infinity for (var j = 0; j < 1000; j++) { var d2 = Date.now() - d0 if (d2 > 0 && d2 < dmin) dmin = d2 d0 = Date.now() } return { perf: min === Infinity ? "?" : min.toFixed(4), date: dmin === Infinity ? "?" : dmin } } function collectWasm() { var out = { hasWasm: !!window.WebAssembly } if (window.WebAssembly) { try { var simdBytes = new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7b, 0x03, 0x02, 0x01, 0x00, 0x0a, 0x17, 0x01, 0x15, 0x00, 0x41, 0x00, 0xfd, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b ]) out.simd = WebAssembly.validate(simdBytes) } catch (e) { out.simd = false } out.threads = typeof SharedArrayBuffer !== "undefined" try { out.bulk = WebAssembly.validate(new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x05, 0x03, 0x01, 0x00, 0x00, 0x0a, 0x08, 0x01, 0x06, 0x00, 0xfc, 0x0a, 0x00, 0x00, 0x0b ])) } catch (e) { out.bulk = false } } return out } function collectBench() { var start = performance.now() var x = 0 var n = 2000000 for (var i = 0; i < n; i++) { x = (x + i) % 97 x = Math.sqrt(x) + Math.log(i + 1) } var dt = performance.now() - start return { ms: dt > 0 ? dt.toFixed(1) : "?", opsPerMs: dt > 0 ? Math.round(n / dt) : 0 } } function collectWebRTC() { return new Promise(function (resolve) { if (!window.RTCPeerConnection) { resolve(null) return } var candidates = [] var done = false var pc try { pc = new RTCPeerConnection({ iceServers: [] }) } catch (e) { resolve(null) return } var finish = function () { if (done) return done = true try { pc.close() } catch (e) {} if (candidates.length) resolve({ hash: stableHash(candidates.join("|")), count: candidates.length }) else resolve(null) } try { pc.createDataChannel("zfp") pc.onicecandidate = function (e) { if (e.candidate && e.candidate.candidate) candidates.push(e.candidate.candidate) if (!e.candidate) finish() } pc.createOffer().then(function (o) { return pc.setLocalDescription(o) }).catch(finish) } catch (e) { finish() } setTimeout(finish, 2000) }) } function collectBattery() { return new Promise(function (resolve) { if (!navigator.getBattery) { resolve(null) return } navigator .getBattery() .then(function (b) { resolve({ level: Math.round(b.level * 100), charging: b.charging }) }) .catch(function () { resolve(null) }) }) } function collectMediaDevices() { return new Promise(function (resolve) { if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { resolve(null) return } navigator.mediaDevices .enumerateDevices() .then(function (devs) { var count = {} for (var i = 0; i < devs.length; i++) { count[devs[i].kind] = (count[devs[i].kind] || 0) + 1 } resolve(count) }) .catch(function () { resolve(null) }) }) } function collectPlugins() { var names = [] if (navigator.plugins && navigator.plugins.length) { for (var i = 0; i < navigator.plugins.length; i++) { names.push(navigator.plugins[i].name) } } if (names.length === 0 && navigator.pdfViewerEnabled !== undefined) { return { pdfViewer: navigator.pdfViewerEnabled } } return names.length ? { plugins: names.slice(0, 5) } : null } function collectSpeech() { return new Promise(function (resolve) { var SS = window.speechSynthesis if (!SS || !SS.getVoices) { resolve(null) return } var done = false var finish = function () { if (done) return done = true var v = [] try { v = SS.getVoices() || [] } catch (e) {} if (!v.length) { resolve(null) return } var names = [] for (var i = 0; i < v.length; i++) { names.push(v[i].voiceURI || v[i].name + "@" + v[i].lang) } resolve({ count: v.length, hash: stableHash(names.join("|")) }) } try { if (SS.getVoices().length) finish() else { SS.onvoiceschanged = finish setTimeout(finish, 2000) } } catch (e) { finish() } }) } function collectIntl() { var out = {} if (typeof Intl.supportedValuesOf === "function") { try { out.calendars = Intl.supportedValuesOf("calendar").join(",") } catch (e) {} try { out.numbering = Intl.supportedValuesOf("numberingSystem").join(",") } catch (e) {} try { out.units = Intl.supportedValuesOf("unit").join(",") } catch (e) {} try { out.collations = Intl.supportedValuesOf("collation").join(",") } catch (e) {} } try { out.locale = Intl.DateTimeFormat().resolvedOptions().locale } catch (e) {} try { var c = new Intl.Collator(LANG).resolvedOptions() out.collation = c.collation out.caseFirst = c.caseFirst out.numeric = c.numeric out.sensitivity = c.sensitivity } catch (e) {} return Object.keys(out).length ? out : null } var driftCache = null function computeDrift(pts) { if (!pts || pts.length < 4) return null var x0 = pts[0][1] var y0 = pts[0][0] var n = pts.length var sx = 0, sy = 0, sxx = 0, sxy = 0 for (var i = 0; i < n; i++) { var x = pts[i][1] - x0 var y = pts[i][0] - y0 sx += x sy += y sxx += x * x sxy += x * y } var denom = n * sxx - sx * sx if (!denom) return null var slope = (n * sxy - sx * sy) / denom var ppm = (slope - 1) * 1e6 var sse = 0 for (var j = 0; j < n; j++) { var xx = pts[j][1] - x0 var yy = pts[j][0] - y0 var err = yy - slope * xx sse += err * err } return { ppm: Math.round(ppm * 10) / 10, sd: Math.round(Math.sqrt(sse / n) * 100) / 100, samples: n } } function collectClockDrift() { if (driftCache) return Promise.resolve(driftCache) return new Promise(function (resolve) { var pts = [] var prevD = Date.now() var prevP = performance.now() var end = Date.now() + 2000 function sample() { var d = Date.now() var p = performance.now() if (d !== prevD || p !== prevP) { pts.push([d, p]) prevD = d prevP = p } if (Date.now() < end && pts.length < 800) { setTimeout(sample, 5) } else { driftCache = computeDrift(pts) resolve(driftCache) } } sample() }) } var workerDataPromise = null function getWorkerData() { if (workerDataPromise) return workerDataPromise workerDataPromise = new Promise(function (resolve) { if (typeof Worker === "undefined") { resolve(null) return } var w try { w = new Worker("/js/zf-worker.js?v=9") } catch (e) { resolve(null) return } var done = false var finish = function (val) { if (done) return done = true try { w.terminate() } catch (e) {} resolve(val) } w.onmessage = function (e) { var d = e.data || {} finish({ math: d.math || null, calendars: d.calendars || null, locale: d.locale || null, numbering: d.numbering || null, simd: typeof d.simd === "boolean" ? d.simd : null, bench: d.bench || null, fonts: Array.isArray(d.fonts) ? d.fonts : null, colorDepth: d.colorDepth !== undefined && d.colorDepth !== null ? String(d.colorDepth) : "", orientation: d.orientation || "", codecs: d.codecs || null, fontLocalApi: d.fontLocalApi || null, fontLocalBits: d.fontLocalBits || null, fontSig: d.fontSig || null }) } w.onerror = function () { finish(null) } setTimeout(function () { finish(null) }, 4000) try { w.postMessage(null) } catch (e) { finish(null) } }) return workerDataPromise } function collectWorker() { return getWorkerData().then(function (d) { if (!d) return null var diffs = [] try { var mainMath = mathRaw() if (mainMath && d.math && mainMath !== d.math) diffs.push("math") } catch (e2) {} try { var mainCal = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("calendar").join(",") : null if (mainCal && d.calendars && mainCal !== d.calendars) diffs.push("intl") } catch (e2) {} try { var mainLocale = Intl.DateTimeFormat().resolvedOptions().locale if (mainLocale && d.locale && mainLocale !== d.locale) diffs.push("locale") } catch (e2) {} return { diffs: diffs, workerMath: d.math ? stableHash(d.math).slice(0, 12) : null, workerLocale: d.locale || null, workerSimd: typeof d.simd === "boolean" ? d.simd : null, workerBench: d.bench || null } }) } function collectWorkerFonts() { return getWorkerData().then(function (d) { if (!d || !d.fonts) return null var list = d.fonts.join(",") return { count: d.fonts.length, via: "canvas", list: list, hash: stableHash(list) } }) } function mainThreadCodecs() { var got = {} try { if (navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo) { var tests = { vp9: { type: "file", video: { contentType: 'video/webm;codecs="vp09.00.10.08"', width: 1920, height: 1080, bitrate: 4000000, framerate: 30 } }, av1: { type: "file", video: { contentType: 'video/webm;codecs="av01.0.04M.08"', width: 1920, height: 1080, bitrate: 4000000, framerate: 30 } }, opus: { type: "file", audio: { contentType: 'audio/ogg;codecs="opus"' } } } return Promise.all( ["vp9", "av1", "opus"].map(function (k) { return navigator.mediaCapabilities .decodingInfo(tests[k]) .then(function (r) { got[k] = r && r.supported ? "1" : "0" }) .catch(function () { got[k] = "0" }) }) ).then(function () { return got }) } } catch (e) {} return Promise.resolve(null) } function collectCodecs() { return getWorkerData().then(function (d) { if (d && d.codecs) { var keys = Object.keys(d.codecs) if (keys.length === 3) { var o = {} for (var k in d.codecs) o[k] = d.codecs[k] === "1" return o } } return mainThreadCodecs().then(function (main) { if (!main) return null var o = {} for (var k in main) o[k] = main[k] === "1" return o }) }) } var clockOffsetPromise = null function collectClockOffset() { if (clockOffsetPromise) return clockOffsetPromise var p = new Promise(function (resolve) { if (typeof fetch !== "function") { clockOffsetPromise = null resolve(null) return } var offsets = [] var rtts = [] var n = 0 var started = Date.now() function sample() { var p0 = performance.now() fetch("/api/time", { cache: "no-store" }) .then(function (r) { return r.json() }) .then(function (j) { var p1 = performance.now() var mid = (p0 + p1) / 2 if (typeof j.t === "number") { var localWall = Date.now() - performance.now() + mid offsets.push(j.t - localWall) rtts.push(p1 - p0) } n++ if (n < 12 && Date.now() - started < 4000 && offsets.length < 6) { setTimeout(sample, 40) } else { finish() } }) .catch(function () { n++ if (n < 12 && Date.now() - started < 4000) { setTimeout(sample, 40) } else { finish() } }) } function finish() { var o = null if (offsets.length >= 1) { var sorted = offsets.slice().sort(function (a, b) { return a - b }) var rttSorted = rtts.slice().sort(function (a, b) { return a - b }) o = { offsetMs: Math.round(sorted[Math.floor(sorted.length / 2)]), rttMs: Math.round(rttSorted[Math.floor(rttSorted.length / 2)]), samples: offsets.length } } if (!o) clockOffsetPromise = null resolve(o) } sample() }) clockOffsetPromise = p return p } function signalDefs() { return [ { id: "ua", entropy: 12, stable: false, device: false, collect: collectUA, format: function (v) { return v } }, { id: "platform", entropy: 4, stable: true, device: true, collect: collectPlatform, format: function (v) { var parts = [v.platform] if (v.hintPlatform && v.hintPlatform !== v.platform) parts.push(v.hintPlatform) if (v.arch) parts.push(v.arch + (v.bitness ? "/" + v.bitness : "")) if (v.mobile) parts.push("mobile") return parts.join(" · ") } }, { id: "languages", entropy: 4, stable: false, device: false, collect: collectLanguages, format: function (v) { return v.langs + (v.extra && v.extra.locale ? " · locale " + v.extra.locale : "") } }, { id: "timezone", entropy: 5, stable: true, device: true, collect: collectTimezone, format: function (v) { return v.tz + " (UTC" + v.offset + ") · " + v.dst } }, { id: "screen", entropy: 6, stable: true, device: true, core: true, collect: collectScreen, format: function (v) { return v.w + "×" + v.h + " · " + v.availW + "×" + v.availH + " · " + v.colorDepth + " bits · dpr " + v.dpr + " · " + v.orientation }, hashOf: function (v) { return "cd=" + v.colorDepth } }, { id: "hardware", entropy: 4, stable: true, device: true, collect: collectHardware, format: function (v) { var parts = [] if (v.cores) parts.push(v.cores + " cores") if (v.memory) parts.push(v.memory) return parts.join(" · ") || t("notAvailable") } }, { id: "touch", entropy: 2, stable: true, device: true, collect: collectTouch, format: function (v) { return "maxTouchPoints " + v.maxTouchPoints + " · coarse " + (v.coarsePointer ? "oui" : "non") } }, { id: "canvas", entropy: 24, stable: true, device: false, collect: collectCanvas, format: function (v) { return v.passes + " passes · " + v.hash.slice(0, 16) + "…" } }, { id: "webgl", entropy: 16, stable: true, device: true, collect: collectWebGL, format: function (v) { return v.vendor + " / " + v.renderer + " · " + (v.webgl2 ? "WebGL2" : "WebGL1") + " · render " + (v.renderHash || "—").slice(0, 12) + "…" }, hashOf: function (v) { return ["vendor=" + v.vendor, "renderer=" + v.renderer, "version=" + v.version, "glsl=" + v.glsl, "webgl2=" + v.webgl2, "ext=" + (v.extHash || ""), "params=" + (v.paramsHash || "")].join("|") } }, { id: "webgpu", entropy: 16, stable: true, device: true, collect: collectWebGPU, format: function (v) { return v.info.vendor + " / " + v.info.architecture + " / " + v.info.device + " · " + v.featuresHash.slice(0, 10) + "…" } }, { id: "audio", entropy: 20, stable: true, device: false, collect: collectAudio, format: function (v) { return v.hash.slice(0, 16) + "…" } }, { id: "audiocontext", entropy: 6, stable: true, device: true, core: true, collect: collectAudioContext, format: function (v) { return v.sampleRate + " Hz · " + v.channels + " canaux · base " + v.baseLatency + " ms · out " + v.outputLatency + " ms" }, hashOf: function (v) { return "ch=" + v.channels } }, { id: "fonts", entropy: 18, stable: true, device: true, collect: collectFonts, format: function (v) { return v.count + " " + t("fontsCount") + " (" + v.list.split(",").slice(0, 6).join(", ") + "…)" } }, { id: "emoji", entropy: 8, stable: true, device: true, core: true, collect: collectEmoji, format: function (v) { return v.hash.slice(0, 16) + "…" }, hashOf: function (v) { return "w=" + (v.widths || "") } }, { id: "media", entropy: 5, stable: false, device: false, collect: collectMedia, format: function (v) { var on = [] for (var k in v) if (v[k]) on.push(k) return on.join(", ") || "aucune" } }, { id: "scrollbar", entropy: 3, stable: true, device: true, collect: collectScrollbar, format: function (v) { return v.width }, hashOf: function (v) { var w = parseInt(v.width, 10) return (isNaN(w) || w < 5) ? "overlay" : "classic=" + w } }, { id: "storage", entropy: 2, stable: true, device: false, collect: collectStorage, format: function (v) { var on = [] if (v.localStorage) on.push("localStorage") if (v.sessionStorage) on.push("sessionStorage") if (v.indexedDB) on.push("IndexedDB") return on.join(", ") || "aucun" } }, { id: "storagequota", entropy: 4, stable: true, device: true, collect: collectStorageQuota, format: function (v) { return "quota " + (v.quota / 1073741824).toFixed(1) + " Go · usage " + (v.usage / 1048576).toFixed(0) + " Mo" }, hashOf: function (v) { return "q=" + v.quota } }, { id: "math", entropy: 7, stable: true, device: false, collect: collectMath, format: function (v) { return v.hash.slice(0, 16) + "…" } }, { id: "timing", entropy: 3, stable: false, device: false, collect: collectTiming, format: function (v) { return "perf " + v.perf + " ms · date " + v.date + " ms" } }, { id: "wasm", entropy: 4, stable: true, device: false, collect: collectWasm, format: function (v) { var on = ["wasm"] if (v.simd) on.push("SIMD") if (v.threads) on.push("threads") if (v.bulk) on.push("bulk-mem") return on.join(", ") } }, { id: "bench", entropy: 8, stable: false, device: true, collect: collectBench, format: function (v) { return v.opsPerMs + " ops/ms (" + v.ms + " ms)" } }, { id: "webrtc", entropy: 16, stable: false, device: false, collect: collectWebRTC, format: function (v) { return v.count + " candidats ICE" } }, { id: "battery", entropy: 4, stable: false, device: true, collect: collectBattery, format: function (v) { return v.level + "%" + (v.charging ? " (charge)" : "") } }, { id: "mediadevices", entropy: 4, stable: false, device: true, collect: collectMediaDevices, format: function (v) { var parts = [] for (var k in v) parts.push(v[k] + " " + k) return parts.join(", ") || t("notAvailable") } }, { id: "plugins", entropy: 2, stable: false, device: false, collect: collectPlugins, format: function (v) { return v.plugins ? v.plugins.join(", ") : v.pdfViewer !== undefined ? "pdfViewer: " + v.pdfViewer : t("notAvailable") } }, { id: "speech", entropy: 10, stable: true, device: true, collect: collectSpeech, format: function (v) { return v.count + " voix · " + v.hash.slice(0, 12) + "…" } }, { id: "intl", entropy: 6, stable: true, device: false, collect: collectIntl, format: function (v) { return "calendriers " + (v.calendars || "—") + " · " + (v.locale || "") } }, { id: "clockdrift", entropy: 8, stable: true, device: true, collect: collectClockDrift, format: function (v) { return (v.ppm > 0 ? "+" : "") + v.ppm + " ppm · écart-type " + v.sd + " ms · " + v.samples + " échantillons" }, hashOf: function (v) { return "drift=" + Math.round(v.ppm / 5) * 5 } }, { id: "clockoffset", entropy: 6, stable: true, device: true, core: true, collect: collectClockOffset, format: function (v) { return "écart " + (v.offsetMs > 0 ? "+" : "") + v.offsetMs + " ms vs serveur · RTT médian " + v.rttMs + " ms · " + v.samples + " éch." }, hashOf: function (v) { return "off=" + Math.round(v.offsetMs / 1000) * 1000 } }, { id: "workerfonts", entropy: 14, stable: true, device: true, collect: collectWorkerFonts, format: function (v) { return v.count + " polices (worker, masquées RFP)" + (v.list ? " (" + v.list.split(",").slice(0, 6).join(", ") + "…)" : "") } }, { id: "codecs", entropy: 6, stable: true, device: true, core: true, collect: collectCodecs, format: function (v) { var on = [] for (var k in v) if (v[k]) on.push(k) return on.join(", ") || "aucun" }, hashOf: function (v) { return Object.keys(v).sort().map(function (k) { return k + "=" + (v[k] ? "y" : "n") }).join(",") } }, { id: "worker", entropy: 4, stable: false, device: false, collect: collectWorker, format: function (v) { if (!v) return t("notAvailable") var parts = [] if (v.diffs && v.diffs.length) parts.push("masqué main-thread: " + v.diffs.join(",")) else parts.push("cohérent main/worker") if (v.workerBench) parts.push("worker " + v.workerBench + " ops/ms") if (v.workerSimd) parts.push("SIMD") return parts.join(" · ") } } ] } var NAME_KEYS_FR = { ua: "Agent utilisateur", platform: "Plateforme", languages: "Langues", timezone: "Fuseau horaire", screen: "Écran", hardware: "Matériel", touch: "Tactile", canvas: "Canvas 2D (5 passes)", webgl: "WebGL / GPU", webgpu: "WebGPU (carte graphique)", audio: "Traitement audio", audiocontext: "Contexte audio", fonts: "Polices", emoji: "Rendu emoji", media: "Médias CSS", scrollbar: "Barre de défilement", storage: "Stockage", storagequota: "Quota de stockage", math: "Moteur JS", timing: "Précision de l'horloge", wasm: "WebAssembly", bench: "Performance machine", webrtc: "WebRTC", battery: "Batterie", mediadevices: "Périphériques", plugins: "Plugins", speech: "Voix de synthèse", intl: "Tables de locales (Intl)", clockdrift: "Dérive d'horloge", clockoffset: "Dérive d'horloge (serveur)", workerfonts: "Polices réelles (worker)", codecs: "Codecs vidéo/audio", worker: "Contournement worker" } var HOW_KEYS_FR = { ua: "Le navigateur annonce son nom, sa version, son moteur et, via les Client Hints, l'architecture du processeur et la version exacte. Point de départ de toute empreinte, il change à chaque mise à jour mais regroupe fortement.", platform: "La famille de système d'exploitation (Windows, macOS, Linux, Android…) et, via les Client Hints à haute entropie, l'architecture (x86, arm64) et le modèle du processeur. Très stable.", languages: "La liste des langues acceptées (Accept-Language) révèle la langue et la région de l'utilisateur, souvent plus précise que l'IP. La locale exacte choisie par Intl précise encore le signal.", timezone: "Le fuseau horaire (Intl) et le décalage UTC permettent de situer l'utilisateur. La détection de l'heure d'été renseigne sur le pays. Deux appareils identiques ne partagent un fuseau que dans la même zone.", screen: "La résolution, la surface disponible, la profondeur de couleur, le ratio de pixels (dpr) et l'orientation distinguent un téléphone d'un ordinateur. Valeurs matérielles, stables.", hardware: "Le nombre de cœurs CPU (hardwareConcurrency) et la mémoire vive déclarée (deviceMemory). Les navigateurs récents arrondissent ces valeurs pour créer des groupes de milliers d'utilisateurs.", touch: "Le nombre de points de contact simultanés et la précision du pointeur (fine/coarse) séparent écrans tactiles et appareils à souris. Très stable sur mobile.", canvas: "Cinq dessins différents (dégradés, transformations, modes de fusion, textes, emoji) sont rendus puis hachés. Le rendu dépend du moteur, de l'anti-aliasing, du système et du matériel : c'est l'un des signaux les plus discriminants.", webgl: "La carte graphique annonce son constructeur et son modèle via WebGL, ainsi que des dizaines de limites matérielles (taille de texture, précision des shaders). Un triangle est rendu puis ses pixels sont relus et hachés : une vraie signature du pilote GPU.", webgpu: "La nouvelle API graphique expose l'adaptateur GPU : constructeur, architecture, nom du modèle et limite matérielles (taille de texture, tampons). Encore plus précis que WebGL, très stable.", audio: "Le navigateur génère un son hors ligne via l'API WebAudio et on analyse la forme d'onde résultante. Les traitements internes (compresseur, rééchantillonnage) diffèrent d'un moteur à l'autre.", audiocontext: "Un contexte audio réel expose la fréquence d'échantillonnage matérielle (souvent 44,1 kHz sur Linux, 48 kHz sur Windows) et les latences du pilote audio. Ce sont des signatures de la carte son et de l'OS.", fonts: "On mesure la largeur d'un texte dans une soixantaine de polices : seules celles installées sur le système provoquent une largeur différente. La liste des polices est un marqueur très personnel (logiciels installés, OS, langue).", emoji: "Les emoji sont rendus par une police dédiée du système (Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji…). La présence et la forme de chaque emoji trahissent l'OS et les polices installées.", media: "Les media queries (thème sombre, réduction des animations, précision du pointeur, gamut de couleurs, forced-colors, display-mode) révèlent le système et l'affichage, y compris sur des navigateurs à empreinte artificielle.", scrollbar: "La largeur de la barre de défilement native est mesurée via un élément hors écran. Elle dépend de l'OS et du thème (par ex. 15 px sur GNOME, 17 px sur Windows). Signal très stable.", storage: "La disponibilité de localStorage, sessionStorage et IndexedDB varie selon les navigateurs et les modes de navigation privée. Certains navigateurs bloquent ces APIs en mode strict.", storagequota: "L'API Storage estime le quota total et l'usage du navigateur. Le quota dépend de la taille du disque et du navigateur : il révèle une classe de capacité, pas une valeur exacte.", math: "Les fonctions mathématiques (sin, cos, log, sinh…) produisent des résultats légèrement différents selon le moteur JavaScript. On compare les 17 chiffres significatifs : c'est une empreinte du moteur, impossible à falsifier depuis JavaScript.", timing: "La résolution de performance.now() et de Date.now() diffère selon les navigateurs, et les protections anti-empreinte l'arrondissent volontairement (ex. 100 ms avec RFP). Elle révèle le moteur et la présence d'une protection.", wasm: "Le support de WebAssembly et de ses extensions (SIMD, threads, mémoire bulk) dépend du moteur et de la plateforme. La validation de modules binaires permet de tester chaque fonctionnalité sans exécuter de code.", bench: "Un calcul répété mesure la vitesse du processeur (opérations par milliseconde). Signal lié au matériel mais sensible à la charge de la machine : il reste utile pour regrouper des machines comparables.", webrtc: "L'API WebRTC peut exposer des adresses locales (candidats ICE) normalement invisibles sur Internet. Les navigateurs récents les masquent avec des noms mDNS pour limiter la fuite.", battery: "Le niveau et l'état de charge de la batterie (API dépréciée) peuvent servir de signal. Sa forte variabilité la rend utile pour distinguer appareils fixes et mobiles.", mediadevices: "La liste des caméras, micros et sorties audio (enumerateDevices) donne un aperçu du matériel présent, sans demander de permission.", plugins: "L'ancienne liste des plugins a longtemps identifié les navigateurs de bureau. Elle est aujourd'hui presque vide partout, mais l'état du visualiseur PDF reste exploitable.", speech: "L'API SpeechSynthesis liste les voix de synthèse installées sur le système (identifiant unique par voix). La liste dépend de l'OS et des paquets installés : c'est un marqueur matériel très stable.", intl: "Les tables de locales du moteur (calendriers, systèmes de numération, unités, collations supportés via Intl.supportedValuesOf) diffèrent selon la version et la plateforme du navigateur.", clockdrift: "La dérive entre les horloges monotone (performance.now) et temps réel (Date.now) est intéressante en théorie, mais RFP quantifie Date.now() sur une grille grossière : la pente mesurée devient aléatoire à chaque chargement (ex. +7261 ppm puis +1480 ppm dans LibreWolf, contre -5 ppm dans Brave). Ce signal est affiché mais exclu du ZPD car il ne survit pas à RFP.", clockoffset: "Le serveur renvoie son horloge (Date.now côté serveur). En comparant huit mesures avec l'horloge locale, on estime l'écart médian (et le RTT). Un RTC calé sur NTP est stable : l'écart mesuré reste le même d'un navigateur à l'autre sur la même machine et le même réseau, à quelques millisecondes près.", workerfonts: "Le worker mesure la largeur du texte dans une soixantaine de polices via un OffscreenCanvas. Mais RFP masque aussi les polices dans le worker (liste factice : LibreWolf renvoie par exemple des polices Windows inexistantes). La liste est donc différente de celle de Brave : ce signal est affiché mais exclu du ZPD (seules les horloges, les codecs, l'audio et la couleur y figurent).", codecs: "mediaCapabilities teste le décodage de VP9, AV1 et Opus (dans le worker). Le support dépend du GPU et de l'OS bien plus que du moteur : il est stable entre navigateurs sur une même machine.", worker: "Les web workers ne voient ni le DOM ni les extensions qui patchent le thread principal. En y recomputant Math, Intl et WebAssembly puis en comparant avec le thread principal, on détecte un masquage côté page, et on obtient un benchmark CPU propre (worker), non parasité par le rendu." } var PROTECT_KEYS_FR = { ua: "Utilisez un navigateur avec rotation de l'User-Agent (Firefox avec RFP, Brave) ou une extension qui le brouille. L'objectif n'est pas de le cacher mais de le rendre identique à celui d'une foule.", platform: "Les Client Hints se neutralisent avec des en-têtes CSP (permissions-policy) côté serveur, et côté navigateur avec un user-agent uniforme. Firefox propose « Resist fingerprinting » (RFP) qui fige ces valeurs.", languages: "Choisissez une liste de langues courte et réaliste dans les réglages du navigateur, ou figez-la (RFP). Les extensions qui aléatoirisent les langues sont souvent plus traçantes que l'original.", timezone: "Les navigateurs anti-empreinte (Tor, RFP) figent le fuseau horaire à UTC. Si vous voyagez, gardez un fuseau cohérent plutôt qu'une valeur qui change à chaque connexion.", screen: "RFP (Firefox) et Tor imposent une résolution standardisée. Vous pouvez réduire la fenêtre à une taille commune, mais c'est fragile : une fenêtre unique dans une foule de fenêtres identiques est elle-même un signal.", hardware: "RFP arrondit hardwareConcurrency et deviceMemory. Ces valeurs arrondies créent des regroupements de plusieurs milliers d'utilisateurs chacun.", touch: "RFP normalise maxTouchPoints. Sur mobile, le fingerprinting tactile reste un signal fort : utilisez un navigateur anti-empreinte même sur téléphone.", canvas: "Bloquez la lecture du canvas avec une extension (CanvasBlocker) ou activez RFP. Attention : un bruit constant crée une nouvelle empreinte. Vérifiez qu'il est aléatoire à chaque lecture.", webgl: "Désactivez WebGL ou activez RFP qui renvoie un vendor/renderer générique et neutralise le rendu de test. Tor va plus loin en simulant une configuration matérielle commune.", webgpu: "WebGPU est une API très récente, pas encore neutralisée par les protections classiques. Désactivez-la si vous le pouvez (about:config dom.webgpu.enabled) ; seule Tor l'émule pour l'instant.", audio: "RFP et Tor neutralisent les différences audio en retournant une valeur fixe. Les extensions d'anti-empreinte peuvent aussi bruter l'analyse audio.", audiocontext: "La fréquence d'échantillonnage et les latences du contexte audio sont exposées par l'API elle-même. RFP neutralise partiellement ; sous Linux, changer de pilote audio (PipeWire) change ces valeurs.", fonts: "Bloquez la détection des polices avec une extension (e.g. Font Blocker) ou RFP qui limite les polices et les largeurs mesurables. Désinstaller des polices exotiques réduit le signal.", emoji: "La police emoji du système est difficile à masquer. RFP ne la neutralise pas totalement ; le meilleur réflexe est d'utiliser un navigateur anti-empreinte qui fige aussi le rendu des glyphes.", media: "RFP fige les media queries (thème clair, aucune préférence). Ces valeurs normalisées vous noient dans la foule des utilisateurs de navigateurs protégés.", scrollbar: "La largeur de barre de défilement est très difficile à masquer : elle dépend de l'OS. Les navigateurs anti-empreinte la standardisent (et le thème GNOME peut la changer).", storage: "Bloquez le stockage ou utilisez le mode privé. Attention : la simple disponibilité ou indisponibilité de ces APIs est déjà un signal de 1 à 2 bits.", storagequota: "Impossible à masquer proprement : le quota est annoncé par l'API. C'est un signal d'appareil que seules les protections de navigateur peuvent arrondir.", math: "Impossible à corriger côté utilisateur : c'est une propriété interne du moteur. Seules les protections du navigateur (Tor, RFP) qui forcent des résultats standardisés l'atténuent.", timing: "RFP arrondit volontairement la résolution de performance.now() à 100ms. Vérifiez avec ce test : si la valeur affichée est ≥ 100ms, votre navigateur est déjà protégé.", wasm: "Le support des extensions WASM dépend du moteur, pas de l'utilisateur. Tor et RFP ne le neutralisent pas ; seule la désactivation de WebAssembly (about:config javascript.options.wasm) le supprime.", bench: "La vitesse du processeur ne peut pas être masquée par un navigateur. C'est un signal d'appareil intrinsèque : les protections le réduisent mais ne l'éliminent pas.", webrtc: "Bloquez WebRTC avec une extension ou les réglages du navigateur, et utilisez le mode mDNS (défaut de Chrome) qui masque l'adresse IP locale derrière un nom .local.", battery: "Plus rien à faire : l'API est retirée des navigateurs. Le niveau de batterie reste parfois lisible via des astuces secondaires que RFP neutralise.", mediadevices: "Couvrez ou désactivez vos caméras et micros inutilisés : chaque périphérique présent est une information sur votre matériel. Les navigateurs anti-empreinte renvoient une liste vide.", plugins: "Rien à faire : la liste des plugins a déjà disparu. La présence du visualiseur PDF est neutralisée par RFP.", speech: "La liste des voix de synthèse est exposée par l'API et dépend des paquets installés. RFP ne la neutralise pas ; désinstaller des voix de synthèse tierces réduit le signal.", intl: "Les tables de locales sont internes au moteur, impossibles à modifier depuis une page. Seul un navigateur anti-empreinte peut les figer.", clockdrift: "Quasi impossible à spoof côté site : la dérive vient du couple matériel/OS, pas du navigateur. RFP ne fait que réduire la précision, pas la pente. Aucune protection simple.", clockoffset: "Impossible à spoof : l'écart provient du RTC local (calé par NTP) et du chemin réseau. Seul un VPN ou un réseau différent change l'écart. Aucune protection simple.", workerfonts: "RFP masque les polices même dans le worker : ce signal ne sert plus à identifier l'appareil (exclu du ZPD). Désinstaller des polices exotiques ou bloquer les workers (dom.workers.maxPerDomain) le réduit aussi, mais il n'a plus d'impact sur l'empreinte d'appareil.", codecs: "Le support des codecs dépend du GPU et du système. Seul un navigateur qui désactive les codecs matériels le change. Peu d'options côté utilisateur.", worker: "Les extensions de spoofing ne patchent pas les workers. Si vous voulez limiter le signal, bloquez les workers (about:config dom.workers.maxPerDomain) — mais c'est lourd. Le benchmark worker dépend de la charge CPU, donc peu stable dans le temps." } var NAME_KEYS_EN = { ua: "User agent", platform: "Platform", languages: "Languages", timezone: "Time zone", screen: "Screen", hardware: "Hardware", touch: "Touch", canvas: "Canvas 2D (5 passes)", webgl: "WebGL / GPU", webgpu: "WebGPU (graphics card)", audio: "Audio processing", audiocontext: "Audio context", fonts: "Fonts", emoji: "Emoji rendering", media: "CSS media", scrollbar: "Scrollbar", storage: "Storage", storagequota: "Storage quota", math: "JS engine", timing: "Clock resolution", wasm: "WebAssembly", bench: "Machine performance", webrtc: "WebRTC", battery: "Battery", mediadevices: "Devices", plugins: "Plugins", speech: "Speech voices", intl: "Locale tables (Intl)", clockdrift: "Clock drift", clockoffset: "Clock offset (server)", workerfonts: "Real fonts (worker)", codecs: "Media codecs", worker: "Worker bypass" } var HOW_KEYS_EN = { ua: "The browser announces its name, version and engine, and through Client Hints the CPU architecture and exact version. The starting point of any fingerprint, it changes at every update but groups strongly.", platform: "The operating system family (Windows, macOS, Linux, Android…) and, through high-entropy Client Hints, the CPU architecture (x86, arm64) and model. Very stable.", languages: "The list of accepted languages (Accept-Language) reveals the user's language and region, often more precisely than the IP. The exact locale chosen by Intl sharpens the signal further.", timezone: "The time zone (Intl) and UTC offset help locate the user. Daylight-saving detection reveals the country. Two identical devices only share a time zone in the same area.", screen: "Resolution, available surface, color depth, pixel ratio (dpr) and orientation distinguish a phone from a computer. Hardware values, stable over time.", hardware: "The number of CPU cores (hardwareConcurrency) and declared memory (deviceMemory). Recent browsers round these values to create groups of thousands of users.", touch: "The number of simultaneous touch points and pointer precision (fine/coarse) separate touchscreens from mouse-driven devices. Very stable on mobile.", canvas: "Five different drawings (gradients, transforms, blend modes, text, emoji) are rendered and hashed. Rendering depends on the engine, anti-aliasing, system and hardware: one of the most discriminating signals.", webgl: "The GPU announces its vendor and model through WebGL, along with dozens of hardware limits (texture size, shader precision). A triangle is rendered then its pixels are read back and hashed: a true signature of the GPU driver.", webgpu: "The new graphics API exposes the GPU adapter: vendor, architecture, model name and hardware limits (texture size, buffers). Even more precise than WebGL, very stable.", audio: "The browser generates an offline sound through the WebAudio API and the resulting waveform is analyzed. Internal processing (compressor, resampling) differs from one engine to another.", audiocontext: "A real audio context exposes the hardware sampling rate (often 44.1 kHz on Linux, 48 kHz on Windows) and the audio driver latencies. These are signatures of the sound card and OS.", fonts: "We measure the width of a text across about sixty fonts: only fonts installed on the system produce a different width. The font list is a very personal marker (installed software, OS, language).", emoji: "Emoji are rendered by a dedicated system font (Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji…). The presence and shape of each emoji betray the OS and installed fonts.", media: "Media queries (dark theme, reduced motion, pointer precision, color gamut, forced-colors, display-mode) reveal the system and display, even on artificially fingerprinted browsers.", scrollbar: "The native scrollbar width is measured with an off-screen element. It depends on the OS and theme (e.g. 15 px on GNOME, 17 px on Windows). Very stable signal.", storage: "The availability of localStorage, sessionStorage and IndexedDB varies between browsers and private browsing modes. Some browsers block these APIs in strict mode.", storagequota: "The Storage API estimates the total quota and current usage. The quota depends on disk size and browser: it reveals a capacity class, not an exact value.", math: "Math functions (sin, cos, log, sinh…) produce slightly different results depending on the JavaScript engine. We compare all 17 significant digits: an engine fingerprint impossible to spoof from JavaScript.", timing: "The resolution of performance.now() and Date.now() differs between browsers, and anti-fingerprinting protections round it on purpose (e.g. 100 ms with RFP). It reveals the engine and the presence of a protection.", wasm: "Support for WebAssembly and its extensions (SIMD, threads, bulk memory) depends on the engine and platform. Validating binary modules lets us test each feature without executing code.", bench: "A repeated computation measures CPU speed (operations per millisecond). Hardware-related but sensitive to machine load: useful to group comparable machines.", webrtc: "The WebRTC API can expose local addresses (ICE candidates) normally invisible on the Internet. Recent browsers hide them behind mDNS names to limit the leak.", battery: "The battery level and charging state (deprecated API) can be used as a signal. Its high variability makes it useful to tell fixed devices from mobile ones.", mediadevices: "The list of cameras, microphones and audio outputs (enumerateDevices) gives an overview of the present hardware, without asking permission.", plugins: "The old plugins list long identified desktop browsers. It is now almost empty everywhere, but the PDF viewer state remains usable.", speech: "The SpeechSynthesis API lists the speech synthesis voices installed on the system (unique ID per voice). The list depends on the OS and installed packages: a very stable hardware marker.", intl: "The engine's locale tables (supported calendars, numbering systems, units, collations via Intl.supportedValuesOf) differ by browser version and platform.", clockdrift: "The drift between the monotonic clock (performance.now) and the realtime clock (Date.now) is interesting in theory, but RFP quantizes Date.now() on a coarse grid: the measured slope becomes random on every load (e.g. +7261 ppm then +1480 ppm in LibreWolf, vs -5 ppm in Brave). This signal is displayed but excluded from the ZPD because it does not survive RFP.", clockoffset: "The server returns its clock (Date.now server-side). Comparing eight measurements with the local clock estimates the median offset (and RTT). An NTP-synced RTC is stable: the measured offset stays the same across browsers on the same machine and network, within a few milliseconds.", workerfonts: "The worker measures the width of a text across about sixty fonts via an OffscreenCanvas. But RFP also masks fonts inside the worker (fake list: LibreWolf for example returns non-existent Windows fonts). The list therefore differs from Brave's: this signal is displayed but excluded from the ZPD (only clocks, codecs, audio and color depth are in it).", codecs: "mediaCapabilities tests decoding of VP9, AV1 and Opus (inside the worker). Support depends on the GPU and OS far more than on the engine: it is stable across browsers on the same machine.", worker: "Web workers see neither the DOM nor the extensions that patch the main thread. Recomputing Math, Intl and WebAssembly in a worker and comparing with the main thread detects page-side masking, and yields a clean (worker) CPU benchmark unaffected by rendering." } var PROTECT_KEYS_EN = { ua: "Use a browser with User-Agent rotation (Firefox with RFP, Brave) or an extension that garbles it. The goal is not to hide it but to make it identical to a crowd's.", platform: "Client Hints are neutralized with CSP headers (permissions-policy) on the server side, and with a uniform user agent on the browser side. Firefox offers “Resist fingerprinting” (RFP) which freezes these values.", languages: "Choose a short, realistic list of languages in your browser settings, or freeze it (RFP). Extensions that randomize languages are often more trackable than the original.", timezone: "Anti-fingerprinting browsers (Tor, RFP) freeze the time zone to UTC. If you travel, keep a consistent time zone rather than a value that changes at every connection.", screen: "RFP (Firefox) and Tor impose a standardized resolution. You can also reduce your window to a common size, but it is fragile: a single unique window inside a crowd of identical ones is itself a signal.", hardware: "RFP rounds hardwareConcurrency and deviceMemory. These rounded values create groups of several thousand users each.", touch: "RFP normalizes maxTouchPoints. On mobile, touch fingerprinting remains a strong signal: use an anti-fingerprinting browser on your phone too.", canvas: "Block canvas reads with an extension (CanvasBlocker) or enable RFP. Beware: constant noise creates a new fingerprint. Make sure it is random at every read.", webgl: "Disable WebGL or enable RFP, which returns a generic vendor/renderer and neutralizes the test render. Tor goes further by simulating a common hardware configuration.", webgpu: "WebGPU is a very recent API, not yet neutralized by classic protections. Disable it if you can (about:config dom.webgpu.enabled); only Tor emulates it so far.", audio: "RFP and Tor neutralize audio differences by returning a fixed value. Anti-fingerprinting extensions can also fuzz the audio analysis.", audiocontext: "The sampling rate and latencies of the audio context are exposed by the API itself. RFP neutralizes partially; on Linux, switching audio drivers (PipeWire) changes these values.", fonts: "Block font detection with an extension (e.g. Font Blocker) or RFP, which limits fonts and measurable widths. Uninstalling exotic fonts reduces the signal.", emoji: "The system emoji font is hard to mask. RFP does not fully neutralize it; the best reflex is an anti-fingerprinting browser that also freezes glyph rendering.", media: "RFP freezes media queries (light theme, no preference). These normalized values drown you in the crowd of protected-browser users.", scrollbar: "Scrollbar width is very hard to mask: it depends on the OS. Anti-fingerprinting browsers standardize it (and the GNOME theme can change it).", storage: "Block storage or use private mode. Beware: the mere availability or unavailability of these APIs is already a 1–2 bit signal.", storagequota: "Cannot be properly masked: the quota is announced by the API. It is a device signal that only browser protections can round.", math: "Impossible to fix from the user side: it is an internal property of the engine. Only browser protections (Tor, RFP) that force standardized results mitigate it.", timing: "RFP deliberately rounds the resolution of performance.now() to 100ms. Check with this test: if the displayed value is ≥ 100ms, your browser is already protected.", wasm: "WASM extension support depends on the engine, not the user. Tor and RFP do not neutralize it; only disabling WebAssembly (about:config javascript.options.wasm) removes it.", bench: "CPU speed cannot be masked by a browser. It is an intrinsic device signal: protections reduce it but do not eliminate it.", webrtc: "Block WebRTC with an extension or browser settings, and use mDNS mode (Chrome default) which hides your local IP behind a .local name.", battery: "Nothing to do anymore: the API is being removed from browsers. The battery level is sometimes still readable via secondary tricks that RFP neutralizes.", mediadevices: "Cover or disable unused cameras and microphones: every present device is information about your hardware. Anti-fingerprinting browsers return an empty list.", plugins: "Nothing to do: the plugins list has already disappeared. The presence of the PDF viewer is neutralized by RFP.", speech: "The list of speech voices is exposed by the API and depends on installed packages. RFP does not neutralize it; uninstalling third-party voices reduces the signal.", intl: "Locale tables are internal to the engine, impossible to modify from a page. Only an anti-fingerprinting browser can freeze them.", clockdrift: "Almost impossible to spoof from the site side: the drift comes from the hardware/OS pair, not the browser. RFP only reduces precision, not the slope. No simple protection.", clockoffset: "Impossible to spoof: the offset comes from the local RTC (NTP-disciplined) and the network path. Only a VPN or a different network changes it. No simple protection.", workerfonts: "RFP masks fonts even inside the worker: this signal no longer identifies the device (excluded from the ZPD). Uninstalling exotic fonts or blocking workers (dom.workers.maxPerDomain) reduces it too, but it no longer affects the device fingerprint.", codecs: "Codec support depends on the GPU and system. Only a browser that disables hardware codecs changes it. Few user-side options.", worker: "Spoofing extensions do not patch workers. If you want to limit the signal, block workers (about:config dom.workers.maxPerDomain) — but that is heavy. The worker benchmark depends on CPU load, so it is less stable over time." } var NAME_KEYS = LANG === "en" ? NAME_KEYS_EN : NAME_KEYS_FR var HOW_KEYS = LANG === "en" ? HOW_KEYS_EN : HOW_KEYS_FR var PROTECT_KEYS = LANG === "en" ? PROTECT_KEYS_EN : PROTECT_KEYS_FR function renderSignals(signals) { var grid = $id("ft-signals") if (!grid) return grid.textContent = "" for (var i = 0; i < signals.length; i++) { var s = signals[i] var card = el("article", "fp-card" + (s.value === null ? " fp-card-na" : "")) var head = el("div", "fp-card-head") var name = el("h3", "fp-card-name", NAME_KEYS[s.id] || s.id) head.appendChild(name) var badge = el("span", "fp-badge fp-badge-" + (s.stable ? "stable" : "volatile")) badge.textContent = s.stable ? t("stable") : t("volatile") head.appendChild(badge) if (s.core) { var coreTag = el("span", "fp-badge fp-badge-core", t("core")) head.appendChild(coreTag) } card.appendChild(head) var val = el("p", "fp-card-value") if (s.value === null) { val.textContent = t("notAvailable") } else { val.textContent = s.format ? s.format(s.value) : String(s.value) } card.appendChild(val) var meta = el("div", "fp-card-meta") var bits = el("span", "fp-bits") bits.textContent = "≈ " + formatBits(s.entropy) + " " + t("bits") meta.appendChild(bits) var db = el("span", "fp-badge " + (s.device ? "fp-badge-device" : "fp-badge-browser")) db.textContent = s.device ? t("device") : t("browser") meta.appendChild(db) card.appendChild(meta) var details = el("details", "fp-card-details") var sum = el("summary", "", t("signalsHow")) details.appendChild(sum) var how = el("p", "", HOW_KEYS[s.id] || "") details.appendChild(how) var prot = el("div", "fp-card-protect") var protTitle = el("strong", "", t("signalsProtect") + " :") prot.appendChild(protTitle) prot.appendChild(el("p", "", PROTECT_KEYS[s.id] || "")) details.appendChild(prot) card.appendChild(details) grid.appendChild(card) } } function revealSignals() { var section = $id("ft-signals-section") if (section) section.classList.remove("fp-hidden") } function renderResult(args) { var idEl = $id("ft-id") var devIdEl = $id("ft-device-id") var levelEl = $id("ft-level") var devLevelEl = $id("ft-device-level") var bitsEl = $id("ft-bits") var devBitsEl = $id("ft-device-bits") var brBitsEl = $id("ft-browser-bits") var popEl = $id("ft-pop") var scoreEl = $id("ft-score") var stableEl = $id("ft-stable") var devStableEl = $id("ft-device-stable") var fmtId = function (hex) { return hex.slice(0, 16).toUpperCase().replace(/(.{4})(?=.)/g, "$1-") } if (idEl) idEl.textContent = "ZFP-" + fmtId(args.fullHash) if (devIdEl) devIdEl.textContent = "ZPD-" + fmtId(args.deviceHash) if (levelEl) levelEl.textContent = args.level.label if (devLevelEl) devLevelEl.textContent = args.deviceLevel.label if (bitsEl) bitsEl.textContent = formatBits(args.bits) + " " + t("bits") if (devBitsEl) devBitsEl.textContent = formatBits(args.deviceBits) + " " + t("bits") if (brBitsEl) brBitsEl.textContent = formatBits(args.browserBits) + " " + t("bits") if (popEl) popEl.textContent = args.population if (scoreEl) { scoreEl.textContent = Math.round((1 - Math.pow(2, -args.bits)) * 10000) / 100 + "%" } if (stableEl) stableEl.textContent = args.stableCount + " / " + args.total + " " + t("stableCount") if (devStableEl) devStableEl.textContent = args.deviceStable + " / " + args.deviceTotal + " " + t("deviceStableCount") var bar = $id("ft-bar") if (bar) bar.style.width = Math.min(100, Math.max(2, args.bits)) + "%" var devBar = $id("ft-device-bar") if (devBar) devBar.style.width = Math.min(100, Math.max(2, args.deviceBits)) + "%" } function computeLevel(bits) { if (bits >= 90) return { key: "extreme", label: t("levels.extreme"), min: 90 } if (bits >= 60) return { key: "high", label: t("levels.high"), min: 60 } if (bits >= 32) return { key: "moderate", label: t("levels.moderate"), min: 32 } return { key: "low", label: t("levels.low"), min: 0 } } var WINDOWS_FONTS = ["Calibri", "Cambria", "Segoe UI", "Tahoma", "Verdana", "Georgia", "Palatino Linotype", "Comic Sans MS", "Microsoft YaHei", "Arial Narrow"] var LINUX_FONTS = ["DejaVu Sans", "DejaVu Serif", "Liberation Sans", "Liberation Serif", "Noto Sans", "Noto Serif", "Ubuntu", "Ubuntu Mono", "FreeSans"] function detectSpoof(collected) { try { var worker = collected.worker if (worker && worker.diffs && worker.diffs.length) return true } catch (e) {} try { var real = collected.workerfonts var mainF = collected.fonts if (real && mainF && mainF.list && real.list) { var realSet = {} var rl = String(real.list).split(",") for (var a = 0; a < rl.length; a++) realSet[rl[a]] = 1 var ml = String(mainF.list).split(",") for (var b = 0; b < ml.length; b++) { if (!realSet[ml[b]]) return true } } } catch (e) {} try { var plat = collected.platform var fonts = collected.fonts if (!plat || !fonts || !fonts.list) return false var platStr = String(plat.platform || plat.hintPlatform || "") + " " + String(plat.hintPlatform || "") var isWin = /win|windows/i.test(platStr) if (!isWin) return false var list = String(fonts.list).split(",") var linuxHits = 0 for (var i = 0; i < list.length; i++) { if (LINUX_FONTS.indexOf(list[i]) > -1) linuxHits++ } return linuxHits > 0 } catch (e) { return false } } function renderSpoof(collected) { if (!detectSpoof(collected)) return var res = $id("ft-result") if (!res) return var box = el("div", "fp-spoof") var title = el("strong", "", "⚠ " + t("spoofTitle")) box.appendChild(title) box.appendChild(el("p", "", t("spoofText"))) res.insertBefore(box, res.firstChild) } function renderDebug(defs, collected) { if (!/\bdebug\b/.test(window.location.search)) return var box = document.createElement("details") box.className = "fp-debug" var sum = document.createElement("summary") sum.textContent = "Debug : valeurs brutes des signaux" box.appendChild(sum) var pre = document.createElement("pre") var lines = [] for (var i = 0; i < defs.length; i++) { var d = defs[i] var v = collected[d.id] var line = d.id + " [" + (d.stable ? "stable" : "volatile") + "/" + (d.device ? "device" : "browser") + (d.core ? "/core" : "") + "] = " + (v === null || v === undefined ? "null" : JSON.stringify(v)) if (v !== null && v !== undefined && d.hashOf) line += " HASH => " + d.hashOf(v) lines.push(line) } pre.textContent = lines.join("\n") box.appendChild(pre) var res = $id("ft-result") if (res) res.appendChild(box) } var STORAGE_KEY = "fpTestState" var MAX_RUNS = 3 function getTestState() { try { return JSON.parse(sessionStorage.getItem(STORAGE_KEY)) } catch (e) { return null } } function setTestState(state) { try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch (e) {} } function clearTestState() { try { sessionStorage.removeItem(STORAGE_KEY) } catch (e) {} } function fmtId(hex) { return hex.slice(0, 16).toUpperCase().replace(/(.{4})(?=.)/g, "$1-") } function buildRunData(collected, fullHash, deviceHash, args, defs) { var d = { zfp: "ZFP-" + fmtId(fullHash), zpd: "ZPD-" + fmtId(deviceHash), fullHash: fullHash, deviceHash: deviceHash, bits: args.bits, deviceBits: args.deviceBits, browserBits: args.browserBits, timestamp: Date.now(), spoof: detectSpoof(collected), signalHashes: {} } for (var i = 0; i < defs.length; i++) { var s = defs[i] var v = collected[s.id] if (v !== null && v !== undefined) { d.signalHashes[s.id] = { hash: s.hashOf ? s.hashOf(v) : stableString(v), core: !!s.core, stable: !!s.stable, device: !!s.device, entropy: s.entropy } } } return d } function computeSecurityAnalysis(runs) { if (!runs || !runs.length) return null var last = runs[runs.length - 1] var defs = signalDefs() var protections = [] var recoList = [] var spoofDetected = false for (var r = 0; r < runs.length; r++) { if (runs[r].spoof) spoofDetected = true } var canvasStable = true if (runs.length >= 2 && last.signalHashes.canvas && runs[0].signalHashes.canvas) { canvasStable = last.signalHashes.canvas.hash === runs[0].signalHashes.canvas.hash } protections.push({ name: "Canvas", status: spoofDetected ? "protected" : (canvasStable ? "exposed" : "protected"), detail: spoofDetected ? t("protected") : (canvasStable ? t("exposed") : t("protected")) }) if (!spoofDetected) recoList.push(LANG === "fr" ? "Activez le masquage canvas (RFP ou extension)." : "Enable canvas masking (RFP or extension).") var workerDiffs = false try { if (runs.length && runs[runs.length - 1].signalHashes.worker) { var wh = runs[runs.length - 1].signalHashes.worker.hash workerDiffs = wh.indexOf("math") > -1 || wh.indexOf("intl") > -1 } } catch (e) {} protections.push({ name: LANG === "fr" ? "Polices" : "Fonts", status: workerDiffs ? "protected" : "exposed", detail: workerDiffs ? t("protected") : t("exposed") }) if (!workerDiffs) recoList.push(LANG === "fr" ? "Installez un bloqueur d'empreinte (uBlock, LibreWolf, Brave)." : "Install an anti-fingerprint blocker (uBlock, LibreWolf, Brave).") var webglNull = !last.signalHashes.webgl protections.push({ name: "WebGL", status: webglNull ? "protected" : "exposed", detail: webglNull ? (LANG === "fr" ? "Bloqué" : "Blocked") : t("exposed") }) if (!webglNull) recoList.push(LANG === "fr" ? "Bloquez WebGL dans votre navigateur (about:config)." : "Block WebGL in your browser settings.") var timerReduced = false try { var sig = last.signalHashes.timing if (sig && sig.hash.indexOf("?") > -1) timerReduced = true } catch (e) {} protections.push({ name: LANG === "fr" ? "Précision des timers" : "Timer precision", status: timerReduced ? "protected" : "exposed", detail: timerReduced ? t("protected") : t("exposed") }) if (!timerReduced) recoList.push(LANG === "fr" ? "Activez privacy.resistFingerprinting (about:config)." : "Enable resistFingerprinting (about:config).") var hwRounded = false try { var cores = last.signalHashes.hardware if (cores && (cores.hash.indexOf("2") > -1 || cores.hash.indexOf("4") > -1)) hwRounded = true } catch (e) {} protections.push({ name: LANG === "fr" ? "Matériel" : "Hardware", status: hwRounded ? "protected" : "exposed", detail: hwRounded ? t("protected") : t("exposed") }) var screenSpoofed = false try { var sc = last.signalHashes.screen if (sc && sc.hash) screenSpoofed = true } catch (e) {} protections.push({ name: LANG === "fr" ? "Écran" : "Screen", status: screenSpoofed ? "protected" : "exposed", detail: screenSpoofed ? t("protected") : t("exposed") }) protections.push({ name: "Audio", status: last.signalHashes.audio ? "exposed" : "protected", detail: last.signalHashes.audio ? t("exposed") : t("protected") }) if (last.signalHashes.audio) recoList.push(LANG === "fr" ? "Activez l'opacification audio (RFP ou AudioContext)." : "Enable audio obfuscation (RFP or AudioContext).") var stabilityScore = 100 if (runs.length >= 2) { var changed = 0 var total = 0 for (var i = 0; i < defs.length; i++) { if (!defs[i].stable) continue total++ var h0 = runs[0].signalHashes[defs[i].id] var h1 = runs[runs.length - 1].signalHashes[defs[i].id] if (h0 && h1 && h0.hash !== h1.hash) changed++ else if ((!h0) !== (!h1)) changed++ } stabilityScore = total > 0 ? Math.round(((total - changed) / total) * 100) : 100 } var protectedCount = 0 for (var p = 0; p < protections.length; p++) { if (protections[p].status === "protected") protectedCount++ } var protectionScore = Math.round((protectedCount / protections.length) * 100) var secScore = Math.round(protectionScore * 0.65 + stabilityScore * 0.35) return { protections: protections, protectionScore: protectionScore, stabilityScore: stabilityScore, secScore: secScore, recommendations: recoList, zfpStable: runs.length >= 2 ? runs[0].zfp === runs[runs.length - 1].zfp : true, zpdStable: runs.length >= 2 ? runs[0].zpd === runs[runs.length - 1].zpd : true } } function showMultiRunResults(runs) { var hero = $id("ft-hero") if (hero) hero.classList.add("fp-hidden") var result = $id("ft-result") if (result) result.classList.add("fp-hidden") var multi = $id("ft-multi") if (!multi) return multi.classList.remove("fp-hidden") var analysis = computeSecurityAnalysis(runs) if (!analysis) return var secEl = $id("ft-sec-score") if (secEl) secEl.textContent = analysis.secScore + " / 100" var stabEl = $id("ft-stab-score") if (stabEl) stabEl.textContent = analysis.stabilityScore + " %" var introEl = $id("ft-multi-intro") if (introEl) introEl.textContent = t("multiIntro") var zfpEl = $id("ft-multi-zfp") if (zfpEl) zfpEl.textContent = runs[0].zfp var zpdEl = $id("ft-multi-zpd") if (zpdEl) zpdEl.textContent = runs[0].zpd var zfpSub = $id("ft-zfp-sub") if (zfpSub) zfpSub.textContent = t("zfpSub") var zpdSub = $id("ft-zpd-sub") if (zpdSub) zpdSub.textContent = t("zpdSub") var zfpDesc = $id("ft-zfp-desc") if (zfpDesc) zfpDesc.textContent = t("zfpDesc") var zpdDesc = $id("ft-zpd-desc") if (zpdDesc) zpdDesc.textContent = t("zpdDesc") function fpStatus(runs, key) { var hashes = [] for (var i = 0; i < runs.length; i++) hashes.push(runs[i][key]) var unique = {} for (var i = 0; i < hashes.length; i++) unique[hashes[i]] = true var count = Object.keys(unique).length if (count === hashes.length) return { cls: "fp-status-protected", key: "protected" } if (count === 1) return { cls: "fp-status-vulnerable", key: "vulnerable" } return { cls: "fp-status-partial", key: "partial" } } var zfpSt = fpStatus(runs, "zfp") var zpdSt = fpStatus(runs, "zpd") var zfpStatusEl = $id("ft-zfp-status") if (zfpStatusEl) { zfpStatusEl.textContent = t(zfpSt.key); zfpStatusEl.className = "fp-id-card-status " + zfpSt.cls } var zpdStatusEl = $id("ft-zpd-status") if (zpdStatusEl) { zpdStatusEl.textContent = t(zpdSt.key); zpdStatusEl.className = "fp-id-card-status " + zpdSt.cls } var table = $id("ft-multi-table") if (table) { table.textContent = "" var defs = signalDefs() var groups = { core: [], device: [], browser: [] } for (var i = 0; i < defs.length; i++) { var d = defs[i] if (!d.stable) continue var grp = d.core ? "core" : (d.device ? "device" : "browser") groups[grp].push(d) } var groupLabels = { core: t("core"), device: t("device"), browser: t("browser") } var groupOrder = ["core", "device", "browser"] for (var g = 0; g < groupOrder.length; g++) { var grpKey = groupOrder[g] var sigs = groups[grpKey] if (sigs.length === 0) continue var grpTitle = el("h3", "fp-multi-grp-title", groupLabels[grpKey]) table.appendChild(grpTitle) for (var i = 0; i < sigs.length; i++) { var d = sigs[i] var card = el("div", "fp-multi-card") var header = el("div", "fp-multi-card-head") header.appendChild(el("span", "fp-multi-sig", NAME_KEYS[d.id] || d.id)) var h0 = runs[0].signalHashes[d.id] var prevHash = h0 ? h0.hash : null var stable = true for (var r = 1; r < runs.length; r++) { var hr = runs[r].signalHashes[d.id] var curHash = hr ? hr.hash : null if (curHash !== prevHash) stable = false prevHash = curHash } header.appendChild(el("span", stable ? "fp-multi-match" : "fp-multi-diff", stable ? t("stableSig") : t("instable"))) card.appendChild(header) var dots = el("div", "fp-multi-dots") for (var r = 0; r < runs.length; r++) { var hr = runs[r].signalHashes[d.id] var val = hr ? hr.hash : null var dot = el("span", "fp-multi-dot") if (r > 0) { var prevH = runs[r - 1].signalHashes[d.id] var prevV = prevH ? prevH.hash : null dot.classList.add(val === prevV ? "fp-multi-dot-match" : "fp-multi-dot-diff") } else { dot.classList.add("fp-multi-dot-match") } var label = el("span", "fp-multi-dot-label", LANG === "fr" ? "T" + (r + 1) : "R" + (r + 1)) var valLabel = el("span", "fp-multi-dot-val", val ? val.slice(0, 10) + "…" : "—") dot.appendChild(label) dot.appendChild(valLabel) dots.appendChild(dot) } card.appendChild(dots) table.appendChild(card) } } } var protBox = $id("ft-protection") if (protBox) { protBox.textContent = "" var protTitle = el("h3", "", t("whatProtected")) protBox.appendChild(protTitle) for (var p = 0; p < analysis.protections.length; p++) { var pr = analysis.protections[p] var row = el("div", "fp-prot-row") row.appendChild(el("span", "fp-prot-name", pr.name)) var badge = el("span", "fp-badge fp-badge-" + (pr.status === "protected" ? "stable" : "volatile"), pr.detail) row.appendChild(badge) protBox.appendChild(row) } } var recoBox = $id("ft-reco") if (recoBox) { recoBox.textContent = "" if (analysis.recommendations.length) { recoBox.appendChild(el("h3", "", t("recoTitle"))) for (var rc = 0; rc < analysis.recommendations.length; rc++) { recoBox.appendChild(el("p", "fp-reco-item", "→ " + analysis.recommendations[rc])) } } } var restartBtn = $id("ft-restart") if (restartBtn) { restartBtn.textContent = t("restart") restartBtn.addEventListener("click", function () { clearTestState() location.reload() }) } var whatSection = $id("ft-what-section") if (whatSection) whatSection.classList.add("fp-hidden") var privacyNote = $id("ft-privacy-note") if (privacyNote) privacyNote.classList.add("fp-hidden") revealSignals() } function run() { var btn = $id("ft-run") var status = $id("ft-status") var result = $id("ft-result") if (btn) btn.disabled = true if (status) status.textContent = t("running") if (result) result.classList.add("fp-hidden") var defs = signalDefs() var collected = {} var jobs = defs.map(function (d) { return Promise.resolve() .then(function () { var v = d.collect() if (v && v.then) { var t = d.id === "clockoffset" ? 9000 : 3000 return timeout(v, t) } return v }) .then(function (v) { collected[d.id] = v === undefined || v === null ? null : v }) .catch(function () { collected[d.id] = null }) }) Promise.all(jobs).then(function () { var totalBits = 0 var deviceBits = 0 var browserBits = 0 var stableCount = 0 var total = 0 var deviceStable = 0 var deviceTotal = 0 for (var i = 0; i < defs.length; i++) { var d = defs[i] var v = collected[d.id] if (v !== null) { total++ totalBits += d.entropy if (d.stable) stableCount++ if (d.core) { deviceTotal++ deviceBits += d.entropy if (d.stable) deviceStable++ } else { browserBits += d.entropy } } } var parts = [] var devParts = [] for (var j = 0; j < defs.length; j++) { var dj = defs[j] if (collected[dj.id] !== null && dj.stable) { var vStr = dj.hashOf ? dj.hashOf(collected[dj.id]) : stableString(collected[dj.id]) parts.push(dj.id + "=" + vStr) if (dj.core) devParts.push(dj.id + "=" + vStr) } } var fullRaw = parts.sort().join("|") var devRaw = devParts.sort().join("|") var level = computeLevel(totalBits) var deviceLevel = computeLevel(deviceBits) var population = formatPopulation(totalBits) sha256Hex(fullRaw).then(function (fullHash) { sha256Hex(devRaw).then(function (deviceHash) { renderSignals(defs.map(function (d) { return { id: d.id, entropy: d.entropy, stable: d.stable, device: d.device, core: d.core, value: collected[d.id], format: d.format } })) renderResult({ fullHash: fullHash, deviceHash: deviceHash, bits: totalBits, deviceBits: deviceBits, browserBits: browserBits, population: population, level: level, deviceLevel: deviceLevel, stableCount: stableCount, total: total, deviceStable: deviceStable, deviceTotal: deviceTotal }) renderSpoof(collected) renderDebug(defs, collected) var testState = getTestState() if (testState && testState.runs) { var runData = buildRunData(collected, fullHash, deviceHash, { bits: totalBits, deviceBits: deviceBits, browserBits: browserBits }, defs) testState.runs.push(runData) setTestState(testState) if (testState.runs.length >= testState.target) { clearTestState() showMultiRunResults(testState.runs) if (status) status.textContent = "" if (btn) btn.classList.add("fp-hidden") if (result) result.classList.add("fp-hidden") } else { if (status) status.textContent = t("tourDone").replace("{n}", testState.runs.length).replace("{t}", testState.target) if (btn) btn.disabled = true setTimeout(function () { location.reload() }, 1500) } } else { if (status) status.textContent = "" if (btn) { btn.disabled = false btn.textContent = t("again") } if (result) result.classList.remove("fp-hidden") revealSignals() } }) }) }) } function init() { var btn = $id("ft-run") if (!btn) return var testState = getTestState() if (testState && testState.runs && testState.runs.length >= testState.target) { showMultiRunResults(testState.runs) clearTestState() btn.classList.add("fp-hidden") return } btn.textContent = testState && testState.runs ? t("tourN").replace("{n}", testState.runs.length + 1).replace("{t}", testState.target) : t("run3") btn.addEventListener("click", function () { if (!getTestState()) { setTestState({ runs: [], target: MAX_RUNS }) } run() }) try { collectClockOffset() getWorkerData() } catch (e) {} if (testState && testState.runs) { var status = $id("ft-status") if (status) status.textContent = t("tourRunning").replace("{n}", testState.runs.length + 1).replace("{t}", testState.target) setTimeout(function () { run() }, 600) } var note = $id("ft-privacy-note") if (note) note.textContent = t("privacyNote") var intro = $id("ft-intro") if (intro) intro.textContent = t("intro") var idHow = $id("ft-id-how") if (idHow) idHow.textContent = t("idHow") var devHow = $id("ft-device-how") if (devHow) devHow.textContent = t("deviceHow") var multiTitle = $id("ft-multi-title") if (multiTitle) multiTitle.textContent = t("multiTitle") var secLbl = $id("ft-sec-lbl") if (secLbl) secLbl.textContent = t("secScore") var stabLbl = $id("ft-stab-lbl") if (stabLbl) stabLbl.textContent = t("stabScore") var whatTitle = $id("ft-what-title") if (whatTitle) whatTitle.textContent = t("whatIsTitle") var whatList = $id("ft-what") if (whatList) { whatList.textContent = "" var items = TEXTS[LANG].whatIs for (var i = 0; i < items.length; i++) { whatList.appendChild(el("p", "", items[i])) } } var signalsTitle = $id("ft-signals-title") if (signalsTitle) signalsTitle.textContent = t("signalsTitle") var entropyLabel = $id("ft-entropy-label") if (entropyLabel) entropyLabel.textContent = t("entropyLabel") var devEntropyLabel = $id("ft-device-entropy-label") if (devEntropyLabel) devEntropyLabel.textContent = t("deviceEntropyLabel") var brEntropyLabel = $id("ft-browser-entropy-label") if (brEntropyLabel) brEntropyLabel.textContent = t("browserEntropyLabel") var entropyNote = $id("ft-entropy-note") if (entropyNote) entropyNote.textContent = t("entropyNote") var idLabel = $id("ft-id-label") if (idLabel) idLabel.textContent = t("idLabel") var devIdLabel = $id("ft-device-label") if (devIdLabel) devIdLabel.textContent = t("deviceLabel") var popLabel = $id("ft-pop-label") if (popLabel) popLabel.textContent = t("population") var levelLabel = $id("ft-level-label") if (levelLabel) levelLabel.textContent = t("level") var devLevelLabel = $id("ft-device-level-label") if (devLevelLabel) devLevelLabel.textContent = t("deviceLevelLabel") var scoreLabel = $id("ft-score-label") if (scoreLabel) scoreLabel.textContent = t("scoreLabel") var estimatedNote = $id("ft-estimated-note") if (estimatedNote) estimatedNote.textContent = t("estimatedNote") var stabilityLabel = $id("ft-stability-label") if (stabilityLabel) stabilityLabel.textContent = t("stabilityLabel") } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init) } else { init() } })()