mirror of
https://github.com/pvlnes/homelab.git
synced 2026-06-03 21:13:49 +00:00
Compare commits
No commits in common. "1164ef117141774de7a53461430be34862ba8be9" and "2c0ca60b6599d15cfd810c37f97efc3e60c4536f" have entirely different histories.
1164ef1171
...
2c0ca60b65
@ -110,18 +110,14 @@ truenews.sesur.dev {
|
|||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
t.sesur.dev {
|
t.sesur.dev {
|
||||||
root * /opt/homelab/services/mtproto_page
|
root * /opt/homelab/services/mtproto_page/xk9m2p4q7
|
||||||
|
|
||||||
handle /xk9m2p4q7/data.json {
|
@data path /data.json
|
||||||
respond 404
|
basicauth @data {
|
||||||
|
pvlx $2b$05$wXo0zmemeoOJ3ukx4pORSuq/9IoH/Lo5PIvGk3uzNvcAMmtpjI1o2
|
||||||
}
|
}
|
||||||
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"
|
||||||
|
|||||||
@ -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"}
|
|
||||||
@ -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.')
|
|
||||||
@ -108,6 +108,7 @@
|
|||||||
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;
|
||||||
@ -119,7 +120,9 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
.auth-label span { color: var(--accent); }
|
.auth-label span { color: var(--accent); }
|
||||||
.input-wrap { position: relative; }
|
.input-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.input-wrap input {
|
.input-wrap input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@ -169,12 +172,14 @@
|
|||||||
.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);
|
||||||
@ -265,10 +270,13 @@
|
|||||||
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>
|
||||||
@ -291,7 +299,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()">👁</button>
|
<button onclick="toggleVis()" id="eye-btn">👁</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="err-msg" id="err-msg">Неправильный пароль. Ты знаешь кому писать.</p>
|
<p class="err-msg" id="err-msg">Неправильный пароль. Ты знаешь кому писать.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -311,19 +319,6 @@
|
|||||||
<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;
|
||||||
@ -332,40 +327,26 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '...';
|
btn.textContent = '...';
|
||||||
|
|
||||||
|
let res;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('./data.enc');
|
res = await fetch('/data.json', {
|
||||||
const { salt, iv, ct } = await res.json();
|
headers: { 'Authorization': 'Basic ' + btoa('pvlx:' + password) }
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
showError();
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '→ Нажать';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
if (res.ok) {
|
||||||
'raw',
|
const proxies = await res.json();
|
||||||
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 = '→ Нажать';
|
||||||
@ -384,6 +365,15 @@
|
|||||||
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)}`;
|
||||||
}
|
}
|
||||||
@ -396,8 +386,8 @@
|
|||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'proxy-card';
|
card.className = 'proxy-card';
|
||||||
|
|
||||||
const hdr = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
hdr.className = 'card-header';
|
header.className = 'card-header';
|
||||||
|
|
||||||
const idx = document.createElement('span');
|
const idx = document.createElement('span');
|
||||||
idx.className = 'node-index';
|
idx.className = 'node-index';
|
||||||
@ -411,7 +401,7 @@
|
|||||||
flag.className = 'node-flag';
|
flag.className = 'node-flag';
|
||||||
flag.textContent = p.flag;
|
flag.textContent = p.flag;
|
||||||
|
|
||||||
hdr.append(idx, name, flag);
|
header.append(idx, name, flag);
|
||||||
|
|
||||||
const meta = document.createElement('div');
|
const meta = document.createElement('div');
|
||||||
meta.className = 'card-meta';
|
meta.className = 'card-meta';
|
||||||
@ -448,7 +438,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
actions.append(tgBtn, copyBtn);
|
actions.append(tgBtn, copyBtn);
|
||||||
card.append(hdr, meta, actions);
|
card.append(header, meta, actions);
|
||||||
list.appendChild(card);
|
list.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user