sesur upd

This commit is contained in:
pvlnes 2026-05-02 18:07:33 +03:00
parent 2c0ca60b65
commit eedb023cf1
4 changed files with 91 additions and 43 deletions

View File

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

View File

@ -0,0 +1 @@
{"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

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