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:
197
README.md
197
README.md
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user