Compare commits

..

No commits in common. "1164ef117141774de7a53461430be34862ba8be9" and "2c0ca60b6599d15cfd810c37f97efc3e60c4536f" have entirely different histories.

4 changed files with 43 additions and 90 deletions

View File

@ -110,18 +110,14 @@ truenews.sesur.dev {
file_server
}
t.sesur.dev {
root * /opt/homelab/services/mtproto_page
root * /opt/homelab/services/mtproto_page/xk9m2p4q7
handle /xk9m2p4q7/data.json {
respond 404
}
handle /xk9m2p4q7/encrypt.py {
respond 404
}
handle {
file_server
@data path /data.json
basicauth @data {
pvlx $2b$05$wXo0zmemeoOJ3ukx4pORSuq/9IoH/Lo5PIvGk3uzNvcAMmtpjI1o2
}
file_server
header {
X-Robots-Tag "noindex, nofollow"
X-Content-Type-Options "nosniff"

View File

@ -1 +0,0 @@
{"salt": "kHW+St6vw5/6MkfH3tjtuw==", "iv": "LPQqGr84T4UVthgd", "ct": "scddFF7pZD1lxOPUia7RnJIp+ytSdmC1Gi9QvZ8oEQBAzhGAnlO8uoOsG06q915r8rykb0ni7A3XZaL1716PzgLYlh5g8dyrF0Xh/I8EyRlOyN8IiBBczpYeq3gH4R+ofh/tpJ/xzjeGx41hXvQnjpgzy/3PMoGLYzLbVKstD2kcyLZR9qYMBMN7dzkXVAeQpSeBPk6mWIFwMLgxvCRu50Lsic5UNYMdx+rnlwCosW8CMpTajv6mAsY8e+Ks58BuFPfpsPgiXrwVRnvYebv0C6NDjbIiqN8b2/HNBYjEfO1FrrIO1qspGUe3Vulc88eB1F6AX16P0Vp0j3YbuuX6CczEvo8Kq1zjlL9kqwBvisv8Atv6OzHwII2GND1tdd80aglQWtjL7xPCav+j2imAnhmhEEENgsTB1wbphSGSG1x1wWTZmtSgfTEprgDDSRhmnbiI7PcOiKHzOOm3ci3clsJZtF89ssMnBaR1sNNTnrTHsHSn4U/wXqimuLjrI5cmpLZT+5cSMMWYVYGSu59gi27vysn5GKf6j/6AQCOJbDLs6pipF8500dSEiZEqzOMf0kf44FR86eGuVCXJqSyozkHdnvieCC7eW2R+MoB9MGZL8XjbUtVB2X+7kxKSmWvZ/lLr1cSEsZsSJxOP+Sbfl6rzGjUeHp+hhuQWx9NZVPcZn8492jdp"}

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python3
"""
Generates data.enc from data.json using AES-256-GCM + PBKDF2.
Re-run whenever you change data.json or rotate the password.
Usage on the server:
nix-shell -p python3Packages.cryptography --run "python3 encrypt.py"
"""
import json, os, base64, getpass
from pathlib import Path
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
here = Path(__file__).parent
plaintext = here.joinpath('data.json').read_bytes()
password = getpass.getpass('Password: ').encode()
salt = os.urandom(16)
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000)
key = kdf.derive(password)
iv = os.urandom(12)
ciphertext = AESGCM(key).encrypt(iv, plaintext, None)
here.joinpath('data.enc').write_text(json.dumps({
'salt': base64.b64encode(salt).decode(),
'iv': base64.b64encode(iv).decode(),
'ct': base64.b64encode(ciphertext).decode(),
}))
print('data.enc written.')

View File

@ -108,6 +108,7 @@
50% { opacity: 0.3; }
}
/* ── auth wall ── */
#auth-wall {
width: 100%; max-width: 360px;
display: flex; flex-direction: column; gap: 1.5rem;
@ -119,7 +120,9 @@
margin-bottom: 0.5rem;
}
.auth-label span { color: var(--accent); }
.input-wrap { position: relative; }
.input-wrap {
position: relative;
}
.input-wrap input {
width: 100%;
background: var(--surface);
@ -169,12 +172,14 @@
.btn-primary:not(:disabled):hover { color: var(--bg); }
.btn-primary:not(:disabled):hover::before { transform: translateX(0); }
/* ── proxy list ── */
#proxy-list {
width: 100%; max-width: 680px;
display: none;
flex-direction: column; gap: 1rem;
animation: fadeUp 0.4s ease both;
}
.section-title {
font-size: 0.65rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--dim);
@ -265,10 +270,13 @@
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.proxy-card:nth-child(2) { animation-delay: 0.05s; }
.proxy-card:nth-child(3) { animation-delay: 0.10s; }
.proxy-card:nth-child(4) { animation-delay: 0.15s; }
.proxy-card:nth-child(5) { animation-delay: 0.20s; }
.proxy-card:nth-child(6) { animation-delay: 0.25s; }
.proxy-card:nth-child(7) { animation-delay: 0.30s; }
</style>
</head>
<body>
@ -291,7 +299,7 @@
<p class="auth-label">Введи <span>пароль</span></p>
<div class="input-wrap">
<input type="password" id="pass-input" placeholder="Введите пароль" autocomplete="off" />
<button onclick="toggleVis()">👁</button>
<button onclick="toggleVis()" id="eye-btn">👁</button>
</div>
<p class="err-msg" id="err-msg">Неправильный пароль. Ты знаешь кому писать.</p>
</div>
@ -311,19 +319,6 @@
<script>
document.getElementById('yr').textContent = new Date().getFullYear();
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;
@ -332,40 +327,26 @@
btn.disabled = true;
btn.textContent = '...';
let res;
try {
const res = await fetch('./data.enc');
const { salt, iv, ct } = await res.json();
res = await fetch('/data.json', {
headers: { 'Authorization': 'Basic ' + btoa('pvlx:' + password) }
});
} catch {
showError();
btn.disabled = false;
btn.textContent = '→ Нажать';
return;
}
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));
if (res.ok) {
const proxies = await res.json();
document.getElementById('auth-wall').style.display = 'none';
renderProxies(proxies);
const list = document.getElementById('proxy-list');
list.style.display = 'flex';
list.style.flexDirection = 'column';
} catch {
} else {
showError();
btn.disabled = false;
btn.textContent = '→ Нажать';
@ -378,12 +359,21 @@
input.classList.add('error');
msg.classList.add('show');
input.animate(
[{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }],
{ duration: 200, iterations: 3 }
[{transform:'translateX(-6px)'},{transform:'translateX(6px)'},{transform:'translateX(0)'}],
{duration: 200, iterations: 3}
);
setTimeout(() => { input.classList.remove('error'); msg.classList.remove('show'); }, 3000);
}
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 tgUrl(p) {
return `tg://proxy?server=${encodeURIComponent(p.server)}&port=${encodeURIComponent(p.port)}&secret=${encodeURIComponent(p.secret)}`;
}
@ -396,8 +386,8 @@
const card = document.createElement('div');
card.className = 'proxy-card';
const hdr = document.createElement('div');
hdr.className = 'card-header';
const header = document.createElement('div');
header.className = 'card-header';
const idx = document.createElement('span');
idx.className = 'node-index';
@ -411,7 +401,7 @@
flag.className = 'node-flag';
flag.textContent = p.flag;
hdr.append(idx, name, flag);
header.append(idx, name, flag);
const meta = document.createElement('div');
meta.className = 'card-meta';
@ -448,7 +438,7 @@
});
actions.append(tgBtn, copyBtn);
card.append(hdr, meta, actions);
card.append(header, meta, actions);
list.appendChild(card);
});
}