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.
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.
v1:<base64url(nonce || ciphertext || tag)>
v1: — ein Versionspräfix, um das Format später zu wechseln.nonce — 12 zufällige Bytes, für jede Verschlüsselung neu.ciphertext+tag — alles, was Cipher.doFinalzurückgibt; das 128-Bit-Tag ist enthalten.SecureRandom() (die des Systems). Auf Android ist das /dev/urandom + Linux-PRNG.
EncryptedSharedPreferences unter dem Namen master_key und verlässt das Gerät nie im Klartext.K_obj zur lokalen Speicherung verwendet.encrypt_k_master(K_obj) unter dem Schlüssel obj_key_{id}.data_cipher auf dem Server).K_obj_new → data_cipher und jedes Bundle, das ihn trug, neu verschlüsseln.{obj_id: K_obj} für die Objekte, die dem Gast erlaubt sind.bundle_cipher. Der K_guest selbst bekommt der Server nie zu sehen.entrixy.com/key#<user_key><base64url(K_guest)> — ein einziger Block aus 75 Zeichen.
Das Fragment geht mit der HTTP-Anfrage nicht an den Server — eine grundlegende Garantie des Browsers.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.| Key | Wert |
|---|---|
master_key | 32 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. |
| Tabelle / Feld | Inhalt |
|---|---|
numbers.data_cipher | Mit K_obj verschlüsseltes JSON: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}. |
numbers.id / type | Im Klartext — nötig für Routing und die Anzeige des Symboltyps. |
keys.bundle_cipher | Mit K_guest verschlüsseltes JSON: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}. |
keys.key_hash / sign_pub | Nur ein Fingerabdruck des Schlüssels und die öffentliche Hälfte seines Signaturpaars. Der Schlüssel selbst erreicht den Server nie. |
keys.mode / force_when_busy | Im Klartext — für Routing und eine schnelle Zugriffsprüfung. |
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>
user_key und 43 Zeichen des Gastschlüssels.user_key erreicht den Server nie: Der Server kennt nur seinen Fingerabdruck, und das Telefon des Gastes beweist durch die Signatur seiner Anfragen, dass es den Schlüssel besitzt.#...) landet nie im Serverprotokoll — das ist eine Garantie des Browsers./key — schlichtes JS: sie liest location.hash, zerlegt den Block und übergibt den Schlüssel
entweder der App oder der Webversion /app/. Der Server sieht den Schlüssel nie./download/android,
von wo die App den Schlüssel selbst aufnimmt.K_obj.K_obj → data_cipher.POST /api/number_add mit data_cipher plus dem Typ. Der Server gibt eine id zurück.obj_key_{id} = encrypt_k_master(K_obj).Das neue JSON lokal bauen, mit dem vorhandenen K_obj → POST /api/number_update.
K_obj verschlüsseln, der sich nicht ändert.
[obj1, obj2, obj3]. Dabei entsteht K_guest.{obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.K_guest → bundle_cipher.POST /api/key_create mit bundle_cipher, obj_ids (im Klartext — für die Zugriffsmatrix).entrixy.com/key#{user_key}{base64url(K_guest)}./key reads location.hash und übergibt den Schlüssel der App oder der Webversion.POST /api/key_bundle.php mit einer vom Schlüssel signierten Anfrage → erhält bundle_cipher und die Liste der obj_ids.K_guest→ und erhält {obj_keys, welcome}.K_guest und die entschlüsselten obj_keys werden gespeichert im EncryptedSharedPreferences des Gast-Clients.data_cipher des Objekts → Entschlüsselung mit dem lokalen obj_key_{id}.A key carries delegate_depth — how many steps further it may travel.
The holder of such a key issues nothing itself: it creates a one-time invitation
(POST /api/key_delegate.php?a=create), and the key is born on the
recipient's device.
user_key and a P-256 pair,
and sends the public half with ?a=redeem.parent_key_id, delegate_depth = parent − 1 and no bundle.
Expiry and the app-only flag are inherited from the parent: nobody hands on more
than they hold.needs_bundle,
builds the bundle with a fresh K_guest and seals that key to the
recipient's public half (ECDH P-256 → HKDF-SHA256 → AES-256-GCM) → key_cipher.bundle_cipher und key_cipher,
unseals K_guest with its private half and reads the bundle as any other guest.Neither the server nor the person who passed the access on can read the child's bundle: the sealing key never leaves the recipient's device. The whole chain hangs off the owner — nobody re-encrypts anything on the way.
Der Besitzer drückt „Löschen“ → POST /api/key_revoke.php → the server deletes the key record.
On the next bundle request the guest is refused and loses access. Revocation is cascading:
everything issued further down that key's chain goes with it, along with any invitations
still waiting for a recipient.
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.
Losing K_master bedeutet den Verlust des Zugangs zu allen Objekten und Schlüsseln, daher erstellt die App eine Sicherung: eine Datei, verschlüsselt mit einem Passwort, das der Besitzer festlegt. Der Server ist daran nicht beteiligt: Er erstellt, speichert und liest die Datei nicht.
K_master, die Objekte und ihre Geheimnisse, die an Gäste ausgegebenen Schlüssel und die von anderen erhaltenen Schlüssel.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.
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.
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.
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.
Eine Sicherungsdatei in den Einstellungen: Passwort → PBKDF2-SHA256 → AES-256-GCM, und ihre Wiederherstellung auf einem neuen Telefon (Abschnitt 7).
Eine einmalige Routine im Client beim ersten Start der neuen Version:
K_obj für jedes davon erzeugen.data_cipher.phone=NULL, …) — aber erst nach Bestätigung.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.
| Risk | Measure |
|---|---|
| Verlust von K_master | Eine mit dem Passwort des Besitzers verschlüsselte Sicherungsdatei (Abschnitt 7). |
| Kompromittierung des Besitzergeräts | Nicht 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 Bundles | Der 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 Bundle | Das AES-GCM-Tag passt nicht, und der Client meldet einen beschädigten Schlüssel. |
| Abfluss von K_guest über einen Screenshot des QR-Codes | Technisch 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 Browserverlauf | Die Seite /key ruft direkt nach dem Lesen des Hashs history.replaceState(..., '#')auf, was das Fragment löscht. |
Zuletzt aktualisiert: 19. April 2026