Entrixy — Ende-zu-Ende-Verschlüsselung

Eine Zero-Knowledge-Architektur: Der Server besitzt Entschlüsselungsschlüssel für nichts als technische Kennungen. Jedes bedeutungstragende Feld — Rufnummern, Adressen, Koordinaten, Webhook-Geheimnisse, WLAN-BSSIDs, Begrüßungen — wird auf dem Gerät des Besitzers verschlüsselt und liegt auf dem Server als undurchsichtiger Blob. Der Entschlüsselungsschlüssel erreicht den Gast im Einladungslink, im Adressfragment, das der Browser nie an den Server sendet.

Beschlossen am 19.04.2026. Die Arbeit läuft in Etappen — siehe Abschnitt „Phasen“.

1. Ziele und Grenzen

2. Kryptografisches Primitiv

Algorithm: AES-256-GCM. Auf der JVM ab Werk verfügbar (über javax.crypto), und vom Android Keystore unterstützt. Es liefert Vertraulichkeit und Authentifizierung zugleich.

Format des Chiffretexts

v1:<base64url(nonce || ciphertext || tag)>

Entropiequelle

SecureRandom() (die des Systems). Auf Android ist das /dev/urandom + Linux-PRNG.

3. Schlüsselhierarchie

K_master — der Hauptschlüssel des Besitzers

K_obj — der Objektschlüssel

K_guest — der Schlüssel des Gast-Bundles

Changing K_guest ist nur durch Ausstellung eines neuen Links möglich. Genau darauf beruht der Widerruf: Der Server löscht den Schlüsseleintrag, und der Gast erhält keinen aktuellen bundle_cipher mehr.

4. Was wo gespeichert wird

Auf dem Gerät des Besitzers (EncryptedSharedPreferences)

KeyWert
master_key32 Bytes im Klartext (SharedPreferences verschlüsselt sie ohnehin).
obj_key_{id}Der K_obj des Objekts, umschlossen mit K_master.
obj_plain_{id}(optional) ein Cache des entschlüsselten JSON, damit die Oberfläche schnell zeichnet.

Auf dem Server

Tabelle / FeldInhalt
numbers.data_cipherMit K_obj verschlüsseltes JSON: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}.
numbers.id / typeIm Klartext — nötig für Routing und die Anzeige des Symboltyps.
keys.bundle_cipherMit K_guest verschlüsseltes JSON: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}.
keys.user_key / mode / force_when_busyIm Klartext — für Routing und eine schnelle Zugriffsprüfung.
Die Begrüßung für den Gast wird separat im Bundle verschlüsselt, der Server sieht also auch sie nicht.
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>

6. Wichtigste Abläufe

Ein Objekt anlegen

  1. Der Besitzer drückt „Hinzufügen“ — dabei entsteht K_obj.
  2. Die Felder werden in JSON gepackt und verschlüsselt mit K_objdata_cipher.
  3. POST /api/number_add mit data_cipher plus dem Typ. Der Server gibt eine id zurück.
  4. Lokal gespeichert wird: obj_key_{id} = encrypt_k_master(K_obj).

Ein Objekt aktualisieren

Das neue JSON lokal bauen, mit dem vorhandenen K_objPOST /api/number_update. K_obj verschlüsseln, der sich nicht ändert.

