Statt Ordnern <base>_meine_seite/ mit send.key/recv.key erzeugt genkey jetzt
vier flache Dateien <base>_{meine,partner}_seite__{send,recv}.key. So trägt der
Name Seite+Rolle -> im Browser hoch/runtergeladen überschreibt der aktualisierte
Schlüssel sauber die richtige Datei (kein generisches send.key mehr).
Interop-Runner + mjs + README an das neue Schema angepasst.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
260 lines
10 KiB
Markdown
260 lines
10 KiB
Markdown
<p align="center">
|
|
<img src="web/icon-512.png" alt="schattenpost" width="128">
|
|
</p>
|
|
|
|
<h1 align="center">schattenpost</h1>
|
|
|
|
<p align="center">
|
|
<strong>Vertrauliche Nachrichten via One-Time-Pad + LSB-Steganografie.</strong><br>
|
|
<em>Beweisbar sicher verschlüsselt, authentifiziert, versteckt in einem harmlosen Foto — komplett lokal im Browser.</em>
|
|
</p>
|
|
|
|
---
|
|
|
|
schattenpost verschlüsselt Text mit einem echten One-Time-Pad (XOR gegen
|
|
`os.urandom`- bzw. `crypto.getRandomValues`-Zufall), authentifiziert ihn mit
|
|
einem Poly1305-Einmal-MAC und versteckt das Ganze 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.
|
|
|
|
Zwei Frontends, **ein identisches Dateiformat**:
|
|
|
|
* **`schattenpost.py`** — Kommandozeilen-Tool (Python + Pillow).
|
|
* **`web/`** — client-side Web-App / PWA, läuft im Browser (auch iOS), offline-fähig.
|
|
|
|
---
|
|
|
|
## ⚠️ Ehrliche Einordnung (bitte lesen)
|
|
|
|
Dies ist ein **Hobby- und Lernprojekt**, kein auditiertes Sicherheitsprodukt. Die
|
|
Krypto-Bausteine (OTP, Poly1305 nach RFC 8439) sind solide und getestet, aber:
|
|
|
|
* **Kein unabhängiges Sicherheits-Audit.** Nutzung auf eigenes Risiko.
|
|
* **Steganografie verschleiert den Inhalt, nicht die Existenz einer Kommunikation.**
|
|
Wer Metadaten/Traffic beobachtet, sieht *dass* du Bilder verschickst.
|
|
* **Die harte Nuss bleibt der Schlüsselaustausch.** Ein One-Time-Pad ist nur so
|
|
sicher wie die Art, wie ihr das Schlüsselmaterial *persönlich* übergebt (siehe
|
|
[OPSEC](#opsec-empfehlung)). Wird das Pad abgefangen oder kopiert, ist alles
|
|
hin.
|
|
* **Endgeräte-Sicherheit ist Voraussetzung.** Gegen ein kompromittiertes Gerät
|
|
(Keylogger, Screen-Grab) hilft keine Verschlüsselung.
|
|
|
|
Kurz: mathematisch sauber, aber die Sicherheit steht und fällt mit *deiner*
|
|
Handhabung der Schlüssel und Geräte.
|
|
|
|
---
|
|
|
|
## Warum das (theoretisch) unknackbar ist
|
|
|
|
One-Time-Pad ist der einzige Cipher mit *informationstheoretischer* Sicherheit —
|
|
beweisbar unknackbar, solange **alle drei** Regeln gelten:
|
|
|
|
1. Der Schlüssel ist echt zufällig.
|
|
2. Der Schlüssel ist mindestens so lang wie die Nachricht.
|
|
3. 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.
|
|
|
|
### Authentifizierung (ohne die Sicherheit aufzugeben)
|
|
|
|
OTP verschlüsselt, sagt aber nichts über *Echtheit* — wer das Format kennt,
|
|
könnte einzelne Bits kippen. schattenpost hängt deshalb an jede Nachricht einen
|
|
**Poly1305-Einmal-MAC** (RFC 8439). Der 32-Byte-MAC-Schlüssel wird pro Nachricht
|
|
**frisch aus demselben One-Time-Pad** gezogen — direkt hinter dem Keystream — und
|
|
nie wiederverwendet. Genau unter dieser Bedingung ist Poly1305 ein
|
|
*informationstheoretisch* sicherer Authenticator: die „beweisbar unknackbar"-
|
|
Zusage bleibt vollständig erhalten, kein rechnerisch-sicheres HMAC/SHA nötig.
|
|
|
|
Der MAC deckt den Header (`stream_id`, `offset`, `length`) **und** den Ciphertext
|
|
ab (encrypt-then-MAC). Jede Veränderung am Bild — oder ein nicht synchroner
|
|
Schlüssel — lässt das Aufdecken hart abbrechen, statt stillen Müll zu liefern.
|
|
|
|
## 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üsselsätze auf dem Stick.
|
|
|
|
## Voraussetzungen
|
|
|
|
* **CLI:** Python ≥ 3.8 und [Pillow](https://python-pillow.org/) (`pip install Pillow`).
|
|
* **Web:** ein moderner Browser. Kein Build-Schritt, keine Abhängigkeiten — die
|
|
App ist eine einzelne `index.html` mit eingebettetem Krypto-Kern.
|
|
|
|
## CLI
|
|
|
|
```bash
|
|
pip install Pillow
|
|
```
|
|
|
|
### 1. Schlüsselpaar erzeugen (einmalig, pro Kontakt)
|
|
|
|
```bash
|
|
python3 schattenpost.py genkey -o kontakt_bob -s 1000000 # 1 MB je Richtung
|
|
```
|
|
|
|
Erzeugt vier Dateien mit beschreibenden Namen (Seite + Rolle stehen im Dateinamen
|
|
— einheitlich zum Web-Frontend):
|
|
|
|
* `kontakt_bob_meine_seite__send.key` + `kontakt_bob_meine_seite__recv.key`
|
|
→ **du** behältst sie.
|
|
* `kontakt_bob_partner_seite__send.key` + `kontakt_bob_partner_seite__recv.key`
|
|
→ **dem Partner** geben (Stick, persönlich).
|
|
|
|
### 2. Senden
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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 `bash web/_interop_run.sh`: der Runner lädt exakt
|
|
den Krypto-Kern aus der ausgelieferten `index.html`, fährt beide Senderichtungen
|
|
CLI↔Web durch, prüft den Poly1305-MAC cross-implementation gegen den
|
|
RFC-8439-Vektor und verifiziert die Manipulations-Erkennung.
|
|
|
|
### 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.key` wird 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.key` zum Download** und musst ihn auf dem Stick ersetzen — sonst würde der
|
|
nächste Sendevorgang denselben Schlüsselbereich erneut benutzen.
|
|
|
|
### Lokal starten
|
|
|
|
```bash
|
|
python3 -m http.server -d web 8000 # http://localhost:8000
|
|
```
|
|
|
|
Produktiv hinter einen beliebigen Static-Host / Webserver. **HTTPS ist nötig**,
|
|
damit Service Worker und PWA-Installation greifen.
|
|
|
|
### ⚠️ 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** (`SPS3`, 41 B Header, dann Ciphertext, LSB MSB-first
|
|
in die R/G/B-Kanäle):
|
|
|
|
| Feld | Größe | Inhalt |
|
|
|------|-------|--------|
|
|
| magic | 4 B | `SPS3` |
|
|
| version | 1 B | `3` |
|
|
| stream_id | 8 B | welcher Strom (Schlüssel-Check) |
|
|
| offset | 8 B | Pad-Offset (uint64 BE) |
|
|
| length | 4 B | Ciphertext-Länge (uint32 BE) |
|
|
| tag | 16 B | Poly1305-MAC über die 25 B davor + Ciphertext |
|
|
|
|
Pro Nachricht verbraucht der Pad-Strom **`length + 32` Bytes**: `length` für den
|
|
Keystream, 32 für den Poly1305-Einmalschlüssel (`r|s`). Der Sende-Offset wandert
|
|
entsprechend weiter. Alte `SPS2`-Bilder (ohne Tag) werden beim Aufdecken noch
|
|
gelesen, aber als **unauthentifiziert** markiert.
|
|
|
|
## Projektstruktur
|
|
|
|
```
|
|
schattenpost.py CLI (genkey / hide / reveal)
|
|
web/
|
|
index.html Web-App + Krypto-Kern (zwischen ==CORE START/END== markiert)
|
|
sw.js Service Worker (Offline-Cache der App-Shell)
|
|
manifest.webmanifest PWA-Manifest
|
|
icon-192.png, App-Icons
|
|
icon-512.png
|
|
_interop_run.sh Reproduzierbarer CLI↔Web-Interop-Test
|
|
_interop_test.mjs Web-Core-Seite des Tests (lädt den Kern aus index.html)
|
|
_interop_rawrgb.py Test-Helfer (PNG ↔ rohes RGB)
|
|
```
|
|
|
|
Dateien mit `_`-Präfix sind Entwickler-/Test-Werkzeuge, nicht Teil der App.
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
pip install Pillow
|
|
bash web/_interop_run.sh
|
|
```
|
|
|
|
Prüft Poly1305 gegen den RFC-8439-Vektor (Python *und* JS), beide Senderichtungen
|
|
CLI↔Web und die Manipulations-Erkennung.
|
|
|
|
## Grenzen / Hinweise
|
|
|
|
* **Manipulationsschutz ist drin** (SPS3 / Poly1305-Einmal-MAC): Bit-Kippen am
|
|
Bild wird erkannt, das Aufdecken bricht ab. Steganografie *versteckt* aber
|
|
weiterhin nur — sie verschleiert nicht, *dass* überhaupt kommuniziert wird.
|
|
* **Der eine Rest-Fall:** derselbe `send.key` gleichzeitig auf zwei Geräten (ohne
|
|
Stick-Prinzip) kann trotzdem kollidieren — das Bild-Offset hilft nur dem
|
|
Empfänger. Regel: **ein `send.key`, ein aktives Sendegerät.**
|
|
* Verschickte Stego-Bilder **immer als Datei/Dokument**, nie als „Foto".
|
|
* Schlüssel gehören **niemals** in ein Repository — die `.gitignore` blockt
|
|
`*.key` bewusst.
|
|
|
|
## Lizenz
|
|
|
|
[MIT](LICENSE) — mach damit, was du willst, aber ohne Gewähr.
|