Files
weircon-front/secretdrop.py
agw 50612a643a Fjern interne referencer fra kildekoden
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
2026-10-01 17:30:00 +02:00

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"