document.getElementById('yr').textContent = new Date().getFullYear(); document.getElementById('submit-btn').addEventListener('click', tryAuth); document.getElementById('eye-btn').addEventListener('click', toggleVis); document.getElementById('pass-input').addEventListener('keydown', e => { if (e.key === 'Enter') tryAuth(); }); function toggleVis() { const inp = document.getElementById('pass-input'); inp.type = inp.type === 'password' ? 'text' : 'password'; } function b64ToBytes(b64) { return Uint8Array.from(atob(b64), c => c.charCodeAt(0)); } async function tryAuth() { const password = document.getElementById('pass-input').value; if (!password) return; const btn = document.getElementById('submit-btn'); btn.disabled = true; btn.textContent = '...'; try { const res = await fetch('./data.enc'); const { salt, iv, ct } = await res.json(); const keyMaterial = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey'] ); const key = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: b64ToBytes(salt), iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt'] ); // Wrong password → AES-GCM auth tag mismatch → throws → catch below const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: b64ToBytes(iv) }, key, b64ToBytes(ct) ); const proxies = JSON.parse(new TextDecoder().decode(decrypted)); document.getElementById('auth-wall').style.display = 'none'; renderProxies(proxies); const list = document.getElementById('proxy-list'); list.style.display = 'flex'; list.style.flexDirection = 'column'; } catch { showError(); btn.disabled = false; btn.textContent = '→ Нажать'; } } function showError() { const input = document.getElementById('pass-input'); const msg = document.getElementById('err-msg'); input.classList.add('error'); msg.classList.add('show'); input.animate( [{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }], { duration: 200, iterations: 3 } ); setTimeout(() => { input.classList.remove('error'); msg.classList.remove('show'); }, 3000); } function tgUrl(p) { return `tg://proxy?server=${encodeURIComponent(p.server)}&port=${encodeURIComponent(p.port)}&secret=${encodeURIComponent(p.secret)}`; } function renderProxies(proxies) { const list = document.getElementById('proxy-list'); proxies.forEach((p, i) => { const url = tgUrl(p); const card = document.createElement('div'); card.className = 'proxy-card'; const hdr = document.createElement('div'); hdr.className = 'card-header'; const idx = document.createElement('span'); idx.className = 'node-index'; idx.textContent = `NODE ${String(i + 1).padStart(2, '0')}`; const name = document.createElement('span'); name.className = 'node-name'; name.textContent = p.name; const flag = document.createElement('span'); flag.className = 'node-flag'; flag.textContent = p.flag; hdr.append(idx, name, flag); const meta = document.createElement('div'); meta.className = 'card-meta'; [['server', p.server], ['port', p.port], ['secret', p.secret]].forEach(([k, v]) => { const key = document.createElement('span'); key.className = 'key'; key.textContent = k; const val = document.createElement('span'); val.className = 'val'; val.textContent = v; meta.append(key, val); }); const actions = document.createElement('div'); actions.className = 'card-actions'; const tgBtn = document.createElement('a'); tgBtn.className = 'btn-tg'; tgBtn.href = url; tgBtn.textContent = '✈ Open in Telegram'; const copyBtn = document.createElement('button'); copyBtn.className = 'btn-copy'; copyBtn.textContent = '⎘ Copy link'; copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(url).then(() => { copyBtn.textContent = '✓ Copied'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = '⎘ Copy link'; copyBtn.classList.remove('copied'); }, 2000); }); }); actions.append(tgBtn, copyBtn); card.append(hdr, meta, actions); list.appendChild(card); }); }