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.user_key / 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 — die Kennung auf Serverseite. Ohne sie gibt der Server das Bundle nicht heraus.#...) 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).user_key.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 user_key → 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, deshalb ist ein Export unerlässlich.
K_master → es entsteht master_backup_blob.obj_keys vom Server — sie liegen im Bundle der eigenen Schlüssel des Besitzers — und verschlüsselt sie lokal neu.K_master, deshalb ist die Passphrase Pflicht.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.
Ein Einstellungsbildschirm mit „Hauptschlüssel exportieren“: Passphrase → Argon2id → QR-Code. Wiederherstellung über den Scanner, dazu Tests des Umzugs zwischen Geräten. Nicht umgesetzt.
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 | Ein verpflichtender Sicherungsablauf (Phase 5). Solange keine Kopie existiert, wird ein Warnhinweis gezeigt. |
| 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