Kommentarer og docstrings nævnte containernumre, værtsplatformen og edge-hostens adresse og sti. Eksempel-env'en bruger nu en pladsholder. Ingen adfærd er ændret. Claude-Session: https://claude.ai/code/session_01GGe7kUhooMFuzY6GaDb2w9
111 lines
4.2 KiB
Python
111 lines
4.2 KiB
Python
"""Hemmeligheds-drop: giv et token eller kodeord til serveren uden om chatten.
|
|
|
|
Værdien krypteres med det samme i requesten med værtens offentlige nøgle
|
|
og kun den krypterede udgave gemmes. Portalen har ikke den private nøgle, så
|
|
den kan hverken vise eller læse en gemt hemmelighed bagefter, heller ikke hvis
|
|
portalens container kompromitteres. På værten henter `secret-drop get <navn>` filen,
|
|
dekrypterer den og sletter den her (hver hemmelighed kan hentes én gang).
|
|
|
|
Format (version 1), samme idé som en libsodium/age "sealed box":
|
|
b"WSD1" | ephemeral X25519 public key (32) | nonce (12) | ChaCha20-Poly1305
|
|
nøglen = HKDF-SHA256(X25519(eph, modtager), salt = eph_pub || modtager_pub,
|
|
info = b"weircon secret-drop v1"); navnet er associated data, så en fil
|
|
ikke kan omdøbes til en anden hemmelighed uden at dekrypteringen fejler.
|
|
"""
|
|
import base64
|
|
import json
|
|
import os
|
|
import re
|
|
import time
|
|
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
|
|
import config
|
|
|
|
DIR = config.DATA / "secret-drop"
|
|
PUBKEY = config.DATA / "secret-drop.pub"
|
|
NAVN = re.compile(r"^[a-z0-9][a-z0-9._-]{0,63}$")
|
|
MAKS = 64 * 1024
|
|
INFO = b"weircon secret-drop v1"
|
|
|
|
|
|
def _recipient() -> X25519PublicKey:
|
|
raw = base64.b64decode(PUBKEY.read_text().strip())
|
|
return X25519PublicKey.from_public_bytes(raw)
|
|
|
|
|
|
def seal(name: str, value: bytes) -> bytes:
|
|
recipient = _recipient()
|
|
eph = X25519PrivateKey.generate()
|
|
eph_pub = eph.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
rcp_pub = recipient.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
key = HKDF(algorithm=hashes.SHA256(), length=32, salt=eph_pub + rcp_pub, info=INFO).derive(eph.exchange(recipient))
|
|
nonce = os.urandom(12)
|
|
return b"WSD1" + eph_pub + nonce + ChaCha20Poly1305(key).encrypt(nonce, value, name.encode())
|
|
|
|
|
|
def store(name: str, value: str, note: str, by: str) -> str | None:
|
|
"""Gem en hemmelighed krypteret. Returnerer en fejltekst, eller None."""
|
|
name = (name or "").strip().lower()
|
|
if not NAVN.match(name):
|
|
return "Navnet må kun indeholde små bogstaver, tal, punktum, bindestreg og understreg"
|
|
data = (value or "").strip().encode()
|
|
if not data:
|
|
return "Værdien er tom"
|
|
if len(data) > MAKS:
|
|
return "Værdien er for stor (maks 64 KB)"
|
|
DIR.mkdir(mode=0o700, exist_ok=True)
|
|
blob = seal(name, data)
|
|
del data
|
|
# Skrives atomisk og kun læsbar for portalen; en gammel med samme navn erstattes.
|
|
for suffix, content, mode in ((".wsd", blob, "wb"),
|
|
(".json", json.dumps({"name": name, "note": (note or "").strip()[:200],
|
|
"by": by, "created": int(time.time()),
|
|
"bytes": len(blob)}, ensure_ascii=False), "w")):
|
|
tmp = DIR / f".{name}{suffix}.tmp"
|
|
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
with os.fdopen(fd, mode) as f:
|
|
f.write(content)
|
|
os.replace(tmp, DIR / f"{name}{suffix}")
|
|
return None
|
|
|
|
|
|
def pending() -> list[dict]:
|
|
"""Hemmeligheder der venter på at blive hentet (kun metadata)."""
|
|
if not DIR.exists():
|
|
return []
|
|
out = []
|
|
for meta in sorted(DIR.glob("*.json")):
|
|
try:
|
|
m = json.loads(meta.read_text())
|
|
except ValueError:
|
|
continue
|
|
m["alder"] = _alder(int(time.time()) - int(m.get("created", 0)))
|
|
out.append(m)
|
|
return sorted(out, key=lambda m: m.get("created", 0), reverse=True)
|
|
|
|
|
|
def delete(name: str) -> bool:
|
|
if not NAVN.match(name or ""):
|
|
return False
|
|
found = False
|
|
for suffix in (".wsd", ".json"):
|
|
p = DIR / f"{name}{suffix}"
|
|
if p.exists():
|
|
p.unlink()
|
|
found = True
|
|
return found
|
|
|
|
|
|
def _alder(sek: int) -> str:
|
|
if sek < 60:
|
|
return "lige nu"
|
|
if sek < 3600:
|
|
return f"{sek // 60} min siden"
|
|
if sek < 86400:
|
|
return f"{sek // 3600} timer siden"
|
|
return f"{sek // 86400} dage siden"
|