Einen Gastschlüssel erstellen

  1. Der Besitzer hat gewählt: [obj1, obj2, obj3]. Dabei entsteht K_guest.
  2. Das Bundle-JSON wird zusammengesetzt: {obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.
  3. Verschlüsselt mit K_guestbundle_cipher.
  4. POST /api/key_create mit bundle_cipher, obj_ids (im Klartext — für die Zugriffsmatrix).
  5. Der Server gibt zurück: user_key.
  6. Dem Besitzer wird ein QR-Code mit dem Link angezeigt entrixy.com/key#{user_key}{base64url(K_guest)}.

Der Gast nimmt den Schlüssel an

  1. Der Gast öffnet den QR-Code oder den Link. Die Seite /key reads location.hash und übergibt den Schlüssel der App oder der Webversion.
  2. Der Client fordert an: POST /api/key_bundle.php mit user_key → erhält bundle_cipher und die Liste der obj_ids.
  3. Entschlüsselt das Bundle mit K_guest→ und erhält {obj_keys, welcome}.
  4. K_guest und die entschlüsselten obj_keys werden gespeichert im EncryptedSharedPreferences des Gast-Clients.
  5. Fortan bei jedem Aufruf: GET des verschlüsselten data_cipher des Objekts → Entschlüsselung mit dem lokalen obj_key_{id}.

Einen Gastschlüssel widerrufen

Der Besitzer drückt „Löschen“ → POST /api/key_revoke.php → der Server löscht den Schlüsseleintrag. Beim nächsten Bundle-Abruf wird der Gast abgewiesen und verliert den Zugang.

Einen Objektschlüssel rotieren

Der Besitzer drückt in den Objekteinstellungen „Schlüssel neu erzeugen“. Dabei entsteht K_obj_new, und jeder bundle_cipher, der dieses Objekt enthält, wird neu verschlüsselt. Der Client kennt sie nicht auswendig, deshalb baut der Client des Besitzers die Bundles neu: Er holt den bundle_cipher jedes eigenen Schlüssels, entschlüsselt ihn, setzt K_obj_newein und verschlüsselt neu. Die neuen Blobs gehen in einem Schwung an den Server.

7. Sicherung und Wiederherstellung

Dieser Abschnitt beschreibt einen Plan, der noch nicht umgesetzt ist: Ein Export des Hauptschlüssels existiert in der App heute nicht.

Losing K_master bedeutet den Verlust des Zugangs zu allen Objekten und Schlüsseln, deshalb ist ein Export unerlässlich.

Ohne Passphrase ist der Sicherungs-QR-Code schlicht K_master, deshalb ist die Passphrase Pflicht.

8. Einführungsphasen

Phase 1 — der Kryptospeicher auf dem Client

Add Crypto.kt: ein AES-256-GCM-Wrapper, Erzeugung und Speicherung von K_master, Hilfsfunktionen für K_obj, Serialisierung des Formats v1:<base64url>. Unit-Tests: verschlüsseln → entschlüsseln → vergleichen. An den Server geht noch nichts — die Verkabelung ist schlicht bereit.

Phase 2 — Migration des Serverschemas (erledigt)

Alle sensiblen Felder sind umgezogen nach numbers.data_cipher und user_keys.bundle_cipher. Die alten Klartextspalten (phone, label, radius, time_*, geo_*, wifi_*, share_*, has_avatar, security_level, user_keys.label, hosts.label, pending_actions.phone, hosts.last_ip) wurden aus der Datenbank entfernt (Phase 7). webhook_url und webhook_secret bleiben NUR dann im Klartext, wenn webhook_mode='server' — ohne sie kann der Server die HTTP-Anfrage nicht senden. Bei webhook_mode='phone' werden sie verschlüsselt in data_cipher.

Phase 3 — der Client liest und schreibt den Blob

Beim Anlegen oder Ändern verschlüsselt der Client das JSON und sendet data_cipher. Beim Synchronisieren liest er den Blob und entschlüsselt ihn. Neue Objekte leben ganz im Blob; alte ohne K_obj nutzen aus Gründen der Rückwärtskompatibilität weiter die Klartextfelder.

Phase 4 — das Gast-Bundle

Das Erstellen eines Schlüssels erzeugt K_guest, setzt das Bundle zusammen und sendet bundle_cipheran den Server. Der Link trägt den Schlüssel im Fragment; die Seite /key liest den Hash und reicht den Schlüssel an die App weiter.

Phase 5 — Sicherung und Wiederherstellung

Ein Einstellungsbildschirm mit „Hauptschlüssel exportieren“: Passphrase → Argon2id → QR-Code. Wiederherstellung über den Scanner, dazu Tests des Umzugs zwischen Geräten. Nicht umgesetzt.

Phase 6 — Migration bestehender Daten

Eine einmalige Routine im Client beim ersten Start der neuen Version:

  1. Alle eigenen Objekte vom Server lesen (die alten Klartextfelder).
  2. Einen K_obj für jedes davon erzeugen.
  3. Die Felder verschlüsseln und senden: data_cipher.
  4. Der Server leert die alten Spalten (phone=NULL, …) — aber erst nach Bestätigung.
Nach erfolgreicher Migration werden die veralteten Felder per ALTER entfernt.

Phase 7 — Audit

Ein Sicherheitsaudit, extern oder in Eigenregie. Wir prüfen, dass der Server sensible Daten wirklich nicht sieht: Ein Datenbank-Dump darf nichts als Blobs enthalten.

9. Risiken und Maßnahmen

RiskMeasure
Verlust von K_masterEin verpflichtender Sicherungsablauf (Phase 5). Solange keine Kopie existiert, wird ein Warnhinweis gezeigt.
Kompromittierung des BesitzergerätsNicht vollständig vermeidbar: Der Zugang zur App ist durch PIN oder Biometrie geschützt, dazu EncryptedSharedPreferences und Android Keystore.
Die Rotation eines Objektschlüssels erfordert den Neuaufbau aller BundlesDer Client des Besitzers nimmt alle bundle_cipher — lokal oder vom Server —, baut sie neu und sendet sie in einem Schwung. Eine seltene Operation.
Der Server manipuliert ein BundleDas AES-GCM-Tag passt nicht, und der Client meldet einen beschädigten Schlüssel.
Abfluss von K_guest über einen Screenshot des QR-CodesTechnisch nicht zu verhindern: Der Schlüssel steckt im Code selbst. Zeigen Sie den QR-Code nur einem Gast, dem Sie vertrauen.
Das Adressfragment bleibt im BrowserverlaufDie Seite /key ruft direkt nach dem Lesen des Hashs history.replaceState(..., '#')auf, was das Fragment löscht.

Zuletzt aktualisiert: 19. April 2026