Web-App vergibt beim Senden einen Screenshot-artigen Namen (Screenshot_YYYYMMDD_HHMMSS.png) statt des verraeterischen harmlos.png. Screenshot-Stil passt zum PNG-Format (echte Kamerafotos sind JPG). Umstellbar ueber CARRIER_PREFIX. README-Beispiele nachgezogen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.2 KiB
schattenpost
Vertrauliche Nachrichten via One-Time-Pad + LSB-Steganografie.
Verschlüsselt Text mit einem echten One-Time-Pad (XOR gegen os.urandom- bzw.
crypto.getRandomValues-Zufall) und versteckt den Ciphertext in den
niederwertigsten Bits eines Trägerfotos. Das Ergebnis sieht aus wie ein ganz
normales Bild, trägt die Nachricht aber bit-genau in sich.
Es gibt zwei Frontends mit identischem Dateiformat:
schattenpost.py— Kommandozeilen-Tool (Python + Pillow).web/— eine client-side Web-App / PWA (läuft im Browser, auch iOS).
Warum das (theoretisch) unknackbar ist
One-Time-Pad ist der einzige Cipher mit informationstheoretischer Sicherheit — beweisbar unknackbar, solange alle drei Regeln gelten:
- Der Schlüssel ist echt zufällig.
- Der Schlüssel ist mindestens so lang wie die Nachricht.
- Jeder Schlüsselabschnitt wird nur ein einziges Mal benutzt.
Regel 3 ist der klassische Todesstoß (two-time-pad). schattenpost erzwingt sie
über richtungsgetrennte Schlüssel + einen Offset, der im Schlüssel selbst
lebt.
Das Schlüsselmodell
Jede Beziehung nutzt zwei unabhängige Schlüsselströme — einen pro Senderichtung. Jede Seite hält zwei Dateien:
| Datei | Zweck | Verhalten |
|---|---|---|
send.key |
Senden | Trägt den Sende-Offset im Header; er wandert bei jeder Nachricht vor. |
recv.key |
Empfangen | Wird nur gelesen (Offset kommt aus dem Bild), nie verändert. |
Dein send.key besteht aus demselben Zufallsmaterial wie der recv.key deines
Partners — und umgekehrt. Weil du und dein Partner zum Senden nie denselben
Strom benutzt, könnt ihr gleichzeitig schreiben, ohne je denselben
Schlüsselbereich zu treffen.
Ein zufälliger Stream-Identifier in jedem Schlüssel landet auch im Bild.
Dadurch erkennt das Aufdecken sofort, ob der richtige recv.key benutzt wird —
statt stillen Kauderwelsch zu liefern.
OPSEC-Empfehlung
Lege die Schlüssel auf einen USB-Stick und häng ihn nur ans gerade benutzte
Gerät. Dann existiert jeder send.key (mit seinem Offset) physisch nur einmal
— zwei Zähler können nicht auseinanderdriften, und auf Handy/Laptop liegt nie ein
Schlüssel herum. Mehrere Kontakte = mehrere Schlüsselordner auf dem Stick.
CLI
pip install Pillow
1. Schlüsselpaar erzeugen (einmalig, pro Kontakt)
python3 schattenpost.py genkey -o kontakt_bob -s 1000000 # 1 MB je Richtung
Erzeugt zwei Ordner:
kontakt_bob_meine_seite/→ du behältst ihn (enthältsend.key+recv.key).kontakt_bob_partner_seite/→ dem Partner geben (Stick, persönlich).
2. Senden
python3 schattenpost.py hide \
-t "Treffpunkt morgen 18 Uhr." \
-k kontakt_bob_meine_seite/send.key \
-c urlaubsfoto.jpg \
-o Screenshot_20260714_220801.png
Der Sende-Offset im send.key wird dabei automatisch fortgeschrieben.
Alternativ -i datei statt -t, oder Text über stdin.
3. Empfangen
python3 schattenpost.py reveal -s Screenshot_20260714_220801.png -k kontakt_bob_meine_seite/recv.key
Mit -o klartext.txt in eine Datei statt auf stdout.
Weboberfläche & PWA (web/)
Eine client-side App: Verschlüsselung und Steganografie laufen zu 100 % im Browser. Schlüssel, Klartext und Nachricht verlassen das Gerät nie. Ein Server, der die Dateien ausliefert, sieht nichts.
- Überall erreichbar, iOS inklusive (nur Safari-Standard-APIs).
- PWA: über „Zum Home-Bildschirm" installierbar; der Service Worker cacht die App-Shell → danach offline nutzbar (kein Server mehr nötig).
- Format-kompatibel zum CLI — am Terminal senden, am Handy empfangen und
umgekehrt. Abgesichert durch
web/_interop_test.mjs, der exakt den Krypto-Kern aus der ausgeliefertenindex.htmllädt und beide Richtungen gegen das CLI prüft.
Rollenverteilung (wichtig wegen der Browser-Sandbox)
Ein Browser darf nicht in eine Datei auf dem Stick zurückschreiben. Deshalb:
- 📥 Empfangen: überall per Web/PWA — der
recv.keywird nur gelesen. ✓ - 📤 Senden: am Laptop am besten per CLI (schreibt den Offset direkt auf
den Stick). In der Web-App bekommst du nach dem Verstecken den aktualisierten
send.keyzum Download und musst ihn auf dem Stick ersetzen — sonst würde der nächste Sendevorgang denselben Schlüsselbereich erneut benutzen.
Deployment
Statische Dateien — einfach hosten:
python3 -m http.server -d web 8000 # lokal: http://localhost:8000
Produktiv hinter einen beliebigen Static-Host / Webserver (HTTPS nötig, damit der Service Worker / die PWA-Installation greift).
⚠️ iOS-Eigenheit
Stego-Bild über „In Dateien sichern" ablegen und als Datei teilen — nicht in „Fotos" speichern. iOS re-komprimiert Bilder aus der Fotos-Mediathek und würde die Nachricht zerstören.
Formate
Schlüsseldatei (SPK1, 24-Byte-Header + Zufallsmaterial):
| Feld | Größe | Inhalt |
|---|---|---|
| magic | 4 B | SPK1 |
| version | 1 B | 1 |
| role | 1 B | 1 = send, 0 = recv |
| stream_id | 8 B | Strom-Kennung (identisch im Paar) |
| send_offset | 8 B | nächster freier Offset (uint64 BE) |
| (padding) | 2 B | |
| material | n B | echtes Zufalls-Pad |
Stego-Container im Bild (SPS2, 25 B Header, dann Ciphertext, LSB MSB-first
in die R/G/B-Kanäle):
| Feld | Größe | Inhalt |
|---|---|---|
| magic | 4 B | SPS2 |
| version | 1 B | 2 |
| stream_id | 8 B | welcher Strom (Schlüssel-Check) |
| offset | 8 B | Pad-Offset (uint64 BE) |
| length | 4 B | Ciphertext-Länge (uint32 BE) |
Grenzen / Hinweise
- Steganografie versteckt, authentifiziert aber nicht. Wer das Format kennt, könnte Bits kippen. Für Manipulationsschutz bräuchte es zusätzlich einen MAC.
- Der eine Rest-Fall: derselbe
send.keygleichzeitig auf zwei Geräten (ohne Stick-Prinzip) kann trotzdem kollidieren — das Bild-Offset hilft nur dem Empfänger. Regel: einsend.key, ein aktives Sendegerät. - Verschickte Stego-Bilder immer als Datei/Dokument, nie als „Foto".
- Schlüssel gehören niemals in ein Repository — die
.gitignoreblockt*.keybewusst.