Source Code
The entire project, readable in your browser. No hidden JavaScript, no sneaky beacons. If we claimed we don't track you, this is where you check.
|
Pages/
Feeds/
Backend/
Includes/
Stylesheet/
JavaScript/
data/counter.txt and data/guestbook.txt hold visitor-supplied content
(a number and signed messages), so they are not shown here.
|
1 /* ============================================================================ 2 fingerprint.js -- Local browser fingerprinting 3 ---------------------------------------------------------------------------- 4 Builds several independent fingerprints (canvas, audio, WebGL, fonts, screen, 5 timezone, language, hardware), then hashes the whole bundle into one stable 6 SHA-256 identifier. Also produces per-signal hashes and a rough uniqueness / 7 tracking-risk estimate. 8 9 Exposes: window.EYES.fingerprint() -> Promise<object> 10 11 NOTHING is uploaded. The hash never leaves the browser. This exists purely to 12 show the visitor how identifiable they are with no cookies involved. 13 ========================================================================== */ 14 (function () { 15 "use strict"; 16 17 window.EYES = window.EYES || {}; 18 19 /* ---- SHA-256 via Web Crypto, with a pure-JS fallback ------------------- */ 20 function sha256(str) { 21 // Prefer the native, correct implementation. 22 if (window.crypto && window.crypto.subtle && window.TextEncoder) { 23 try { 24 var data = new TextEncoder().encode(str); 25 return window.crypto.subtle.digest('SHA-256', data).then(function (buf) { 26 var arr = Array.prototype.slice.call(new Uint8Array(buf)); 27 return arr.map(function (b) { return ('00' + b.toString(16)).slice(-2); }).join(''); 28 }); 29 } catch (e) { /* fall through */ } 30 } 31 // Fallback: synchronous pure-JS SHA-256 wrapped in a resolved Promise. 32 return Promise.resolve(sha256_js(str)); 33 } 34 35 /* Pure-JS SHA-256 (used only if Web Crypto is unavailable, e.g. old/insecure 36 contexts). Compact but complete implementation. */ 37 function sha256_js(ascii) { 38 function rightRotate(value, amount) { return (value >>> amount) | (value << (32 - amount)); } 39 var mathPow = Math.pow; 40 var maxWord = mathPow(2, 32); 41 var result = ''; 42 var words = []; 43 var asciiBitLength = ascii.length * 8; 44 45 var hash = sha256_js.h = sha256_js.h || []; 46 var k = sha256_js.k = sha256_js.k || []; 47 var primeCounter = k.length; 48 var isComposite = {}; 49 for (var candidate = 2; primeCounter < 64; candidate++) { 50 if (!isComposite[candidate]) { 51 for (var i = 0; i < 313; i += candidate) isComposite[i] = candidate; 52 hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0; 53 k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0; 54 } 55 } 56 57 // Reset hash to initial values each call. 58 hash = hash.slice(0, 8); 59 60 ascii += '\x80'; 61 while (ascii.length % 64 - 56) ascii += '\x00'; 62 for (i = 0; i < ascii.length; i++) { 63 var j = ascii.charCodeAt(i); 64 if (j >> 8) return ''; // ASCII only in fallback path 65 words[i >> 2] |= j << ((3 - i) % 4) * 8; 66 } 67 words[words.length] = (asciiBitLength / maxWord) | 0; 68 words[words.length] = asciiBitLength; 69 70 for (j = 0; j < words.length;) { 71 var w = words.slice(j, j += 16); 72 var oldHash = hash.slice(0); 73 74 for (i = 0; i < 64; i++) { 75 var w15 = w[i - 15], w2 = w[i - 2]; 76 var a = hash[0], e = hash[4]; 77 var temp1 = hash[7] 78 + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) 79 + ((e & hash[5]) ^ (~e & hash[6])) 80 + k[i] 81 + (w[i] = (i < 16) ? w[i] : ( 82 w[i - 16] 83 + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) 84 + w[i - 7] 85 + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) 86 ) | 0); 87 var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) 88 + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); 89 90 hash = [(temp1 + temp2) | 0].concat(hash); 91 hash[4] = (hash[4] + temp1) | 0; 92 } 93 94 for (i = 0; i < 8; i++) hash[i] = (hash[i] + oldHash[i]) | 0; 95 } 96 97 for (i = 0; i < 8; i++) { 98 for (j = 3; j + 1; j--) { 99 var b = (hash[i] >> (j * 8)) & 255; 100 result += ((b < 16) ? 0 : '') + b.toString(16); 101 } 102 } 103 return result; 104 } 105 106 /* ---- Canvas fingerprint ------------------------------------------------ */ 107 function canvasFP() { 108 try { 109 var canvas = document.createElement('canvas'); 110 canvas.width = 280; canvas.height = 60; 111 var ctx = canvas.getContext('2d'); 112 if (!ctx) return { supported: false, data: '' }; 113 114 ctx.textBaseline = 'top'; 115 ctx.font = "14px 'Arial'"; 116 ctx.textBaseline = 'alphabetic'; 117 ctx.fillStyle = '#f60'; 118 ctx.fillRect(125, 1, 62, 20); 119 ctx.fillStyle = '#069'; 120 ctx.font = "11pt no-real-font-123"; 121 ctx.fillText('WhatCanILearn <canvas> 1.0 ☠🔒', 2, 15); 122 ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; 123 ctx.font = "18pt Arial"; 124 ctx.fillText('WhatCanILearn <canvas> 1.0 ☠🔒', 4, 45); 125 126 // geometry 127 ctx.globalCompositeOperation = 'multiply'; 128 ['rgb(255,0,255)','rgb(0,255,255)','rgb(255,255,0)'].forEach(function (c, i) { 129 ctx.fillStyle = c; 130 ctx.beginPath(); 131 ctx.arc(50 + i * 80, 40, 40, 0, Math.PI * 2, true); 132 ctx.closePath(); ctx.fill(); 133 }); 134 ctx.fillStyle = 'rgb(255,0,255)'; 135 ctx.arc(75, 40, 75, 0, Math.PI * 2, true); 136 ctx.arc(75, 40, 25, 0, Math.PI * 2, true); 137 ctx.fill('evenodd'); 138 139 var data = canvas.toDataURL(); 140 return { supported: true, data: data }; 141 } catch (e) { 142 return { supported: false, data: '' }; 143 } 144 } 145 146 /* ---- Audio fingerprint (offline oscillator, no sound played) ----------- */ 147 function audioFP() { 148 return new Promise(function (resolve) { 149 var AC = window.OfflineAudioContext || window.webkitOfflineAudioContext; 150 if (!AC) { resolve({ supported: false, data: '' }); return; } 151 var done = false; 152 var t = setTimeout(function () { 153 if (!done) { done = true; resolve({ supported: true, data: 'timeout' }); } 154 }, 1500); 155 try { 156 var ctx = new AC(1, 44100, 44100); 157 var osc = ctx.createOscillator(); 158 osc.type = 'triangle'; 159 osc.frequency.value = 10000; 160 var comp = ctx.createDynamicsCompressor(); 161 comp.threshold.value = -50; 162 comp.knee.value = 40; 163 comp.ratio.value = 12; 164 comp.attack.value = 0; 165 comp.release.value = 0.25; 166 osc.connect(comp); 167 comp.connect(ctx.destination); 168 osc.start(0); 169 ctx.startRendering(); 170 ctx.oncomplete = function (ev) { 171 if (done) return; done = true; clearTimeout(t); 172 try { 173 var buf = ev.renderedBuffer.getChannelData(0); 174 var sum = 0; 175 for (var i = 4500; i < 5000; i++) sum += Math.abs(buf[i]); 176 resolve({ supported: true, data: sum.toString() }); 177 } catch (e) { resolve({ supported: true, data: 'error' }); } 178 }; 179 } catch (e) { 180 if (!done) { done = true; clearTimeout(t); resolve({ supported: false, data: '' }); } 181 } 182 }); 183 } 184 185 /* ---- WebGL fingerprint ------------------------------------------------- */ 186 function webglFP() { 187 try { 188 var canvas = document.createElement('canvas'); 189 var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 190 if (!gl) return { supported: false, data: '' }; 191 192 var parts = []; 193 var dbg = gl.getExtension('WEBGL_debug_renderer_info'); 194 if (dbg) { 195 parts.push(gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL)); 196 parts.push(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)); 197 } 198 parts.push(gl.getParameter(gl.VERSION)); 199 parts.push(gl.getParameter(gl.SHADING_LANGUAGE_VERSION)); 200 parts.push(gl.getParameter(gl.VENDOR)); 201 parts.push(gl.getParameter(gl.RENDERER)); 202 parts.push(gl.getParameter(gl.MAX_VERTEX_ATTRIBS)); 203 parts.push(gl.getParameter(gl.MAX_TEXTURE_SIZE)); 204 parts.push(gl.getParameter(gl.MAX_VIEWPORT_DIMS).join('x')); 205 parts.push(gl.getParameter(gl.MAX_RENDERBUFFER_SIZE)); 206 parts.push(gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE).join(',')); 207 parts.push(gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE).join(',')); 208 var ext = gl.getSupportedExtensions() || []; 209 parts.push(ext.sort().join(',')); 210 211 // A small render to capture pixel-level driver quirks. 212 try { 213 var buffer = gl.createBuffer(); 214 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 215 var verts = new Float32Array([-0.2,-0.9,0, 0.4,-0.26,0, 0,0.7,0]); 216 gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW); 217 parts.push('buf:ok'); 218 } catch (e) {} 219 220 return { supported: true, data: parts.join('~') }; 221 } catch (e) { 222 return { supported: false, data: '' }; 223 } 224 } 225 226 /* ==== main ============================================================== */ 227 EYES.fingerprint = function fingerprint() { 228 var nav = navigator; 229 230 // Gather synchronous signals. 231 var canvas = canvasFP(); 232 var webgl = webglFP(); 233 234 var fonts = (EYES._detectedFonts) || (function () { 235 // Reuse detect.js font logic if it already ran; otherwise a quick probe. 236 try { 237 if (EYES.lastReport && EYES.lastReport.browser && EYES.lastReport.browser.fonts) { 238 return EYES.lastReport.browser.fonts; 239 } 240 } catch (e) {} 241 return []; 242 })(); 243 244 var screenSig = [ 245 screen.width, screen.height, screen.availWidth, screen.availHeight, 246 screen.colorDepth, screen.pixelDepth, window.devicePixelRatio 247 ].join('x'); 248 249 var tzSig = (function () { 250 try { return Intl.DateTimeFormat().resolvedOptions().timeZone + '|' + new Date().getTimezoneOffset(); } 251 catch (e) { return String(new Date().getTimezoneOffset()); } 252 })(); 253 254 var langSig = ((nav.languages || [nav.language]).join(',')) + '|' + (nav.language || ''); 255 256 var hwSig = [ 257 nav.hardwareConcurrency || '?', nav.deviceMemory || '?', nav.platform || '?', 258 nav.maxTouchPoints || 0, nav.vendor || '?' 259 ].join('|'); 260 261 var uaSig = nav.userAgent || ''; 262 263 return audioFP().then(function (audio) { 264 // Per-signal hashes (parallel). 265 var signals = { 266 canvas: canvas.data, 267 audio: audio.data, 268 webgl: webgl.data, 269 fonts: fonts.join(','), 270 screen: screenSig, 271 timezone: tzSig, 272 language: langSig, 273 hardware: hwSig, 274 ua: uaSig 275 }; 276 277 var keys = Object.keys(signals); 278 var hashJobs = keys.map(function (k) { return sha256(signals[k] || '(none)'); }); 279 // The combined signal that produces the master ID. 280 var combined = keys.map(function (k) { return k + '=' + (signals[k] || ''); }).join('||'); 281 hashJobs.push(sha256(combined)); 282 283 return Promise.all(hashJobs).then(function (hashes) { 284 var out = { supported: { canvas: canvas.supported, audio: audio.supported, webgl: webgl.supported }, 285 signals: {}, id: hashes[hashes.length - 1] }; 286 keys.forEach(function (k, i) { 287 out.signals[k] = { value: signals[k], hash: hashes[i], short: hashes[i].slice(0, 16) }; 288 }); 289 290 // Uniqueness / risk heuristic. More distinguishing signals present -> 291 // more identifiable. This is a rough, honest-ish estimate, not science. 292 var entropyBits = 0; 293 if (canvas.supported && canvas.data) entropyBits += 8; 294 if (webgl.supported && webgl.data) entropyBits += 7; 295 if (audio.supported && audio.data && audio.data !== 'timeout') entropyBits += 4; 296 if (fonts.length) entropyBits += Math.min(6, Math.round(fonts.length / 6)); 297 entropyBits += 3; // screen 298 entropyBits += 2; // timezone 299 entropyBits += 2; // language 300 entropyBits += 3; // hardware 301 if (uaSig) entropyBits += 4; 302 303 var population = Math.pow(2, entropyBits); 304 out.entropyBits = entropyBits; 305 out.oneIn = population; 306 out.uniqueness = Math.min(100, Math.round((entropyBits / 40) * 100)); 307 308 var risk = 'LOW'; 309 if (entropyBits >= 30) risk = 'HIGH'; 310 else if (entropyBits >= 20) risk = 'MEDIUM'; 311 out.trackingRisk = risk; 312 313 out._collectedAt = new Date().toISOString(); 314 return out; 315 }); 316 }); 317 }; 318 319 EYES._sha256 = sha256; 320 321 })(); 322 You are reading the real file being executed on the server. What you see is what runs. |