v2: richtungsgetrennte Schluessel, Offset im Schluessel, PWA

- Neues Schluesselmodell: pro Beziehung zwei Stroeme (send.key/recv.key),
  Sende-Offset lebt im Schluessel-Header und wird beim Senden fortgeschrieben.
  Loest two-time-pad bei bidirektionaler Nutzung deterministisch.
- Stream-Identifier im Bild -> falscher Schluessel wird erkannt statt
  Kauderwelsch zu liefern. Rollen-Guards (send/recv) gegen Fehlbenutzung.
- CLI genkey erzeugt gerichtetes Keypaar (meine_seite/partner_seite).
- Web-App auf v2 umgestellt; Senden liefert aktualisierten send.key zum
  Download (Browser-Sandbox kann nicht auf den Stick schreiben).
- PWA: manifest + service worker (offline) + icons.
- Interop CLI<->Web in beide Richtungen per Node-Test abgesichert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 21:46:06 +00:00
parent ad0b309acf
commit 30127db247
9 changed files with 549 additions and 469 deletions

197
README.md
View File

@@ -2,142 +2,165 @@
Vertrauliche Nachrichten via **One-Time-Pad** + **LSB-Steganografie**.
Verschlüsselt Text mit einem echten One-Time-Pad (XOR gegen `os.urandom`-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. Gedacht als unauffälliger Kanal, dessen Inhalt selbst bei Mitlesen des
Transports nicht rekonstruierbar ist.
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 —
nicht nur „rechnerisch schwer", sondern beweisbar unknackbar. Das gilt aber nur,
solange **alle drei** Regeln eingehalten werden:
beweisbar unknackbar, solange **alle drei** Regeln gelten:
1. Der Schlüssel (das *Pad*) ist echt zufällig.
2. Das Pad ist mindestens so lang wie die Nachricht.
3. Jeder Pad-Abschnitt wird **nur ein einziges Mal** benutzt.
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
automatisch über einen Offset-Zähler (siehe unten).
über **richtungsgetrennte Schlüssel** + einen Offset, der im Schlüssel selbst
lebt.
## Installation
## 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
```bash
pip install Pillow
```
Sonst nur Python-Stdlib.
## Benutzung
### 1. Pad erzeugen (einmalig)
### 1. Schlüsselpaar erzeugen (einmalig, pro Kontakt)
```bash
python3 schattenpost.py genkey -o pad.key -s 1000000 # 1 MB Pad
python3 schattenpost.py genkey -o kontakt_bob -s 1000000 # 1 MB je Richtung
```
Das erzeugte `pad.key` **sicher und persönlich** an den Gesprächspartner
übergeben (USB-Stick, kein digitaler Kanal!). Beide Seiten brauchen die
byte-identische Datei. Daneben entsteht `pad.key.offset` — der Verbrauchszähler.
Erzeugt zwei Ordner:
### 2. Nachricht verstecken
* `kontakt_bob_meine_seite/`**du** behältst ihn (enthält `send.key` + `recv.key`).
* `kontakt_bob_partner_seite/`**dem Partner** geben (Stick, persönlich).
### 2. Senden
```bash
python3 schattenpost.py hide \
-t "Treffpunkt morgen 18 Uhr am alten Hafen." \
-k pad.key \
-t "Treffpunkt morgen 18 Uhr." \
-k kontakt_bob_meine_seite/send.key \
-c urlaubsfoto.jpg \
-o harmlos.png
```
Alternativ `-i datei.txt` statt `-t`, oder Text über stdin.
Der Sende-Offset im `send.key` wird dabei automatisch fortgeschrieben.
Alternativ `-i datei` statt `-t`, oder Text über stdin.
### 3. Nachricht rausholen (Gegenseite)
### 3. Empfangen
```bash
python3 schattenpost.py reveal -s harmlos.png -k pad.key
python3 schattenpost.py reveal -s harmlos.png -k kontakt_bob_meine_seite/recv.key
```
Mit `-o klartext.txt` in eine Datei statt auf stdout.
## ⚠️ Der eine wichtige Haken
## Weboberfläche & PWA (`web/`)
**Verschicke das Stego-PNG immer als Datei/Dokument, niemals als „Foto".**
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.
Messenger (WhatsApp, Telegram-„Foto", …) jagen Fotos durch verlustbehaftete
JPEG-Kompression. Das verändert die Pixelwerte und **zerstört die versteckten
Bits sofort** — `reveal` schlägt dann fehl. Als Dokument/Datei bleibt das PNG
bit-genau erhalten.
* **Ü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 ausgelieferten `index.html` lädt und beide Richtungen gegen das CLI prüft.
## Wie der Pad-Wiederverwendungsschutz funktioniert
### Rollenverteilung (wichtig wegen der Browser-Sandbox)
- `pad.key.offset` merkt sich den nächsten unbenutzten Byte-Offset im Pad.
- `hide` verschlüsselt ab diesem Offset, schreibt den Offset **in den
Stego-Header** und zählt den Zähler um die Nachrichtenlänge weiter.
- `reveal` liest den Offset aus dem Bild und weiß automatisch, welcher
Pad-Abschnitt gilt.
Ein Browser darf **nicht** in eine Datei auf dem Stick zurückschreiben. Deshalb:
So wird garantiert kein Pad-Byte doppelt verwendet — solange der Zähler nicht
manuell zurückgedreht wird. Der Sender führt den Zähler; der Empfänger braucht
ihn nicht (der Offset steckt im Bild).
## Container-Format
Vor der LSB-Einbettung wird ein kleiner Header vorangestellt:
| Feld | Größe | Inhalt |
|---------|-------|-------------------------------|
| magic | 4 B | `SPST` |
| version | 1 B | `1` |
| offset | 8 B | Pad-Offset (uint64, BE) |
| length | 4 B | Ciphertext-Länge (uint32, BE) |
| ciphertext | n B | XOR(plaintext, pad[offset…]) |
Die Bits werden linear (MSB zuerst) in die LSBs der R/G/B-Kanäle geschrieben.
## Weboberfläche (`web/index.html`)
Eine **einzige, self-contained HTML-Datei** — kein Framework, kein CDN, kein
Build. Verschlüsselung und Steganografie laufen zu 100 % im Browser
(client-side): Pad, Klartext und Nachricht verlassen das Gerät nie. Ein Server,
der die Datei ausliefert, ist ein reiner „dummer" Fileserver und sieht nichts.
* **Überall erreichbar, iOS inklusive.** Nutzt nur Safari-Standard-APIs
(Canvas, FileReader, `crypto.getRandomValues`).
* **Format-kompatibel zum CLI.** Am Terminal verstecken, am Handy im Browser
aufdecken — und umgekehrt. Byte-Interop ist per Test abgesichert
(`web/_interop_test.mjs` lädt exakt den Krypto-Kern aus der ausgelieferten
`index.html` und prüft beide Richtungen gegen das Python-CLI).
* **Offset-Tracking** merkt sich pro Pad (via `localStorage`) den nächsten
freien Offset. Achtung: Der Zähler ist pro Browser/Gerät. Wer abwechselnd über
mehrere Instanzen sendet, muss den Offset manuell synchron halten — sonst
droht Pad-Wiederverwendung.
* 📥 **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.
### Deployment
Es ist eine statische Datei — einfach irgendwo hinlegen:
Statische Dateien — einfach hosten:
```bash
# lokal testen
python3 -m http.server -d web 8000 # -> http://localhost:8000
# produktiv: web/index.html hinter einen beliebigen Webserver / Static-Host
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.** Ein Angreifer, der
das Format kennt, könnte Bits kippen. Für Manipulationsschutz bräuchte es
zusätzlich einen MAC (z.B. HMAC mit einem separaten Pad-Bereich).
- Die Sicherheit steht und fällt mit der **geheimen, sicheren Übergabe des
Pads**. Das ist der eigentliche Aufwand bei OTP — es gibt kein Schlüssel-
Austauschprotokoll, das dir das abnimmt.
- Pads gehören **niemals** in ein Repository. Die `.gitignore` blockt `*.key`
und `*.offset` bewusst.
* **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.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.