Sicherheitsmodell
Was diese App schützt, wie sie es tut, und — genauso wichtig — wogegen sie nicht hilft. Wer eine Sicherheitszusage nicht überprüfen kann, hat keine.
Wie ein Konto entsteht
Es gibt keine Registrierung. Beim Anlegen erzeugt dein Browser zwei Schlüsselpaare auf der Kurve P-256: eines zum Signieren (ECDSA), eines für Schlüsselaustausch (ECDH). Deine Identität ist der SHA-256-Hash beider öffentlicher Schlüssel. Der Server erfährt nur diesen Hash und die öffentlichen Teile — nie einen privaten Schlüssel, nie ein Passwort.
Beim Verbinden schickt der Server eine Zufallszahl, die du signierst. Damit ist bewiesen, dass du den privaten Schlüssel hast. Ein Angreifer, der die Verbindung mitschneidet, kann daraus nichts wiederverwenden.
Die drei Stufen
cipher kennt drei Stufen mit drei verschiedenen Versprechen. Stufe 1 und 2 sind Bereiche, zwischen denen du wechselst; Stufe 3 ist eine Eigenschaft eines einzelnen Raums und in beiden Bereichen verfügbar.
| Stufe | Zugang | Was bleibt |
|---|---|---|
| 1 — Offen | direkt, optional mit App-Passwort | Identität und Verlauf, verschlüsselt auf diesem Gerät |
| 2 — Verborgen | PEM-Schlüsseldatei bei jedem Start | eigener Verlauf in einem Tresor, der von außen wie Zufall aussieht |
| 3 — Flüchtig | keiner — ein Raum innerhalb Stufe 1 oder 2 | nichts: kein Tresor, keine Warteschlange, kein Anrufeintrag |
Der verborgene Bereich hat ein eigenes Konto mit eigenem Kontaktcode und eigener Verbindung. Für das Relay sind das zwei verschiedene Menschen.
Wo dein Schlüssel liegt
Der Schlüssel der Stufe 2 existiert in genau zwei Zuständen: im Arbeitsspeicher dieses Tabs, und in der PEM-Datei, die du selbst verwahrst — verschlüsselt mit PBKDF2-HMAC-SHA-256, 600 000 Iterationen, dann AES-256-GCM. Auf dem Gerät landet er nie.
Der Schlüssel der Stufe 1 muss bleiben, sonst wäre es keine normale App.
Er liegt in Tresor A, verschlüsselt mit einem zufälligen Datenschlüssel.
Dieser wiederum ist verpackt — entweder mit deinem App-Passwort
(PBKDF2, 600 000 Runden), oder, wenn du keines setzt, mit einem
Gerät-Schlüssel, der als extractable: false in der Datenbank
liegt: benutzbar, aber nicht auslesbar.
Die App benutzt kein localStorage, sessionStorage, keine Cookies und keinen Service Worker. In IndexedDB liegen genau vier Objekte:
slots | 16 Byte Salt und 8 gleich große Schlüsselfächer |
a | Tresor A — Stufe 1 |
b | Tresor B — Stufe 2 |
dk | der Gerät-Schlüssel, nur ohne App-Passwort |
slots und b gibt es ab dem allerersten Start,
gefüllt mit Zufall. Dass sie da sind, beantwortet also nicht die Frage, ob
dieses Gerät einen verborgenen Bereich hat.
Die Schlüsselfächer
Acht Fächer zu je 256 Byte, nach dem Vorbild von LUKS. Ein Fach kann den Datenschlüssel für Tresor B enthalten — dann öffnet es die PEM-Datei. Ein anderes kann eine Notfall-Markierung enthalten — dann öffnet es ein Passwort, ganz ohne Datei. Alle übrigen sind Zufall.
Der Klartext jedes Fachs ist ein Datensatz fester Länge, verschlüsselt mit AES-256-GCM. Alle acht sind damit exakt gleich lang, egal was drinsteht. Ein forensisches Abbild sieht 8 × 256 Byte Zufall und beantwortet keine Frage: nicht, ob ein verborgener Bereich existiert, nicht, ob ein Notfall-Passwort gesetzt ist, nicht, wie viele Schlüssel es gibt.
Beim Entsperren werden immer alle acht Fächer geprüft, und der Versuch dauert immer mindestens 1,2 Sekunden — Treffer, Notfall-Löschung und Fehlversuch sind an der Zeit nicht zu unterscheiden.
Bei der Meldung gilt das für den Weg, auf dem das Notfall-Passwort eingegeben wird: Passwort ohne Datei. Dort gibt es genau eine Antwort — „Schlüssel nicht erkannt“ —, gleich ob ein Notfall-Fach getroffen wurde oder gar nichts.
Wird dagegen eine Datei übergeben, sagt die App, was mit dieser Datei nicht stimmte: kein Schlüsseldateiformat, falsches Passwort, oder gültig aber ohne passendes Fach. Sonst wäre ein tippfehlerhaftes Passwort von einer verwechselten Datei nicht zu unterscheiden, und Nutzer stünden ohne jeden Anhaltspunkt da. Keine dieser drei Antworten verrät, ob es auf diesem Gerät überhaupt einen verborgenen Bereich gibt — sie beschreiben ausschließlich die eingereichte Datei. Und das Notfall-Passwort erreicht diesen Zweig nicht, weil es ohne Datei eingegeben wird.
Das Notfall-Passwort
Wird es im Feld des verborgenen Bereichs eingegeben, verschwindet dieser Bereich: Tresor B wird mit frischem Zufall überschrieben, und alle acht Fächer ebenso — damit ist auch die echte PEM-Datei ab da wertlos. Danach zeigt die App exakt dieselbe Meldung wie ein Fehlversuch ohne Datei: „Schlüssel nicht erkannt“.
Tresor A bleibt dabei unangetastet. Ein offener Bereich, der plötzlich leer ist, wäre genau das Signal, das die ganze Konstruktion vermeiden soll.
Was der Server sieht
| Sieht er | Sieht er nicht |
|---|---|
|
Öffentliche Schlüssel und die daraus abgeleitete Nutzer-ID Zufällige Raum-IDs und wer sie abonniert Zeitpunkt und Größe der Chiffrate IP-Adressen der Verbindungen |
Nachrichteninhalte Raumnamen und Mitgliederlisten Raumschlüssel oder irgendeinen privaten Schlüssel SDP, ICE-Kandidaten, DTLS-Fingerabdrücke Audio- oder Videodaten |
Der Server ist ein Relay ohne Datenbank. Alles liegt im RAM, ist gedeckelt, verfällt nach 24 Stunden und wird gelöscht, sobald alle Mitglieder es abgeholt haben. Ein Prozess-Neustart löscht alles.
Nachrichten
Jeder Raum hat einen AES-256-GCM-Schlüssel. Der Klartext wird zuerst mit deinem Identitätsschlüssel signiert, dann verschlüsselt. Raum-ID und Schlüssel-ID hängen als additional authenticated data mit drin — ein Chiffrat lässt sich also nicht in einen anderen Raum umhängen.
Weil vor dem Verschlüsseln signiert wird, reicht der Raumschlüssel allein nicht, um sich als jemand anderes auszugeben: auch ein Gruppenmitglied kann keine Nachricht in fremdem Namen fälschen.
Räume und Schlüsselwechsel
Ein Raum mit zwei Mitgliedern ist ein Direktchat, mit mehr eine Gruppe — technisch dasselbe. Der Raumschlüssel wird einzeln für jedes Mitglied verpackt: ECDH mit einem Wegwerf-Schlüssel des Absenders gegen deinen öffentlichen ECDH-Schlüssel, HKDF-SHA-256, AES-256-GCM. Der Server transportiert das Paket, ohne es öffnen zu können.
Bei jeder Änderung der Mitgliederliste wird ein neuer Raumschlüssel erzeugt. Wer neu dazukommt, kann nichts Älteres lesen — auch nicht das, was noch in der Warteschlange des Servers liegt. Wer entfernt wird, kann nichts Neues mehr lesen, selbst wenn die Verbindung bestehen bleibt.
Anrufe
Sprach- und Videoanrufe laufen als P2P-Mesh: jedes Paar hat eine eigene Verbindung, WebRTC verschlüsselt sie mit DTLS-SRTP. Medien berühren den Server nie.
Der übliche Angriff auf WebRTC ist der Signalisierungsserver, der die DTLS-Fingerabdrücke im SDP austauscht und sich dazwischenschaltet. Hier geht das nicht: SDP und ICE-Kandidaten reisen durch denselben verschlüsselten Raumkanal wie Nachrichten. Das Relay sieht nur Chiffrat und kann nichts ersetzen. STUN und TURN laufen auf diesem Server, damit kein Dritter die IP-Adressen der Anrufenden erfährt; ein TURN-Relay leitet ohnehin nur bereits verschlüsselte Pakete weiter.
Mesh trägt etwa sechs Teilnehmende, dann limitiert der Upload.
Beschlagnahmtes Gerät
Was gefunden wird, hängt von der Stufe ab. Stufe 3 liegt nie auf dem Gerät — dort ist nichts zu finden, weil nie etwas geschrieben wurde. Stufe 2 liegt dort als Zufall: ohne die PEM-Datei ist nicht einmal erkennbar, dass es sie gibt. Stufe 1 ist ein erkennbarer, verschlüsselter Tresor — mit App-Passwort braucht es das Passwort, ohne genügt ein entsperrtes Gerät.
Das ist der bewusste Handel: Stufe 1 ist bequem und dafür nur so gut wie dein App-Passwort. Was das nicht aushalten soll, gehört in Stufe 2 oder 3.
Wogegen das nicht hilft
- Metadaten. Das Relay sieht, welche Nutzer-IDs welche Raum-IDs abonnieren, wann und wie viel. Der Inhalt bleibt verborgen, der Kommunikationsgraph nicht. Dagegen hülfe nur ein Mixnet.
- Kompromittiertes Endgerät. Keylogger, bösartige Browser-Erweiterung oder ein Screenshot lesen mit, bevor verschlüsselt wird. Ende-zu-Ende endet an den Enden.
- Ein manipulierter Server. Der Code kommt vom Server. Wer ihn kontrolliert, könnte eine schlechtere Version ausliefern — das strukturelle Problem jeder Web-Krypto-App. Die CSP verbietet externe Skripte, aber gegen den Ursprung selbst schützt sie nicht.
- Vertauschte Schlüssel. Kontaktcodes kommen über einen Kanal, den diese App nicht kontrolliert. Vergleicht die Sicherheitsnummer über einen zweiten Weg, bevor es wichtig wird.
- Kein Forward Secrecy pro Nachricht. Es gibt keine Double-Ratchet-Kette. Der Raumschlüssel wechselt bei Mitgliederänderungen, nicht bei jeder Nachricht.
- Speicher ist nicht sicher löschbar. JavaScript kann Strings nicht überschreiben, und das Betriebssystem darf Speicher auf die Platte auslagern. Auch das Überschreiben in IndexedDB ist eine Bitte, keine Garantie: Wear-Leveling, Snapshots und Backups können Reste behalten. Der eigentliche Schutz ist, dass alles verschlüsselt ist und der Schlüssel der Stufe 2 nie auf dem Gerät war.
- Die Stufen lassen sich zeitlich verknüpfen. Offener und verborgener Bereich haben getrennte Identitäten und getrennte Verbindungen — aber dieselbe IP und überlappende Onlinezeiten. Gegen jemanden, der mitprotokolliert, ist die Trennung nicht wasserdicht.
- Stufe 3 ist eine Absprache, keine Durchsetzung. Dieses System bewahrt nichts auf. Ob die Gegenstelle einen unveränderten Client benutzt, keinen Screenshot macht und kein zweites Gerät danebenlegt, kann keine Software erzwingen.
- Wer beitritt, ist eine Vertrauensfrage. Jedes Mitglied kann weitere einladen. In sensiblen Räumen sollte klar sein, wer das darf.
Verwendete Verfahren
| Identität / Signatur | ECDSA P-256, SHA-256 |
| Schlüsselaustausch | ECDH P-256 → HKDF-SHA-256 |
| Nachrichten & Signalisierung | AES-256-GCM, 96-Bit-IV, AAD-gebunden |
| Schlüsseldatei / PEM | PBKDF2-HMAC-SHA-256, 600 000 Runden → AES-256-GCM |
| Tresore auf dem Gerät | AES-256-GCM, auf feste Größenklassen gepolstert |
| App-Passwort | PBKDF2-HMAC-SHA-256, 600 000 Runden → AES-256-GCM |
| Schlüsselfach aus der PEM-Datei | statisches ECDH P-256 → HKDF-SHA-256 → AES-256-GCM |
| Medien | WebRTC DTLS-SRTP, P2P-Mesh |
| Bibliotheken im Browser | keine — ausschließlich Web Crypto |
Der gesamte Client-Code liegt unter /js/ und ist unminifiziert
lesbar. Prüfe ihn, statt dieser Seite zu glauben.