/** * 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; } 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); 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) { // 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 res = await fetch('/api/v2/anon/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pubkey: this.pubkey, timestamp: timestamp, signature: signature }) }); const data = await res.json(); 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 }; window.dispatchEvent(new CustomEvent('f0ck:anon_session_ready', { detail: window.f0ckAnonIdentity })); } } catch (err) { console.warn('[ANON_SSH] Session handshake failed:', err); } } /** * 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); 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; } try { if (!this.pubkey) { await this.generateIdentity(); } await this.ensureSession(true); // 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 { window.location.href = '/'; } } /** * Update visitor navbar UI elements */ 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.f0ckSession && window.f0ckSession.enable_anonymous_access === false) { if (window.f0ckSession.is_anon) { 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; } 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) { this.isSessionReady = true; this.updateNavUI(true); } else { await this.ensureSession(); this.updateNavUI(true); } } 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) { // 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) || 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(); } })();