/* TAKEONE device fingerprint — strongest guest identifier a browser will let us compute. * * Combines ~15 signals (canvas, WebGL, audio, fonts, screen, locale, hardware) into a * stable 64-char hash. Cached in localStorage AND mirrored to a `_fp` cookie so the * server sees it on every request. Falls back gracefully if any single signal fails * (private browsing, locked-down browsers, no GPU, etc). * * NOT a MAC address — browsers cannot expose MAC. This is the closest practical equivalent. */ (function () { 'use strict'; var STORAGE_KEY = '_takeone_fp'; var COOKIE_KEY = '_fp'; var COOKIE_MAX = 60 * 60 * 24 * 365 * 5; // 5 years // ── Set cookie helper ──────────────────────────────────────────── function setCookie(name, val) { try { document.cookie = name + '=' + encodeURIComponent(val) + '; Max-Age=' + COOKIE_MAX + '; Path=/; SameSite=Lax' + (location.protocol === 'https:' ? '; Secure' : ''); } catch (e) { /* sandboxed iframe etc. */ } } function readCookie(name) { var m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : null; } // ── SHA-256 (Web Crypto where available, JS fallback otherwise) ── function sha256(str) { if (window.crypto && window.crypto.subtle && window.TextEncoder) { return window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)) .then(function (buf) { return Array.from(new Uint8Array(buf)) .map(function (b) { return b.toString(16).padStart(2, '0'); }) .join(''); }); } // Minimal JS fallback (only used in ancient browsers) return Promise.resolve(jsSha256(str)); } // Tiny pure-JS SHA-256 (used only if subtleCrypto missing). Adapted from public-domain refs. function jsSha256(ascii) { function rightRotate(value, amount) { return (value >>> amount) | (value << (32 - amount)); } var mathPow = Math.pow, maxWord = mathPow(2, 32), result = ''; var words = [], asciiBitLength = ascii.length * 8; var hash = jsSha256.h = jsSha256.h || [], k = jsSha256.k = jsSha256.k || [], primeCounter = k.length; var isComposite = {}; for (var candidate = 2; primeCounter < 64; candidate++) { if (!isComposite[candidate]) { for (var i = 0; i < 313; i += candidate) isComposite[i] = candidate; hash[primeCounter] = (mathPow(candidate, .5) * maxWord) | 0; k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0; } } ascii += '\x80'; while (ascii.length % 64 - 56) ascii += '\x00'; for (i = 0; i < ascii.length; i++) { var j = ascii.charCodeAt(i); if (j >> 8) return ''; words[i >> 2] |= j << ((3 - i) % 4) * 8; } words[words.length] = ((asciiBitLength / maxWord) | 0); words[words.length] = (asciiBitLength); for (j = 0; j < words.length;) { var w = words.slice(j, j += 16), oldHash = hash; hash = hash.slice(0, 8); for (i = 0; i < 64; i++) { var w15 = w[i - 15], w2 = w[i - 2]; var a = hash[0], e = hash[4]; var temp1 = hash[7] + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) + ((e & hash[5]) ^ ((~e) & hash[6])) + k[i] + (w[i] = (i < 16) ? w[i] : ( w[i - 16] + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) + w[i - 7] + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) ) | 0); var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); hash = [(temp1 + temp2) | 0].concat(hash); hash[4] = (hash[4] + temp1) | 0; } for (i = 0; i < 8; i++) hash[i] = (hash[i] + oldHash[i]) | 0; } for (i = 0; i < 8; i++) { for (j = 3; j + 1; j--) { var b = (hash[i] >> (j * 8)) & 255; result += ((b < 16) ? 0 : '') + b.toString(16); } } return result; } // ── Signal probes ──────────────────────────────────────────────── function canvasFingerprint() { try { var c = document.createElement('canvas'); c.width = 280; c.height = 60; var ctx = c.getContext('2d'); ctx.textBaseline = 'top'; ctx.font = "14px 'Arial'"; ctx.fillStyle = '#f60'; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = '#069'; ctx.fillText('TAKEONE,fp 🎬', 2, 15); ctx.fillStyle = 'rgba(102,204,0,0.7)'; ctx.fillText('TAKEONE,fp 🎬', 4, 17); // Curved shape exposes GPU sub-pixel rounding differences ctx.beginPath(); ctx.arc(50, 30, 20, 0, Math.PI * 2, true); ctx.closePath(); ctx.fillStyle = 'rgb(255,0,255)'; ctx.fill(); return c.toDataURL(); } catch (e) { return 'canvas:err'; } } function webglFingerprint() { try { var c = document.createElement('canvas'); var gl = c.getContext('webgl') || c.getContext('experimental-webgl'); if (!gl) return 'webgl:none'; var info = gl.getExtension('WEBGL_debug_renderer_info'); var vendor = info ? gl.getParameter(info.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR); var renderer = info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER); var max = gl.getParameter(gl.MAX_TEXTURE_SIZE); var ext = (gl.getSupportedExtensions() || []).sort().join(','); return vendor + '|' + renderer + '|' + max + '|' + ext; } catch (e) { return 'webgl:err'; } } function audioFingerprint() { return new Promise(function (resolve) { try { var Ctx = window.OfflineAudioContext || window.webkitOfflineAudioContext; if (!Ctx) return resolve('audio:none'); var ctx = new Ctx(1, 44100, 44100); var osc = ctx.createOscillator(); osc.type = 'triangle'; osc.frequency.value = 10000; var compressor = ctx.createDynamicsCompressor(); ['threshold','knee','ratio','attack','release'].forEach(function (p) { if (compressor[p] && compressor[p].setValueAtTime) { compressor[p].setValueAtTime(({ threshold:-50, knee:40, ratio:12, attack:0, release:.25 })[p], ctx.currentTime); } }); osc.connect(compressor); compressor.connect(ctx.destination); osc.start(0); ctx.startRendering(); var done = false; ctx.oncomplete = function (e) { if (done) return; done = true; var sum = 0, d = e.renderedBuffer.getChannelData(0); for (var i = 4500; i < 5000; i++) sum += Math.abs(d[i]); resolve(sum.toString()); }; setTimeout(function () { if (!done) { done = true; resolve('audio:timeout'); } }, 1500); } catch (e) { resolve('audio:err'); } }); } function fontsFingerprint() { // Quick probe: list which of N common fonts the OS actually has installed, // detected by measuring text width against fallback baselines. try { var baseFonts = ['monospace','sans-serif','serif']; var testString = 'mmmmmmmmmmlli'; var testSize = '72px'; var fonts = [ 'Andale Mono','Arial','Arial Black','Arial Hebrew','Arial MT','Arial Narrow','Arial Rounded MT Bold','Arial Unicode MS', 'Bitstream Vera Sans Mono','Book Antiqua','Bookman Old Style','Calibri','Cambria','Cambria Math','Century','Century Gothic','Century Schoolbook', 'Comic Sans','Comic Sans MS','Consolas','Courier','Courier New','Geneva','Georgia','Helvetica','Helvetica Neue','Impact', 'Lucida Bright','Lucida Calligraphy','Lucida Console','Lucida Fax','LUCIDA GRANDE','Lucida Handwriting','Lucida Sans','Lucida Sans Typewriter','Lucida Sans Unicode', 'Microsoft Sans Serif','Monaco','Monotype Corsiva','MS Gothic','MS Outlook','MS PGothic','MS Reference Sans Serif','MS Sans Serif','MS Serif','MYRIAD','MYRIAD PRO', 'Palatino','Palatino Linotype','Segoe Print','Segoe Script','Segoe UI','Segoe UI Light','Segoe UI Semibold','Segoe UI Symbol','Tahoma','Times','Times New Roman','Times New Roman PS','Trebuchet MS','Verdana','Wingdings','Wingdings 2','Wingdings 3' ]; var body = document.body || document.documentElement; var span = document.createElement('span'); span.style.position = 'absolute'; span.style.left = '-9999px'; span.style.fontSize = testSize; span.textContent = testString; body.appendChild(span); var defaults = {}; baseFonts.forEach(function (b) { span.style.fontFamily = b; defaults[b] = { w: span.offsetWidth, h: span.offsetHeight }; }); var detected = []; fonts.forEach(function (f) { var diff = false; for (var i = 0; i < baseFonts.length; i++) { span.style.fontFamily = "'" + f + "'," + baseFonts[i]; if (span.offsetWidth !== defaults[baseFonts[i]].w || span.offsetHeight !== defaults[baseFonts[i]].h) { diff = true; break; } } if (diff) detected.push(f); }); body.removeChild(span); return detected.join(','); } catch (e) { return 'fonts:err'; } } function collectSignals() { var nav = window.navigator || {}; var scr = window.screen || {}; return Promise.all([audioFingerprint()]).then(function (results) { return { canvas : canvasFingerprint(), webgl : webglFingerprint(), audio : results[0], fonts : fontsFingerprint(), screen : (scr.width || 0) + 'x' + (scr.height || 0) + 'x' + (scr.colorDepth || 0), dpr : window.devicePixelRatio || 1, platform : nav.platform || '', cpu : nav.hardwareConcurrency || 0, mem : nav.deviceMemory || 0, tz : (Intl && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : '', langs : (nav.languages || []).join(','), touch : (nav.maxTouchPoints || 0) + (('ontouchstart' in window) ? 'T' : ''), pdfviewer : nav.pdfViewerEnabled ? '1' : '0', userAgent : nav.userAgent || '' }; }); } // ── Build / cache the hash ─────────────────────────────────────── function ensureFingerprint() { // 1) localStorage first (fastest path) var cached = null; try { cached = localStorage.getItem(STORAGE_KEY); } catch (e) {} // 2) cookie next (survives some localStorage wipes) if (!cached) cached = readCookie(COOKIE_KEY); if (cached && /^[a-f0-9]{64}$/.test(cached)) { setCookie(COOKIE_KEY, cached); // refresh expiry on each visit window._takeoneFp = cached; return Promise.resolve(cached); } return collectSignals().then(function (sig) { var serialised = JSON.stringify(sig); return sha256(serialised).then(function (hash) { try { localStorage.setItem(STORAGE_KEY, hash); } catch (e) {} setCookie(COOKIE_KEY, hash); window._takeoneFp = hash; // Backfill the view row that the server just inserted from the cookie-less first visit try { var pathMatch = location.pathname.match(/^\/videos\/([^\/?#]+)/); if (pathMatch) { var token = (document.querySelector('meta[name="csrf-token"]') || {}).content || ''; fetch('/videos/' + pathMatch[1] + '/identify', { method : 'POST', headers : { 'Content-Type' : 'application/json', 'X-CSRF-TOKEN' : token, 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin', body: JSON.stringify({ hash: hash }) }).catch(function () { /* best-effort */ }); } } catch (e) {} return hash; }); }); } // Kick off ASAP — but never block paint if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ensureFingerprint, { once: true }); } else { ensureFingerprint(); } })();