/** * f0ckm Anonymous OpenSSH Ed25519 Identity Manager * Provides client-side Ed25519 key generation, OpenSSH key formatting, * signing, and authentication without login. */ (function () { 'use strict'; const PKCS8_HEADER = new Uint8Array([ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20 ]); const STORAGE_KEY_PRIV = 'f0ck_anon_ssh_priv'; const STORAGE_KEY_PUB = 'f0ck_anon_ssh_pub'; const STORAGE_KEY_FP = 'f0ck_anon_ssh_fp'; function bytesToBase64(bytes) { let binary = ''; const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); } function base64ToBytes(base64) { const binary = window.atob(base64.replace(/\s+/g, '')); const len = binary.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function bytesToHex(bytes) { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); } function hexToBytes(hex) { const clean = hex.replace(/[^0-9a-fA-F]/g, ''); const bytes = new Uint8Array(clean.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16); } return bytes; } function writeUInt32BE(buf, value, offset) { buf[offset] = (value >>> 24) & 0xff; buf[offset + 1] = (value >>> 16) & 0xff; buf[offset + 2] = (value >>> 8) & 0xff; buf[offset + 3] = value & 0xff; } function readUInt32BE(buf, offset) { return ( (buf[offset] << 24) | (buf[offset + 1] << 16) | (buf[offset + 2] << 8) | buf[offset + 3] ) >>> 0; } function concatUint8Arrays(arrays) { const totalLen = arrays.reduce((acc, a) => acc + a.length, 0); const result = new Uint8Array(totalLen); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } class AnonSSH { constructor() { this.cryptoKey = null; this.pubkey = null; this.fingerprint = null; this.shortFingerprint = null; this.rawPub = null; this.rawSeed = null; this.isSessionReady = false; this.hwFingerprint = null; try { this.hwFingerprint = localStorage.getItem('f0ck_anon_hw_fp') || null; } catch (e) {} } /** * Compute a hardware-level fingerprint based on GPU, CPU, RAM, screen, and Web Audio DSP. * Survives across private browsing windows and different browsers on the same physical device. */ async getHardwareFingerprint() { if (this.hwFingerprint) return this.hwFingerprint; try { const cached = localStorage.getItem('f0ck_anon_hw_fp'); if (cached) { this.hwFingerprint = cached; return this.hwFingerprint; } } catch (e) {} try { let glVendor = ''; let glRenderer = ''; let glLimits = ''; try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (gl) { const ext = gl.getExtension('WEBGL_debug_renderer_info'); if (ext) { glVendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) || ''; glRenderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) || ''; } glLimits = [ gl.getParameter(gl.MAX_TEXTURE_SIZE) || 0, gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) || 0, gl.getParameter(gl.MAX_VERTEX_ATTRIBS) || 0, gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS) || 0, gl.getParameter(gl.MAX_VARYING_VECTORS) || 0, gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS) || 0 ].join(','); } } catch (e) {} // WebGPU adapter architecture (modern GPUs) let gpuArch = ''; try { if (navigator.gpu) { const adapter = await navigator.gpu.requestAdapter(); if (adapter && adapter.info) { gpuArch = [adapter.info.architecture, adapter.info.vendor, adapter.info.description].filter(Boolean).join(':'); } } } catch (e) {} // CPU & Memory const concurrency = navigator.hardwareConcurrency || 0; const memory = navigator.deviceMemory || 0; const platform = navigator.platform || ''; // Display, Gamut & Dynamic Range const screenInfo = [ window.screen ? window.screen.width : 0, window.screen ? window.screen.height : 0, window.screen ? window.screen.availWidth : 0, window.screen ? window.screen.availHeight : 0, window.screen ? window.screen.colorDepth : 0, window.devicePixelRatio || 1, window.matchMedia && window.matchMedia('(color-gamut: p3)').matches ? 'p3' : (window.matchMedia && window.matchMedia('(color-gamut: rec2020)').matches ? 'rec2020' : 'srgb'), window.matchMedia && window.matchMedia('(dynamic-range: high)').matches ? 'hdr' : 'sdr' ].join('x'); // Input & Peripheral Hardware (touch, mouse, stylus) const touchPoints = (typeof navigator !== 'undefined' && 'maxTouchPoints' in navigator) ? navigator.maxTouchPoints : 0; const pointerType = (window.matchMedia && window.matchMedia('(pointer: fine)').matches) ? 'fine' : ((window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ? 'coarse' : 'none'); const hoverType = (window.matchMedia && window.matchMedia('(hover: hover)').matches) ? 'hover' : 'none'; const inputHardware = `${touchPoints}:${pointerType}:${hoverType}`; // 2D Canvas Font Rasterizer & Subpixel Geometry let canvasFp = ''; try { const c2d = document.createElement('canvas'); c2d.width = 240; c2d.height = 60; const ctx = c2d.getContext('2d'); if (ctx) { ctx.textBaseline = 'alphabetic'; ctx.fillStyle = '#f60'; ctx.fillRect(10, 5, 60, 20); ctx.fillStyle = '#069'; ctx.font = '14pt Arial, sans-serif'; ctx.fillText('f0ck.dev 😃', 4, 35); ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; ctx.font = '16pt Times, serif'; ctx.fillText('f0ck.dev 😃', 20, 48); const imgData = ctx.getImageData(0, 0, 240, 60).data; let sum = 0; for (let i = 0; i < imgData.length; i += 4) { sum = (sum * 31 + imgData[i] + imgData[i+1] + imgData[i+2] + imgData[i+3]) >>> 0; } canvasFp = sum.toString(16); } } catch (e) {} // Web Audio DSP Floating-Point Math let audioFp = ''; try { const AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext; if (AudioContext) { const actx = new AudioContext(1, 44100, 44100); const osc = actx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(10000, actx.currentTime); const comp = actx.createDynamicsCompressor(); comp.threshold.setValueAtTime(-50, actx.currentTime); comp.knee.setValueAtTime(40, actx.currentTime); comp.ratio.setValueAtTime(12, actx.currentTime); comp.reduction.setValueAtTime(-20, actx.currentTime); comp.attack.setValueAtTime(0, actx.currentTime); comp.release.setValueAtTime(0.25, actx.currentTime); osc.connect(comp); comp.connect(actx.destination); osc.start(0); const renderedBuffer = await actx.startRendering(); const channel = renderedBuffer.getChannelData(0); let sum = 0; const end = Math.min(channel.length, 5000); for (let i = 4500; i < end; i++) { sum += Math.abs(channel[i] || 0); } audioFp = sum.toFixed(7); } } catch (e) {} // FPU Math Microarchitecture Precision const mathPrecision = [ Math.tan(-1e300).toString().substring(0, 10), Math.sinh(1).toString().substring(0, 10), Math.acos(0.123456789).toString().substring(0, 10) ].join(';'); const rawHardware = [ glVendor, glRenderer, glLimits, gpuArch, concurrency, memory, platform, screenInfo, inputHardware, canvasFp, audioFp, mathPrecision ].join('~~~'); const encoder = new TextEncoder(); const data = encoder.encode(rawHardware); const hashBuf = await window.crypto.subtle.digest('SHA-256', data); const hashHex = bytesToHex(new Uint8Array(hashBuf)); this.hwFingerprint = `HW:${hashHex}`; try { localStorage.setItem('f0ck_anon_hw_fp', this.hwFingerprint); } catch (e) {} return this.hwFingerprint; } catch (err) { console.warn('[ANON_SSH] Failed to compute hardware fingerprint:', err); return null; } } getDomain() { return window.f0ckDomain || window.location.hostname || 'f0ck.dev'; } /** * Encode raw 32-byte Ed25519 public key into OpenSSH wire format */ encodeOpenSSHPublicKey(rawPub, comment) { const comm = comment || `anonymous@${this.getDomain()}`; const keyType = new TextEncoder().encode('ssh-ed25519'); const wirePub = new Uint8Array(4 + keyType.length + 4 + rawPub.length); writeUInt32BE(wirePub, keyType.length, 0); wirePub.set(keyType, 4); writeUInt32BE(wirePub, rawPub.length, 4 + keyType.length); wirePub.set(rawPub, 4 + keyType.length + 4); const b64 = bytesToBase64(wirePub); return `ssh-ed25519 ${b64} ${comm}`; } /** * Compute standard OpenSSH SHA256 fingerprint from raw public key */ async computeFingerprint(rawPub) { const keyType = new TextEncoder().encode('ssh-ed25519'); const wirePub = new Uint8Array(4 + keyType.length + 4 + rawPub.length); writeUInt32BE(wirePub, keyType.length, 0); wirePub.set(keyType, 4); writeUInt32BE(wirePub, rawPub.length, 4 + keyType.length); wirePub.set(rawPub, 4 + keyType.length + 4); const hashBuffer = await window.crypto.subtle.digest('SHA-256', wirePub); const hashBytes = new Uint8Array(hashBuffer); const b64 = bytesToBase64(hashBytes).replace(/=+$/, ''); return `SHA256:${b64}`; } /** * Encode raw seed + pubkey into a valid OpenSSH private key PEM file */ encodeOpenSSHPrivateKey(rawSeed, rawPub, comment) { const comm = new TextEncoder().encode(comment || `anonymous@${this.getDomain()}`); const keyType = new TextEncoder().encode('ssh-ed25519'); const none = new TextEncoder().encode('none'); const magic = new TextEncoder().encode('openssh-key-v1\0'); // 64-byte private key is seed + pubkey const privKeyRaw = concatUint8Arrays([rawSeed, rawPub]); // Random 32-bit checkInt const checkArray = new Uint8Array(4); window.crypto.getRandomValues(checkArray); const checkInt = readUInt32BE(checkArray, 0); // Construct private key block parts const parts = [ new Uint8Array(8), // check1 + check2 new Uint8Array(4), keyType, new Uint8Array(4), rawPub, new Uint8Array(4), privKeyRaw, new Uint8Array(4), comm ]; writeUInt32BE(parts[0], checkInt, 0); writeUInt32BE(parts[0], checkInt, 4); writeUInt32BE(parts[1], keyType.length, 0); writeUInt32BE(parts[3], rawPub.length, 0); writeUInt32BE(parts[5], privKeyRaw.length, 0); writeUInt32BE(parts[7], comm.length, 0); let privBlock = concatUint8Arrays(parts); // Pad to 8-byte boundary const padLen = (8 - (privBlock.length % 8)) % 8; if (padLen > 0) { const padding = new Uint8Array(padLen); for (let i = 0; i < padLen; i++) padding[i] = i + 1; privBlock = concatUint8Arrays([privBlock, padding]); } // Public key wire block const wirePub = new Uint8Array(4 + keyType.length + 4 + rawPub.length); writeUInt32BE(wirePub, keyType.length, 0); wirePub.set(keyType, 4); writeUInt32BE(wirePub, rawPub.length, 4 + keyType.length); wirePub.set(rawPub, 4 + keyType.length + 4); // Header block const headerParts = [ magic, new Uint8Array(4), none, new Uint8Array(4), none, new Uint8Array(4), // empty kdf options new Uint8Array(4), // num keys = 1 new Uint8Array(4), wirePub, new Uint8Array(4), privBlock ]; writeUInt32BE(headerParts[1], none.length, 0); writeUInt32BE(headerParts[3], none.length, 0); writeUInt32BE(headerParts[5], 0, 0); writeUInt32BE(headerParts[6], 1, 0); writeUInt32BE(headerParts[7], wirePub.length, 0); writeUInt32BE(headerParts[9], privBlock.length, 0); const bodyBytes = concatUint8Arrays(headerParts); const b64 = bytesToBase64(bodyBytes); const lines = b64.match(/.{1,70}/g) || [b64]; return `-----BEGIN OPENSSH PRIVATE KEY-----\n${lines.join('\n')}\n-----END OPENSSH PRIVATE KEY-----\n`; } /** * Decode OpenSSH private key PEM or raw seed string into 32-byte seed */ extractSeedFromInput(input) { if (!input || typeof input !== 'string') { throw new Error('Key input is required'); } const trimmed = input.trim(); // Check if OpenSSH PEM format if (trimmed.includes('-----BEGIN OPENSSH PRIVATE KEY-----')) { const cleanB64 = trimmed .replace(/-----[^\n]+-----/g, '') .replace(/\s+/g, ''); const buf = base64ToBytes(cleanB64); // Find "ssh-ed25519" strings in wire format const needle = new TextEncoder().encode('ssh-ed25519'); let firstIdx = -1; for (let i = 0; i <= buf.length - needle.length; i++) { let match = true; for (let j = 0; j < needle.length; j++) { if (buf[i + j] !== needle[j]) { match = false; break; } } if (match) { firstIdx = i; break; } } if (firstIdx === -1) throw new Error('Not an Ed25519 OpenSSH private key'); // Second occurrence is in private block let secondIdx = -1; for (let i = firstIdx + needle.length; i <= buf.length - needle.length; i++) { let match = true; for (let j = 0; j < needle.length; j++) { if (buf[i + j] !== needle[j]) { match = false; break; } } if (match) { secondIdx = i; break; } } if (secondIdx === -1) throw new Error('Could not parse OpenSSH private key block'); const offset = secondIdx + needle.length; const pubLen = readUInt32BE(buf, offset); const pubOffset = offset + 4; const privLenOffset = pubOffset + pubLen; const privLen = readUInt32BE(buf, privLenOffset); const privOffset = privLenOffset + 4; if (privLen < 32 || privOffset + 32 > buf.length) { throw new Error('Malformed OpenSSH private key length'); } return buf.slice(privOffset, privOffset + 32); } // Check if 64-char hex string (32 bytes) if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { return hexToBytes(trimmed); } // Check if 44-char base64 string (32 bytes) try { const bytes = base64ToBytes(trimmed); if (bytes.length === 32) return bytes; if (bytes.length === 64) return bytes.slice(0, 32); } catch (_) {} throw new Error('Unrecognized key format. Paste an OpenSSH private key (id_ed25519) or 32-byte hex/base64 seed.'); } /** * Import raw 32-byte seed into Web Crypto SubtleCrypto */ async importSeed(seedBytes) { if (seedBytes.length !== 32) throw new Error('Seed must be exactly 32 bytes'); const pkcs8 = concatUint8Arrays([PKCS8_HEADER, seedBytes]); const cryptoKey = await window.crypto.subtle.importKey( 'pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign'] ); // Derive raw public key // Since WebCrypto doesn't directly export public key from private key in all browsers, // we can also generate or sign/verify or use the derived public key // But in modern Chromium/Firefox/Safari, Web Crypto can export or derive. // If needed, we can extract from OpenSSH PEM if available, or generate a sign verify test let rawPub = null; try { // Many browsers support exporting JWK or SPKI const spkiBuffer = await window.crypto.subtle.exportKey?.('spki', cryptoKey).catch(() => null); if (spkiBuffer) { rawPub = new Uint8Array(spkiBuffer).slice(12); } } catch (_) {} return { cryptoKey, rawSeed: seedBytes, rawPub }; } /** * Generate fresh Ed25519 identity in the browser */ async generateIdentity() { const keyPair = await window.crypto.subtle.generateKey( { name: 'Ed25519' }, true, ['sign', 'verify'] ); const rawPubBuf = await window.crypto.subtle.exportKey('raw', keyPair.publicKey); const rawPub = new Uint8Array(rawPubBuf); const pkcs8Buf = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey); const pkcs8 = new Uint8Array(pkcs8Buf); const rawSeed = pkcs8.slice(16); const pubkey = this.encodeOpenSSHPublicKey(rawPub); const fingerprint = await this.computeFingerprint(rawPub); this.cryptoKey = keyPair.privateKey; this.rawPub = rawPub; this.rawSeed = rawSeed; this.pubkey = pubkey; this.fingerprint = fingerprint; this.shortFingerprint = fingerprint.slice(7, 15); localStorage.setItem(STORAGE_KEY_PRIV, bytesToHex(rawSeed)); localStorage.setItem(STORAGE_KEY_PUB, pubkey); localStorage.setItem(STORAGE_KEY_FP, fingerprint); return this.getIdentity(); } /** * Import custom key from user text input */ async importKey(input) { const seedBytes = this.extractSeedFromInput(input); const pkcs8 = concatUint8Arrays([PKCS8_HEADER, seedBytes]); const privateKey = await window.crypto.subtle.importKey( 'pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign'] ); // In Ed25519, the public key is deterministically derived from the seed // To get the public key in Web Crypto, we sign a test challenge or compute it // Let's create an identity check this.cryptoKey = privateKey; this.rawSeed = seedBytes; // Extract rawPub if input was OpenSSH PEM let rawPub = null; if (input.includes('-----BEGIN OPENSSH PRIVATE KEY-----')) { try { const cleanB64 = input.replace(/-----[^\n]+-----/g, '').replace(/\s+/g, ''); const buf = base64ToBytes(cleanB64); const needle = new TextEncoder().encode('ssh-ed25519'); let idx = -1; for (let i = 0; i <= buf.length - needle.length; i++) { let match = true; for (let j = 0; j < needle.length; j++) { if (buf[i + j] !== needle[j]) { match = false; break; } } if (match) { idx = i; break; } } if (idx !== -1) { const pubLen = readUInt32BE(buf, idx + needle.length); const pubBytes = buf.slice(idx + needle.length + 4, idx + needle.length + 4 + pubLen); if (pubBytes.length === 32) rawPub = pubBytes; } } catch (_) {} } if (!rawPub) { // Fallback: generate keypair or re-derive // If browser allows generateKey, generate identity and replace seed rawPub = new Uint8Array(32); // fallback } this.rawPub = rawPub; this.pubkey = this.encodeOpenSSHPublicKey(rawPub); this.fingerprint = await this.computeFingerprint(rawPub); this.shortFingerprint = this.fingerprint.slice(7, 15); localStorage.setItem(STORAGE_KEY_PRIV, bytesToHex(seedBytes)); localStorage.setItem(STORAGE_KEY_PUB, this.pubkey); localStorage.setItem(STORAGE_KEY_FP, this.fingerprint); await this.ensureSession(true, isExplicitLogin); return this.getIdentity(); } getIdentity() { return { pubkey: this.pubkey, fingerprint: this.fingerprint, shortFingerprint: this.shortFingerprint, rawPub: this.rawPub, rawSeed: this.rawSeed }; } /** * Sign an arbitrary UTF-8 string with the Ed25519 private key */ async sign(message) { if (!this.cryptoKey) throw new Error('No active SSH private key'); const msgBytes = new TextEncoder().encode(message); const sigBuffer = await window.crypto.subtle.sign( { name: 'Ed25519' }, this.cryptoKey, msgBytes ); return bytesToBase64(new Uint8Array(sigBuffer)); } /** * Authenticate to backend and establish/refresh anonymous session */ async ensureSession(force = false, isExplicitLogin = false) { if (window.location.pathname === '/banned') { return; } // If user is already logged in as a real registered user, don't overwrite session! if (window.f0ckSession && window.f0ckSession.user && !window.f0ckSession.is_anon) { return; } if (this.isSessionReady && !force) return; try { const timestamp = Date.now(); const message = `anon-auth:${timestamp}:${this.pubkey}`; const signature = await this.sign(message); const tombstone = this.getTombstone(); const hwFingerprint = await this.getHardwareFingerprint(); const res = await fetch('/api/v2/anon/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pubkey: this.pubkey, timestamp: timestamp, signature: signature, tombstone: tombstone, hw_fingerprint: hwFingerprint }) }); const data = await res.json(); if (data.banned) { this.setTombstone({ banned: true, fingerprint: data.fingerprint || this.fingerprint, hw_fingerprint: data.hw_fingerprint || hwFingerprint, reason: data.reason, expires: data.expires }); this.clearStoredIdentity(); this.updateNavUI(false); // ONLY redirect to /banned if user was explicitly trying to login! if (isExplicitLogin) { const anonModal = document.getElementById('anon-ssh-modal'); if (anonModal) anonModal.style.display = 'none'; if (window.location.pathname !== '/banned') { window.location.href = data.redirect || '/banned'; } } return; } if (data.success) { this.isSessionReady = true; if (data.csrf_token) { if (window.f0ckSession) window.f0ckSession.csrf_token = data.csrf_token; const metaCsrf = document.querySelector('meta[name="csrf-token"]'); if (metaCsrf) metaCsrf.content = data.csrf_token; } window.f0ckAnonIdentity = { userId: data.user_id, fingerprint: data.fingerprint, shortFingerprint: data.short_fingerprint, hwFingerprint: data.hw_fingerprint || hwFingerprint }; window.dispatchEvent(new CustomEvent('f0ck:anon_session_ready', { detail: window.f0ckAnonIdentity })); } } catch (err) { console.warn('[ANON_SSH] Session handshake failed:', err); } } /** * Get client ban tombstone if previously banned. */ getTombstone() { try { const raw = localStorage.getItem('f0ck_anon_tombstone'); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } /** * Save client ban tombstone. */ setTombstone(tombstone) { try { localStorage.setItem('f0ck_anon_tombstone', JSON.stringify(tombstone)); if (tombstone && tombstone.banned) { document.cookie = `f0ck_banned=${encodeURIComponent(JSON.stringify(tombstone))}; Path=/; Max-Age=31536000; SameSite=Lax`; } } catch (e) {} } /** * Trigger browser download of id_ed25519 private key file */ downloadPrivateKey() { if (!this.rawSeed || !this.rawPub) return; const pem = this.encodeOpenSSHPrivateKey(this.rawSeed, this.rawPub); const blob = new Blob([pem], { type: 'application/octet-stream' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'id_ed25519'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } /** * Trigger browser download of id_ed25519.pub public key file */ downloadPublicKey() { if (!this.pubkey) return; const content = `${this.pubkey}\n`; const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'id_ed25519.pub'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } /** * Copy public key string to clipboard */ async copyPublicKey() { if (!this.pubkey) return; try { await navigator.clipboard.writeText(this.pubkey); if (typeof window.showToastNotification === 'function') { window.showToastNotification('OpenSSH Public Key copied to clipboard!'); } else { alert('Public key copied to clipboard!'); } } catch (err) { prompt('Copy your OpenSSH Public Key:', this.pubkey); } } /** * Clear stored cryptographic identity from localStorage and memory */ clearStoredIdentity() { localStorage.removeItem(STORAGE_KEY_PRIV); localStorage.removeItem(STORAGE_KEY_PUB); localStorage.removeItem(STORAGE_KEY_FP); document.cookie = 'f0ck_banned=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax'; this.cryptoKey = null; this.rawSeed = null; this.rawPub = null; this.pubkey = null; this.fingerprint = null; this.shortFingerprint = null; this.isSessionReady = false; } /** * Explicitly log in as anonymous (generates identity if none, establishes session, syncs guest favs) */ async loginAsAnonymous() { if (window.f0ckSession && window.f0ckSession.enable_anonymous_access === false) { return; } // If client has a ban tombstone, redirect to /banned directly on login attempt const tombstone = this.getTombstone(); if (tombstone && tombstone.banned) { const anonModal = document.getElementById('anon-ssh-modal'); if (anonModal) anonModal.style.display = 'none'; if (window.location.pathname !== '/banned') { window.location.href = '/banned'; } return; } try { if (!this.pubkey) { await this.generateIdentity(true); } else { await this.ensureSession(true, true); } if (!this.isSessionReady) return; // Sync any guest favorites saved in localStorage to this anon account if (window.f0ckGuestFavs && typeof window.f0ckGuestFavs.importToAccount === 'function') { await window.f0ckGuestFavs.importToAccount(); } this.updateNavUI(true); if (typeof window.showToastNotification === 'function') { window.showToastNotification('Logged in as anonymous'); } // Reload page to reflect authenticated anonymous session across navbar, comments, settings window.location.reload(); } catch (err) { console.error('[ANON_SSH] Login as anonymous failed:', err); alert('Failed to log in as anonymous: ' + (err.message || err)); } } /** * Log out of anonymous session, returning to clean guest state */ async logoutAnonymous() { try { this.clearStoredIdentity(); await fetch('/api/v2/anon/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}); } finally { this.updateNavUI(false); if (typeof window.showToastNotification === 'function') { window.showToastNotification('Logged out from anonymous session'); } window.location.reload(); } } /** * Update navigation bar elements based on anonymous authentication state */ updateNavUI(isAnon) { const icon = document.getElementById('nav-visitor-icon'); const label = document.getElementById('nav-anon-label'); if (icon) { if (isAnon) { icon.classList.remove('fa-user'); icon.classList.add('fa-user-secret'); } else { icon.classList.remove('fa-user-secret'); icon.classList.add('fa-user'); } } if (label) { label.textContent = isAnon ? 'anonymous' : 'guest'; } if (window.f0ckSession && window.f0ckSession.enable_anonymous_access === false) { if (label) label.textContent = 'guest'; const loginAnonBtn = document.getElementById('nav-login-anon-btn'); const modalAnonBtn = document.getElementById('modal-login-as-anon-btn'); const anonIdentityBtn = document.getElementById('nav-anon-identity-btn'); const anonSettingsBtn = document.getElementById('nav-anon-settings-btn'); const anonLogoutBtn = document.getElementById('nav-anon-logout-btn'); const anonDivider = document.getElementById('nav-anon-divider'); if (loginAnonBtn) loginAnonBtn.style.display = 'none'; if (modalAnonBtn) { const wrapper = modalAnonBtn.closest('div'); if (wrapper) wrapper.style.display = 'none'; else modalAnonBtn.style.display = 'none'; } if (anonIdentityBtn) anonIdentityBtn.style.display = 'none'; if (anonSettingsBtn) anonSettingsBtn.style.display = 'none'; if (anonLogoutBtn) anonLogoutBtn.style.display = 'none'; if (anonDivider) anonDivider.style.display = 'none'; return; } const loginAnonBtn = document.getElementById('nav-login-anon-btn'); const anonIdentityBtn = document.getElementById('nav-anon-identity-btn'); const anonSettingsBtn = document.getElementById('nav-anon-settings-btn'); const anonLogoutBtn = document.getElementById('nav-anon-logout-btn'); const anonDivider = document.getElementById('nav-anon-divider'); if (loginAnonBtn) loginAnonBtn.style.display = isAnon ? 'none' : ''; if (anonIdentityBtn) anonIdentityBtn.style.display = isAnon ? '' : 'none'; if (anonSettingsBtn) anonSettingsBtn.style.display = isAnon ? '' : 'none'; if (anonLogoutBtn) anonLogoutBtn.style.display = isAnon ? '' : 'none'; if (anonDivider) anonDivider.style.display = isAnon ? '' : 'none'; const guestFavsNav = document.getElementById('nav-guest-favs'); const guestFavsLink = document.getElementById('nav-guest-favs-link'); if (guestFavsNav) guestFavsNav.style.display = isAnon ? '' : 'none'; if (guestFavsLink) guestFavsLink.style.display = isAnon ? '' : 'none'; } /** * Initialize on page startup */ async init() { if (window.location.pathname === '/banned') { return; } if (window.f0ckSession && window.f0ckSession.enable_anonymous_access === false) { if (window.f0ckSession.is_anon && window.f0ckSession.logged_in) { await fetch('/api/v2/anon/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}); window.location.reload(); return; } this.updateNavUI(false); return; } // If registered user, do not override if (window.f0ckSession && window.f0ckSession.user && !window.f0ckSession.is_anon) { return; } // If client has a ban tombstone, do NOT attempt background session handshake on page load/reload! const tombstone = this.getTombstone(); if (tombstone && tombstone.banned) { this.clearStoredIdentity(); this.updateNavUI(false); this.attachUIListeners(); return; } const storedPriv = localStorage.getItem(STORAGE_KEY_PRIV); const storedPub = localStorage.getItem(STORAGE_KEY_PUB); const storedFp = localStorage.getItem(STORAGE_KEY_FP); if (storedPriv && storedPub && storedFp) { try { const rawSeed = hexToBytes(storedPriv); const pkcs8 = concatUint8Arrays([PKCS8_HEADER, rawSeed]); this.cryptoKey = await window.crypto.subtle.importKey( 'pkcs8', pkcs8, { name: 'Ed25519' }, true, ['sign'] ); this.rawSeed = rawSeed; this.pubkey = storedPub; this.fingerprint = storedFp; this.shortFingerprint = storedFp.slice(7, 15); // Extract rawPub from storedPub wire format const parts = storedPub.trim().split(/\s+/); const wirePub = base64ToBytes(parts[1]); const typeLen = readUInt32BE(wirePub, 0); this.rawPub = wirePub.slice(4 + typeLen + 4, 4 + typeLen + 4 + 32); // User previously logged in as anonymous with this key // If backend session is already active, avoid redundant session churn if (window.f0ckSession && window.f0ckSession.is_anon && window.f0ckSession.logged_in) { this.isSessionReady = true; this.updateNavUI(true); } else { // Unauthenticated guest state: do NOT auto-login as anonymous on page load! this.updateNavUI(false); } } catch (err) { console.warn('[ANON_SSH] Failed to restore stored key:', err); this.clearStoredIdentity(); this.updateNavUI(false); } } else { // Clean guest state! Do NOT auto-generate or establish session! if (window.f0ckSession && window.f0ckSession.is_anon && window.f0ckSession.logged_in) { // Stale backend anon session cookie without corresponding local key: clear cookie await fetch('/api/v2/anon/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}); window.location.reload(); return; } this.updateNavUI(false); } // Hook identity modal triggers and action buttons this.attachUIListeners(); } attachUIListeners() { // Global click handler for Login as anonymous, Identity modal, & Logout document.addEventListener('click', (e) => { const identityTarget = e.target.closest('#nav-anon-identity-btn'); if (identityTarget) { e.preventDefault(); this.openModal(); return; } const loginTarget = e.target.closest('#nav-login-anon-btn, #modal-login-as-anon-btn, .trigger-login-anon'); if (loginTarget) { e.preventDefault(); this.loginAsAnonymous(); return; } const logoutTarget = e.target.closest('#nav-anon-logout-btn, a[href="/logout"]'); if (logoutTarget && ((window.f0ckSession && window.f0ckSession.is_anon && window.f0ckSession.logged_in) || this.pubkey)) { e.preventDefault(); this.logoutAnonymous(); return; } }); const anonBtn = document.getElementById('nav-anon-identity-btn'); if (anonBtn) { anonBtn.addEventListener('click', (e) => { e.preventDefault(); this.openModal(); }); } const modalClose = document.getElementById('anon-ssh-modal-close'); if (modalClose) { modalClose.addEventListener('click', () => this.closeModal()); } const modalOverlay = document.getElementById('anon-ssh-modal'); if (modalOverlay) { modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) this.closeModal(); }); } } openModal() { const modal = document.getElementById('anon-ssh-modal'); if (!modal) return; const fp = this.fingerprint || localStorage.getItem(STORAGE_KEY_FP); const pk = this.pubkey || localStorage.getItem(STORAGE_KEY_PUB); const fpEl = document.getElementById('anon-ssh-fp-display'); const pubEl = document.getElementById('anon-ssh-pub-display'); if (fpEl) fpEl.textContent = fp || 'Generating...'; if (pubEl) pubEl.textContent = pk || ''; modal.style.display = 'flex'; } closeModal() { const modal = document.getElementById('anon-ssh-modal'); if (modal) modal.style.display = 'none'; } } window.f0ckAnonSSH = new AnonSSH(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => window.f0ckAnonSSH.init()); } else { window.f0ckAnonSSH.init(); } })();