mirror of
https://github.com/pvlnes/homelab.git
synced 2026-06-03 20:13:49 +00:00
Compare commits
2 Commits
2c0ca60b65
...
1164ef1171
| Author | SHA1 | Date | |
|---|---|---|---|
| 1164ef1171 | |||
| eedb023cf1 |
@ -110,14 +110,18 @@ 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
|
handle /xk9m2p4q7/data.json {
|
||||||
basicauth @data {
|
respond 404
|
||||||
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
services/mtproto_page/xk9m2p4q7/data.enc
Normal file
1
services/mtproto_page/xk9m2p4q7/data.enc
Normal 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"}
|
||||||
32
services/mtproto_page/xk9m2p4q7/encrypt.py
Normal file
32
services/mtproto_page/xk9m2p4q7/encrypt.py
Normal 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.')
|
||||||
@ -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 = '→ Нажать';
|
||||||
@ -359,21 +378,12 @@
|
|||||||
input.classList.add('error');
|
input.classList.add('error');
|
||||||
msg.classList.add('show');
|
msg.classList.add('show');
|
||||||
input.animate(
|
input.animate(
|
||||||
[{transform:'translateX(-6px)'},{transform:'translateX(6px)'},{transform:'translateX(0)'}],
|
[{ transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(0)' }],
|
||||||
{duration: 200, iterations: 3}
|
{ duration: 200, iterations: 3 }
|
||||||
);
|
);
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user