Ende-zu-Ende-Verschlüsselung — Konzept (Whitepaper)
Stand: 08.11.2026 · Version 1.11
Dieses Whitepaper gibt es auch auf Englisch: English version
öffentliche Beschreibung des Verfahrens (Deutsch und Englisch) und Grundlage der
1. Ziel in einem Satz
Inhalte einer Familie und ihre inhaltlichen Metadaten werden nur auf den Geräten der Familie ver- und entschlüsselt. Die Schlüssel verlassen diese Geräte nie unverschlüsselt. Der Dienst speichert, verteilt und synchronisiert, kann aber nichts davon lesen, auch nicht, wenn er es wollte. Ausnahmen gibt es nur dort, wo eine Person ausdrücklich zustimmt (§9).
Das gilt für jede Familie ab ihrer Gründung (§12.2), und die Familien aus der Zeit vor der Umstellung sind vollständig umgestellt (§12.1). Einzige Ausnahme ist die öffentliche Beispielfamilie: Sie liegt absichtlich im Klartext, damit jeder sie ansehen kann (§4.7).
1.1 Was sich gegenüber dem früheren Verfahren ändert
| Früheres Verfahren | E2E | |
|---|---|---|
| Wer erzeugt Schlüssel | Browser (v1), bei Rotation die Edge Function | immer ein Gerät der Familie |
| Wer kann Schlüssel auspacken | der Dienst mit MEDIA_KEK, jederzeit | nur Geräte von Mitgliedern |
| Getippte Texte, Daten, Orte, Tags, Titel | Klartext (Orte mit Server-Schlüssel) | verschlüsselt |
| Zugang nach Passwortverlust | der Dienst gibt den Schlüssel neu aus | eigenes Gerät, Passkey, Wiederherstellungscode oder die Familie (§6) |
| Geteilte Download-Links | der Dienst entschlüsselt für den Empfänger | Schlüssel steht im Link-Fragment, der Dienst liefert nur Chiffrat (§9.1) |
MEDIA_KEK | existiert | vernichtet (17.10.2026, §12.3) |
1.2 Nicht-Ziele
- Kein Zero-Knowledge. Der Dienst sieht weiterhin: wer in welcher Familie ist, Rollen, E-Mail-Adressen, Profilnamen, Zeitpunkte, Größen, Anzahl der Objekte, Medientypen und Zugriffsmuster (vollständige Liste §10.4).
- Kein Schutz gegen ein kompromittiertes Endgerät. Wer das entsperrte Gerät eines Mitglieds hat, sieht, was dieses Mitglied sieht.
- Kein rückwirkender Entzug. Wer die Familie verlässt, behält den Zugriff auf das, was diese Person schon entschlüsseln konnte. Neues bekommt sie nicht.
- Keine Verfügbarkeitsgarantie gegen den Dienst. Der Dienst kann Daten zurückhalten oder löschen. Er kann sie nicht unbemerkt verändern (§4.3).
2. Bedrohungsmodell
| Angreifer | Fähigkeit | Ziel | Abgedeckt durch |
|---|---|---|---|
| A1 Datendieb | Kopie von Datenbank, Speicher oder Backups | Inhalte lesen | Alles Inhaltliche ist Chiffrat; ohne Gerät gibt es keine Schlüssel (§3). Alte Backups aus der Zeit vor der Umstellung werden gelöscht bzw. laufen ab (§12.3) |
| A2 Neugieriger Betreiber (passiv) | voller Lesezugriff auf alle Server, Logs und Funktionen | Inhalte lesen | wie A1; kein Klartext in Routen, Logs oder Fehlerberichten (§8) |
| A3 Böswilliger Betreiber (aktiv, über die Daten) | kann Zeilen fälschen, umhängen oder zurückspielen, Mitglieder anlegen, Umschläge und öffentliche Schlüssel tauschen, Schalter setzen | eigenen Schlüssel einschleusen, Inhalte verfälschen, Klartext erzwingen | signierte Umschläge (§4.2), Vertrauensspeicher (§5.4), menschliche Bestätigung jedes neuen oder geänderten Schlüssels (§5.3, §6.2), Kopplung mit Prüfcode (§5.2), gebundene und signierte Zeilen mit Revisionsstand (§4.3), keine Herabstufung auf Klartext (§4.7). Restrisiko siehe §10.3 |
| A4 Böswilliger Betreiber (aktiv, über den Code) | liefert anderes JavaScript aus | Schlüssel abgreifen | im Web nicht abgedeckt, offen benannt (§10.1). Die Android-App bringt ihren Code mit (§10.2) |
| A5 Ehemaliges Mitglied | kennt alte Familienschlüssel, ggf. ein Backup | Neues lesen | Rotation beim Austritt (§7), neue Tag-Indizes (§4.5) |
| A6 Link-Empfänger | hat einen Download-Link | mehr sehen als freigegeben | eigener Schlüssel je Link, nur die Auswahl wird damit neu verschlüsselt (§9.1) |
| A7 Drittanbieter (DeepL, Gelato, Render-Dienst, Mistral) | erhält Klartext bei Freigabe | mehr als freigegeben | Klartext nur nach Zustimmung, nur das Nötige, Löschfristen (§9) |
| A8 Netzwerk | TLS-Angreifer | — | TLS + HSTS; dazu sind die Inhalte ohnehin Chiffrat |
| A9 Kontodieb | kennt Passwort bzw. hat eine Sitzung, aber kein Gerät | Inhalte lesen, Konto übernehmen | Die Sitzung allein öffnet keinen Schlüssel. Einen Kontoschlüssel ersetzen geht nur nach frischer Anmeldung, mit Mail an die Person und 24 h Wartezeit (§6.2). Den Wiederherstellungscode ersetzen geht nur nach frischer Anmeldung und mit Mail (§6.3). Wer das Passwort kennt, kann sich frisch anmelden. Die Mail und die Wartezeit machen den Versuch sichtbar und abbrechbar |
3. Schlüsselhierarchie
┌─ Wiederherstellungscode ─(Argon2id)──┐
│ ▼
Gerät ── DSK (nicht extrahierbar) ──packt──▶ Kontoschlüssel AK ◀──packt── Passkey-PRF
(X25519 + Ed25519, je Person)
│ HPKE-Umschlag, signiert
▼
┌────────────────────────────┼─────────────────────────┐
▼ ▼ ▼
Familienschlüssel FMK_v Geschichtsschlüssel SK_v Profilbildschlüssel PAK_v
(je Familie, versioniert) (je gemeinsame Geschichte) (je Person, versioniert)
│ │ │
Zeilen, Medien (K_obj), Tags (TIK), Inhalte der gemeinsamen Profilbild
Familienbild/Logo, Übersetzungen Geschichte| Schlüssel | Art | Lebt wo | Erzeugt von |
|---|---|---|---|
| DSK (Geräte-Speicherschlüssel) | AES-256-GCM, extractable: false | je Gerät und Person: Web als CryptoKey in IndexedDB fm-e2e (Schlüssel dsk:<user>); Android im Keystore (Alias fm-e2e-dsk2-<user>, nur bei entsperrtem Gerät nutzbar, StrongBox wenn vorhanden; Schlüssel älterer Apps unter fm-e2e-dsk-<user> ziehen beim nächsten Öffnen um und werden gelöscht) | dem Gerät, beim ersten Einpacken |
| AK (Kontoschlüssel) | X25519-Paar (Einpacken) + Ed25519-Paar (Signieren) | privat: nur gepackt (DSK auf dem Gerät; Passkey-PRF bzw. Wiederherstellungscode in ak_backups); öffentlich: user_identity_keys mit Selbstsignatur | dem ersten Gerät der Person |
| FMK_v | 32 Byte zufällig | Server nur als Umschlag je berechtigter Person | Eigentümer-Gerät (Gründung, Rotation) |
| SK_v | 32 Byte zufällig | Umschlag je Beteiligtem | Gerät der Autorin bzw. des Autors |
| PAK_v | 32 Byte zufällig | Umschlag je Person, die das Bild sehen darf | Gerät der Person |
| LK (Link-Schlüssel) | 32 Byte zufällig | nur im URL-Fragment #k= | Gerät der teilenden Person |
| TIK_v (Tag-Index) | HMAC-SHA256-Schlüssel, HKDF aus FMK_v | nie gespeichert | abgeleitet |
| K_obj (je Mediendatei) | AES-256-GCM, HKDF aus FMK_v/SK_v | nie gespeichert | abgeleitet (§4.3) |
Format des AK: 0x01 ‖ x25519_priv ‖ x25519_pub ‖ ed25519_seed ‖ ed25519_pub (129 Byte). Beim
Auspacken prüft das Gerät, dass privater und öffentlicher Teil zusammenpassen. Gepackt wird mit
AES-256-GCM und AAD = enc("fm/ak-wrap/v1", Art, user_id, Kennung). Die Art ist device, recovery
oder passkey. Die Kennung ist beim Gerät die Identitäts-id, beim Passkey die Credential-id, beim
Wiederherstellungscode leer.
Schlüsseltrennung. Aus FMK_v/SK_v (bzw. PAK_v) werden mit HKDF getrennte Unterschlüssel abgeleitet:
Zeilen (fm/row-key/v2), Tag-Index (§4.5), Medienbasis (§4.3) und Bilder mit mediaSeal
(fm/media-seal/v2). In Format v2 (§4.3) verschlüsselt der Rohschlüssel selbst nichts mehr. Alle Geräte
lesen v2; bis der Schalter (§4.3) an ist, schreiben sie noch v1, wo FMK_v zugleich
AES-Schlüssel für Zeilen und Bilder und Eingabe für TIK und Medienbasis ist. Das ist nach heutigem
Wissen nicht ausnutzbar, widerspricht aber der Regel „ein Schlüssel, ein Zweck“.
Warum je Person und nicht je Gerät: Die Familie packt einmal pro Person ein. Ein neues eigenes Gerät braucht nur den AK von einem eigenen Gerät, vom Passkey oder aus dem Wiederherstellungscode und berührt die Familie nicht. Der Sicherheitscode bleibt über Gerätewechsel gleich. Eine Rotation kostet O(Mitglieder) statt O(Geräte).
4. Kryptografische Bausteine
4.1 Parameter
| Zweck | Verfahren | Umsetzung |
|---|---|---|
| Symmetrisch | AES-256-GCM, 96-Bit-IV aus crypto.getRandomValues, 128-Bit-Tag | WebCrypto |
| Einpacken an Personen | HPKE (RFC 9180), Modus Base, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM | @hpke/core 1.9, @hpke/dhkem-x25519 1.8 |
| Signaturen, X25519 des AK | Ed25519 (Prüfung streng nach RFC 8032: keine nicht-kanonischen Kodierungen, keine Schlüssel kleiner Ordnung — nicht der ZIP-215-Vorgabewert der Bibliothek) bzw. X25519 | @noble/curves 2.4 (@hpke/dhkem-x25519 bringt intern eine eigene Kopie der noble-X25519-Implementierung mit) |
| Ableitung | HKDF-SHA256 | WebCrypto |
| Wiederherstellungscode | Argon2id, m = 64 MiB, t = 3, p = 1, 16-Byte-Salz, 32 Byte Ausgabe | hash-wasm |
| Passkey | WebAuthn-Erweiterung PRF, eigenes 32-Byte-Salz je Passkey | navigator.credentials |
| Blind Index | HMAC-SHA256, gekürzt auf 128 Bit | WebCrypto |
Kodierung. Jede Folge von Feldern, die in eine AAD, einen HKDF-info, einen HPKE-info oder eine
Signatur eingeht, wird mit enc(…) kodiert: je Feld eine 32-Bit-Länge (Big Endian) und die Bytes;
Zeichenketten als UTF-8, Zahlen als u32 Big Endian. Jede Folge beginnt mit einem eigenen Trenner
fm/<zweck>/v<n>. Damit ist jede Kodierung eindeutig, und kein Wert aus einem Zweck passt in einen
anderen. Die einzige Ausnahme ist das HKDF-Salz der Download-Links ("fm/share/v1" als rohes UTF-8,
§9.1). Die Zeilen-id des Übersetzungs-Caches setzt Tabelle, id, Spalte und Sprache mit | zu einem Feld
zusammen. Wegen der festen Werte und der UUID ist das eindeutig; in Format v2 sind die vier Teile
zusätzlich einzeln mit enc(…) als Bezüge gebunden (§4.3), die zusammengesetzte id benennt dann nur den
Eintrag.
Die Kurven laufen über @noble/curves statt WebCrypto, weil ältere Systeme (iOS < 17) X25519/Ed25519 in
WebCrypto nicht haben. Der private AK liegt deshalb beim Entsperren als Bytes im Arbeitsspeicher der
Seite (§10.7).
4.2 Umschlag (key_envelopes)
Zeile: scope ("family" | "story" | "avatar"), scope_id, version,
recipient (user_id), recipient_identity, enc (32 B), ct (48 B),
sender (user_id), sender_identity, sig (64 B)
HPKE-info = enc("fm/env/v1", scope, scope_id, version, recipient, recipient_X25519_pub)
sig = Ed25519(AK_sender, enc("fm/env-sig/v1", scope, scope_id, version, recipient,
recipient_X25519_pub, sender, enc, ct))Der Empfänger übernimmt einen Umschlag nur, wenn
- er selbst der Empfänger ist und die HPKE-Öffnung mit seinem aktuellen AK gelingt,
- die Signatur gültig ist unter dem aktuellen AK des Absenders aus dem eigenen Vertrauensspeicher (§5.4). Ein früherer, ersetzter AK des Absenders (nach einer Rettung, §6.2) zählt nur noch für Versionen älter als die aktuelle: Was die Person damals eingepackt hat, bleibt lesbar. Wer ihr altes Gerät gestohlen hat, kann aber auch zusammen mit dem Dienst keinen Schlüssel für die aktuelle oder eine neuere Version unterschieben.
- beim Profilbild der Absender die Person selbst ist.
Mehrere Umschläge, ein Prüfwert. Je Empfänger und Version darf jeder berechtigte
Absender einen eigenen Umschlag ablegen (der Primärschlüssel enthält sender_identity); niemand belegt mehr den Platz
eines anderen. Wer eine Version anlegt, veröffentlicht einmal ihren Prüfwert (e2e_set_key_commit, nur der Ersteller,
nur einmal):
commit = SHA-256(enc("fm/key-commit/v1", scope, scope_id, version, Schlüssel))
commit_sig = Ed25519(AK_Ersteller, enc("fm/key-commit-sig/v1", scope, scope_id, version, commit))Das Gerät öffnet alle Umschläge einer Version. Hat sie einen Prüfwert, nimmt es nur einen dazu passenden Schlüssel;
kennt es den Ersteller, muss dessen Signatur gültig sein, sonst gilt die ältere Regel. Ein Mitglied, das einem
Neuling einen anderen Schlüssel einpackt, erreicht damit nichts. Versionen, die vor dem 20.10.2026 angelegt wurden, haben keinen Prüfwert: Gibt es
dort zwei verschiedene Schlüssel, gilt nur der aus dem Umschlag des Erstellers, sonst keiner. Bei eigenen Versionen
ohne Prüfwert (Gründung, Bestand) rüstet das Gerät des Erstellers ihn nach. Nach dem Öffnen legt das Gerät eine
Empfangsquittung ab (key_receipts, nur für sich selbst und nur für eine Version, für die es einen Umschlag hat).
Wartelisten und Rotationsabschluss richten sich nach ihr (§5.3, §7), nicht mehr nach dem bloßen Vorhandensein
irgendeines Umschlags (Ergebnis der internen Prüfung).
Wer überhaupt einpacken darf, erzwingt die Datenbank: bei Familien Eigentümer und Mitglieder, nicht Familienfreunde; bei Geschichten nur die Autorin bzw. der Autor, solange sie der Familie als Eigentümerin oder Mitglied angehört — sonst der Eigentümer; beim Profilbild nur die Person selbst. Eine Version einpacken darf außerdem nur, wer sie angelegt oder selbst bekommen hat. Das schützt gegen Personen in der Familie, die ihre Rolle überschreiten. Gegen den Dienst selbst hilft eine Rollenprüfung beim Empfänger nicht, denn die Rollen kommen vom Dienst. Dagegen schützen Vertrauensspeicher und menschliche Bestätigung.
recipient_identity und sender_identity dienen der Zuordnung und der Zugriffsregel. Sie sind nicht
signiert, die Signatur bindet stattdessen die öffentlichen Schlüssel selbst.
Erster Kontakt. Ein Gerät, das für einen Bereich noch gar keinen Schlüssel hält (neues Mitglied), kennt die Absender noch nicht. Es öffnet alle gültig signierten Umschläge (welcher Schlüssel gilt, entscheidet der Prüfwert) und nimmt die Absender in seinen Vertrauensspeicher auf (TOFU). Dieses Restrisiko steht in §10.3.
4.3 Verschlüsselte Zeilen (enc-Spalte)
Jede inhaltliche Zeile hat eine Spalte enc bytea und enc_key_version int. Darin liegt ein JSON-Objekt
mit allen inhaltlichen Feldern.
Format v1 (gebaut):
AAD = enc("fm/row/v1", 1, table, row_id, scope_id, enc_key_version)
pt = u32(len(json)) ‖ json ‖ 0x00… (gepolstert, §4.4)
enc = 0x01 ‖ iv(12) ‖ AES-GCM(FMK_v bzw. SK_v, pt, AAD)scope_id ist die Familie oder, bei gemeinsamen Geschichten, die Geschichte. Die row_id (UUID) erzeugt
der Client vor dem Verschlüsseln. Die AAD bindet das Chiffrat an Tabelle, Zeile, Bereich und Version: Eine
Bildunterschrift lässt sich nicht an ein anderes Foto oder in eine andere Familie hängen.
Format v2 (Lesen und Schreiben gebaut, eingeschaltet am 14.10.2026): v1 bindet weder die Bezüge noch die Autorin, und es kennt keinen Revisionsstand. Ein böswilliger Dienst könnte deshalb einen Kommentar an ein anderes Foto hängen, ein Kapitel in eine andere Geschichte verschieben, eine ältere Fassung zurückspielen. Ein Mitglied könnte Texte im Namen eines anderen schreiben. v2 schließt das:
K_row = HKDF-SHA256(FMK_v bzw. SK_v, salt = "", info = enc("fm/row-key/v2"))
AAD = enc("fm/row/v2", table, row_id, scope_id, enc_key_version, enc(Bezüge…),
Unterzeichner-Schlüssel-id, rev, flags)
pt = gepolstert(JSON {a: Autor, f: Felder})
enc = 0x02 ‖ Unterzeichner-Schlüssel-id(16) ‖ u32(rev) ‖ flags(u8) ‖ iv(12) ‖ AES-GCM(K_row, pt, AAD) ‖ sig(64)
sig = Ed25519(Unterzeichner, enc("fm/row-sig/v2", AAD, Unterzeichner-user_id, iv ‖ ct))Die AAD enthält nur, was der Leser ohne Nachschlagen kennt: die Zeile und den Kopf des Chiffrats. Die Autorin steht verschlüsselt im Inhalt und wird nach dem Öffnen mit der Spalte verglichen; die Person hinter der Schlüssel-id steht in der Signatur. So öffnet eine Zeile mit dem Familienschlüssel auch dann noch, wenn das Konto ihrer Autorin gelöscht ist — die Datenbank leert dann die Autorenspalte, und der Kontoschlüssel verschwindet mit dem Konto. Angezeigt wird sie dann nicht (die Autorin passt nicht mehr zur Spalte), aber der Eigentümer kann sie als „Bestand“ übernehmen, statt sie zu verlieren.
- Schlüsseltrennung: Der rohe Familien- bzw. Geschichtsschlüssel verschlüsselt in v2 nichts mehr
selbst. Zeilen, Familienbild/Logo/Profilbild (
mediaSeal, unten) und Tag-Index (§4.5) haben je einen eigenen HKDF-Unterschlüssel. - Bezüge und Autor: Die AAD enthält je Tabelle die Spalten, die eine Zeile verorten — Ziel eines
Kommentars, Geschichte und Position eines Kapitels, Album/Geschichte/Kapitel eines Fotos,
is_collaborativeeiner Geschichte,name_idxeines Tags, Quelle und Zielsprache einer Übersetzung. Der Leser nimmt diese Werte aus derselben Zeile, die der Dienst schickt; ist einer davon verändert, öffnet die Zeile nicht. Die Autorin bzw. der Autor im Inhalt muss der Spalte gleichen, sonst gilt die Zeile als nicht geprüft. Verschiebt jemand ein Kapitel, wird es neu versiegelt.albums.cover_photo_idist nicht gebunden (§10.8). - Signatur und wer unterzeichnen darf: Jede Fassung ist mit dem Ed25519-Teil des AK signiert. Der Leser löst den Schlüssel über den Vertrauensspeicher auf (§5.4) und prüft, ob diese Person die Zeile unterzeichnen darf: bei Geschichten, Kapiteln, Fotos, Alben und Kommentaren die Autorin bzw. der Autor oder der Eigentümer (in einer gemeinsamen Geschichte auch deren Autorin), bei Tags jedes Mitglied, bei Übersetzungen nur die Person selbst (jede hat ihren eigenen Eintrag), bei der Familienzeile nur der jeweils aktuelle Eigentümer (sie bindet keine Autorin: nach einer Eigentumsübertragung versiegelt das Gerät des neuen Eigentümers sie neu). Ein früherer Schlüssel einer Person zählt nur für Zeilen einer älteren Schlüsselversion.
- Revision: Jedes Gerät merkt sich je Zeile die höchste gesehene Revision. Eine niedrigere, oder eine v1-Fassung einer Zeile, die es schon als v2 gesehen hat, gilt als zurückgespielt.
- Was der Leser zeigt: Schlägt Signatur, Unterzeichnerregel oder Revision fehl, zeigt die App den Platzhalter „Echtheit nicht bestätigt“ — nie den Text.
- „Nur v2“: Sobald die vom Eigentümer signierte Familienzeile (bzw. die Zeile einer gemeinsamen Geschichte) den Vermerk „nur v2“ trägt, zählen v1-Zeilen dieses Bereichs auf dem Gerät nicht mehr — dauerhaft, auch wenn der Dienst später eine ältere Kopfzeile ohne Vermerk ausliefert.
- Übersetzungen: Die Zeilen-id eines Cache-Eintrags ist
Quelltabelle|Quell-id|Spalte|Zielsprache, also je Eintrag eindeutig (für die Revision); die vier Teile sind zusätzlich als Bezüge eindeutig kodiert gebunden. Ein Eintrag, der die Prüfung nicht besteht, gilt als nicht vorhanden und wird neu übersetzt. Jede Person liest nur Einträge, die sie selbst signiert hat: Sonst könnte ein Mitglied für Omas Geschichte eine erfundene „Übersetzung“ ablegen, die andere als Omas Text lesen (Ergebnis der internen Prüfung). Die Datenbank führt deshalb einen Eintrag je Schreibendem (written_by). - Verknüpfungen (Tags an Fotos, Geschichten und Alben, Zeitleisten-Verknüpfungen) haben keinen Inhalt.
Wer eine anlegt, signiert sie:
sig = Ed25519(Person, enc("fm/link/v2", Tabelle, enc(Spalten…), Person, Schlüssel-id))in den Spaltensigundsig_identity. Leser verwerfen Verknüpfungen mit ungültiger Signatur oder von Personen, die in der Familie nicht schreiben dürfen; unsignierte zählen nur, solange der Bereich nicht „nur v2“ ist. - Reihenfolge: Erst lesen alle Geräte v2 (Web und App 1.0.8), geschrieben wird bis dahin v1.
Dann schaltet ein globaler Schalter in der Datenbank (
e2e_settings.rows_v2_since) alle Schreiber auf v2 — zur Laufzeit gelesen, weil die App Umgebungswerte beim Bau festschreibt. Ein Gerät, das ihn einmal an gesehen hat, bleibt dabei und merkt sich den frühesten Zeitpunkt: Der Dienst kann niemanden auf v1 zurückdrehen und die Frist unten nicht nach hinten verschieben. Vorziehen kann er sie: Meldet er einen Zeitpunkt weit in der Vergangenheit, übernimmt der Eigentümer fremde v1-Zeilen sofort statt nach 14 Tagen als „Bestand“ (§10.9). Inhalte gehen dabei nicht verloren, und das Gerät der Autorin ersetzt den Vermerk durch ihre eigene Signatur, sobald es sich meldet. Auch ohne Schalter schreibt ein Gerät v2, sobald der Bereich „nur v2“ ist. - Neu versiegeln: Nach dem Schalter arbeitet jedes Gerät im Hintergrund ab, was noch offen
ist. Was zu tun ist, liest es jedes Mal aus dem unverschlüsselten Kopf der Chiffrate (Format,
Schlüssel-id, Vermerk), ohne sie zu öffnen; ein abgebrochener Lauf macht beim nächsten Öffnen weiter.
Jede Person versiegelt ihre eigenen alten Zeilen neu, ebenso eigene Zeilen mit dem Vermerk „Bestand“
oder mit einem früheren eigenen Schlüssel (nach einer Rettung). Der Eigentümer (in einer gemeinsamen
Geschichte deren Autorin) übernimmt sofort die Familienzeile, Tags, Familienbild und Logo und die Zeilen
gelöschter Konten; 14 Tage nach dem Schalter auch alle übrigen fremden v1-Zeilen, jeweils mit dem
Vermerk „Bestand“ (
flagsBit 0) und unter Beibehaltung der Autorin. Meldet sich die Autorin später, ersetzt ihr Gerät das durch die eigene Signatur. Den Vermerk darf nur der Eigentümer setzen. Die eigenen Zeitleisten-Verknüpfungen signiert ihre Erstellerin, Tag-Verknüpfungen und verwaiste der Eigentümer. Kommentare und Zeitleisten-Verknüpfungen ändert der Client nur über zwei schmale Funktionen. - „Nur v2“ setzen: Ist im Bereich keine Klartext- oder v1-Zeile und keine ungeprüfte Verknüpfung mehr übrig, setzt das Gerät des Eigentümers den Vermerk in der Familienzeile (bzw. der Zeile der gemeinsamen Geschichte) selbst. Übersetzungen werden nicht umgeschrieben: alte Einträge gelten danach als nicht vorhanden und werden beim nächsten Abruf neu übersetzt und in v2 abgelegt.
| Tabelle | Felder in enc | bleibt sichtbar |
|---|---|---|
stories | Titel, Text, Transkript, Datum | id, family_id, author_id, created_at, is_collaborative, Pfad der Aufnahme |
story_chapters | Titel, Text, Transkript, Datum | id, story_id, author_id, position, created_at |
photos | Bildunterschrift, Aufnahmedatum, Dauer, Ort (Koordinaten und Ortsname) | id, family_id, uploaded_by, created_at, Medientyp (Foto/Video), Pfade, Größen, Bezüge |
albums | Titel, Datum | id, family_id, cover_photo_id |
tags | Name | id, family_id, name_idx (Blind Index, §4.5) |
comments | Text | id, Ziel, author_id, created_at |
families | Name, Motto, Akzentfarbe | id, Eigentümer, Tarif, Kontingent, Pfade von Familienbild und Logo |
content_translations | {sourceHash, text}, immer unter FMK (v2: K_row aus FMK) | family_id, Quelltabelle, Quell-id, Spalte, Zielsprache, Dienst, Zeitpunkt |
photo_tags, story_tags, album_tags, timeline_links | — (reine Verknüpfungen) | alles; signiert in sig/sig_identity (ab dem Schalter) |
Profilnamen (profiles.name) bleiben Klartext (§10.4). Die Datenbank begrenzt die Größe von enc je
Tabelle (Geschichten und Kapitel 512 KiB, Kommentare 32 KiB, Fotos 16 KiB, Alben und Familie 8 KiB,
Tags 4 KiB, Übersetzungen 512 KiB).
Medien. Jede Datei hat einen eigenen Schlüssel, abgeleitet aus dem Familien- bzw. Geschichtsschlüssel der Version N:
K_obj = HKDF-SHA256(FMK_N bzw. SK_N, salt = "",
info = enc("fm/media-object/v1", Bereich, Bereichs-id, Pfad, N, Zeilen-id))Die Zeilen-id ist die photo_id bzw. die Geschichte oder das Kapitel einer Tonaufnahme. Wird eine Datei
auf einen anderen Pfad, an eine andere Zeile, in eine andere Familie oder Version geschoben, öffnet sie mit
einem anderen Schlüssel, und GCM lehnt sie ab. Die Version steht im Pfad (….e2e.v<N>.enc). Der Leser
leitet den Schlüssel aus genau dem Pfad ab, den er lädt. K_obj ist ein nicht extrahierbarer WebCrypto-
Schlüssel. Formate:
- Foto, Vorschaubild, Videoposter:
iv(12) ‖ AES-GCM(K_obj, Datei). Die Bindung kommt allein über K_obj. Ein Original in voller Auflösung wird in E2E-Familien nicht gespeichert. - Video (FMV2): Kopf im Klartext:
"FMV2", Version (u8), Strom-id (16 Byte, zufällig), Blockgröße (u32, 1 MiB), Blockzahl (u32), Gesamtlänge (u64), MIME-Länge (u16), MIME. Dann je Blockiv(12) ‖ AES-GCM(K_obj, Block, AAD = Kopf ‖ u32(Blockindex)). Der Kopf ist so in jedem Block authentisiert. Blöcke lassen sich weder vertauschen noch kürzen. Der Service Worker entschlüsselt mit demselben Schlüssel, den er perpostMessageals nicht extrahierbarenCryptoKeybekommt. Ein unbekanntes Versionsbyte wird beim Lesen abgelehnt. - Ton:
iv(12) ‖ AES-GCM(K_obj, u16(MIME-Länge) ‖ MIME ‖ Audio). - Familienbild, Logo, Profilbild:
mediaSealmitAAD = enc("fm/media/v1", Bereich, Bereichs-id, Pfad, Version). Familienbild und Logo liegen unter FMK (<familie>/branding/<art>.<8 hex>.e2e.v<N>.enc), das Profilbild unter PAK (<user>/avatar.<8 hex>.e2e.v<N>.enc). In Format v2 trägt der Name.s2vor.e2e.v<N>.enc; versiegelt wird dann mitHKDF-SHA256(FMK_N bzw. PAK_N, salt = "", info = enc("fm/media-seal/v2"))undAAD = enc("fm/media/v2", …)wie oben. Der Leser wählt Schlüssel und Format nach dem Pfad, den er lädt; ein Bild unter.s2öffnet nie mit dem rohen Schlüssel.
Bindung an die signierte Zeile (Ergebnis der internen Prüfung). K_obj allein reicht nicht: Wer einen
alten Familienschlüssel hält, etwa ein ausgetretenes Mitglied, könnte zusammen mit dem Dienst für jede
Zeile ein Objekt bauen, das fehlerfrei öffnet. Der signierte Inhalt einer Zeile trägt deshalb obj: je
Rolle den SHA-256 des gespeicherten Chiffrats (base64url). Rollen sind full und thumb (Foto; beim
Video ist thumb das Poster), blocks (Video), audio (Geschichte, Kapitel), cover und logo
(Familienkopf). Der Leser öffnet ein Objekt nur, wenn der Hash seines Chiffrats im obj der geöffneten
Zeile steht. Eine Zeile, die nicht öffnet, zeigt auch keine Medien. Geprüft wird an einer Stelle: Jeder
Leseschlüssel entsteht in objectKeyFor und ist dort an seine Zeile gebunden, und die Entschlüsselung
(Bild, Ton, Videoblock) fragt vor dem Öffnen nach.
- Video: Ein Hash je Block passt nicht in die Zeile. Neben dem Video liegt eine Blockliste
….blocks.e2e.v<N>.encmit eigenem K_obj:"FMB1" ‖ u32(n) ‖ SHA-256(Kopf) ‖ n × SHA-256(Chiffrat von Block i).obj.blocksbindet das Chiffrat der Liste. Der Leser lädt sie vor dem ersten Block und prüft jeden Block. Der Service Worker bekommt die geprüfte Liste von der Seite und prüft jeden Block zwischen Laden und Entschlüsseln. - Familienbild und Logo: Der Familienkopf wird vor den Bildern geöffnet und bindet beide.
- Ändern: Ton ersetzen oder entfernen und Familienbild oder Logo ändern versiegelt die Zeile neu, statt nur die Pfadspalte zu setzen. Ohne Schlüssel auf dem Gerät wird nichts geschrieben.
- Bestand: Objekte, die vor dem 18.10.2026 hochgeladen wurden, bindet ein Planer nach (§12.4). Danach setzt der
Eigentümer im signierten Kopf
mediaBound. Ein Gerät merkt sich das dauerhaft und öffnet von da an keine Medien ohne Bindung mehr.
Profilbild. Es hat keine signierte Zeile, und seinen Schlüssel PAK halten alle, die die Person kennen. Die Person signiert deshalb das gespeicherte Objekt selbst:
"FMAV" ‖ u8(1) ‖ Identitäts-id ‖ u64(signedAt) ‖ sig ‖ versiegeltes Bild
sig = Ed25519(AK, enc("fm/avatar/v1", user_id, Identitäts-id, Pfad, u64(signedAt), SHA-256(versiegeltes Bild)))Der Leser prüft die Signatur gegen den Schlüssel, den sein Vertrauensspeicher für diese Person kennt. Er
lehnt ein älteres signedAt ab, als er schon gezeigt hat, und nimmt, sobald er von einer Person einmal
ein signiertes Bild gezeigt hat, von ihr keine unsignierten mehr an. Das eigene Gerät signiert ein
älteres Bild beim nächsten Öffnen der App nach (und versiegelt es dabei, falls nötig, im Format v2).
4.4 Längen
Texte werden vor dem Verschlüsseln auf Stufen gepolstert: mindestens 64 Byte, bis 256 Byte auf 64 Byte,
bis 4 KiB auf 512 Byte, darüber auf 4 KiB. Gerechnet wird über u32(Länge) ‖ JSON, aufgefüllt mit
Nullbytes, die beim Öffnen geprüft werden. So verrät die Länge eines Titels nicht seinen Inhalt.
Mediengrößen bleiben sichtbar. Sie werden für das Kontingent gebraucht, und der FMV2-Kopf nennt ohnehin
die genaue Länge (§10.4).
4.5 Tags ohne Klartext
TIK_v = HKDF-SHA256(FMK_v, salt = "", info = enc("fm/tag-index/v1"))
name_idx = base64url( HMAC-SHA256(TIK_v, normalize(name))[0..16] )
normalize = NFKC → trim → Leerraum zu einem Leerzeichen → Kleinbuchstabenname_idx ist eindeutig je (family_id, name_idx). Nach einer Rotation versiegelt das rotierende Gerät
jedes Tag einer älteren Version neu und berechnet den Index neu (reindexSealedTags). Bis dahin sucht das
Anlegen eines Tags auch unter den Indizes älterer Versionen, damit keine Dublette entsteht. Ein ehemaliges
Mitglied könnte mit dem alten TIK und einem Datenbankabzug Tag-Namen raten, nach der Neuberechnung nicht
mehr.
4.6 Sortieren, Filtern, Suchen
Der Server sortiert nur nach created_at und liefert die Zeilen einer Familie in einem Rutsch. Datum,
Zeitleiste, Jahre, Tags und Alben entstehen im Browser aus den entschlüsselten Zeilen. Einen
gespeicherten Suchindex gibt es nicht. Eine Volltextsuche über verschlüsselte Inhalte gibt es heute nicht;
wenn sie kommt, dann nur lokal.
4.7 Keine Herabstufung
Ob eine Familie verschlüsselt ist, darf nicht davon abhängen, was der Dienst gerade meldet:
- Kann das Gerät die Liste seiner E2E-Bereiche nicht laden, schreibt es nichts. Ein Fehler heißt nie „klassisch weiter“.
- Es gibt keinen Klartext-Schreibweg mehr. Fehlt dem Gerät der Schlüssel eines Bereichs, lehnt es jede Schreibung ab, gleich was der Dienst meldet: Lässt er eine Familie aus der Liste weg, markiert er eine Geschichte als „gemeinsam“, ohne dass ihr Schlüssel da ist, oder ist das Gerät neu und kennt die Familie noch nicht, dann wird nichts geschrieben. Das gilt für Geschichten, Kapitel, Kommentare, Transkripte, Bildunterschriften, Tags, Alben sowie Name, Motto und Farbe der Familie; Medien waren schon so gesperrt. Die Liste der E2E-Bereiche entscheidet nur noch, welcher Schlüssel genommen wird, nie ob verschlüsselt wird.
- Jedes Gerät merkt sich zusätzlich jeden Bereich, den es einmal als E2E gesehen hat.
- Die Datenbank lehnt Klartext in E2E-Familien zusätzlich ab: Inhaltsspalten, Klartext-Ort, Ort unter
dem Server-Schlüssel, Originale, Objekte ohne
.e2e.v<N>.encsowie Familienbild und Logo im Klartext. Das schützt gegen alte App-Versionen, nicht gegen den Dienst selbst; dagegen schützen die Regeln im Client. - Jede Familie entsteht verschlüsselt (§12.2). E2E heißt seit dem 17.10.2026 schlicht „nicht die Beispielfamilie“;
der frühere Schalter
families.e2e_pilotist entfallen. - Die alten Lesewege sind entfernt (17.10.2026): Der Client zeigt keine Klartextzeilen, öffnet keine Medien unter
den Dateinamen des früheren Verfahrens und lädt kein Familienbild bzw. Logo ohne
.e2e.v<N>.enc. Klartext liest er nur für Demokonten, und die sehen ausschließlich die Beispielfamilie.
5. Identität und Vertrauen
5.1 Erstes Gerät, erster AK
Nach der ersten Anmeldung (Passwort oder Magic Link, unverändert):
- Das Gerät erzeugt den AK und packt ihn mit dem DSK.
- Es veröffentlicht
AK_pubinuser_identity_keysmit der SelbstsignaturEd25519(AK, enc("fm/ak-self/v1", user_id, x25519_pub, ed25519_pub)). Höchstens ein Schlüssel je Person ist aktiv. Ältere tragenreplaced_atund werden nie gelöscht, außer mit dem Konto. Das Gerät speichert den AK zuerst bei sich und veröffentlicht ihn erst danach. Ein abgebrochener Lauf sperrt das Konto so nicht. Auf diesem Weg entsteht nur der erste Schlüssel einer Person (oder einer, wenn keiner aktiv ist). Einen aktiven ersetzt nur „Neu anfangen“ (§6.2). - Ist das Konto schon in einer Familie oder Geschichte, liegt aber weder auf dem Server noch auf dem Gerät ein AK, fragt die App zuerst „Ist das dein erstes Gerät?“, statt still einen neuen Schlüssel anzulegen. Ein inzwischen aufgetauchter Schlüssel gewinnt.
- Eigentümer sehen eine Karte ohne „Später“, bis ein Wiederherstellungscode hinterlegt ist (Pflicht). Alle anderen bekommen ihn angeboten. Einen Passkey gibt es in den Kontoeinstellungen.
Der Fingerabdruck eines AK ist SHA-256(enc("fm/ak-fp/v1", x25519_pub, ed25519_pub)).
5.2 Weitere eigene Geräte
| Weg | Ablauf |
|---|---|
| Kopplung per QR-Code oder Kurzcode | siehe unten |
| Passkey | Synchronisierte Passkeys (iCloud, Google) liefern auf dem neuen Gerät dieselbe PRF-Ausgabe → packt den AK aus ak_backups(kind='passkey') |
| Wiederherstellungscode | Code eintippen → Argon2id → packt den AK aus ak_backups(kind='recovery') |
| Nichts davon | Rettung durch die Familie (§6.2) |
Bei jedem Weg übernimmt das neue Gerät den AK nur, wenn er dem aktiven öffentlichen Schlüssel der Person entspricht.
Kopplung. Das neue Gerät erzeugt ein Wegwerf-X25519-Paar und eine Nonce. In device_pairings legt es
zunächst nur ein Commitment ab: commitment = SHA-256(enc("fm/pair-commit/v1", new_pub, nonce)). Es
zeigt einen QR-Code mit dem Link …/account/geraet-koppeln#p=<pairing-id>&k=<new_pub>&n=<nonce> und
einen 8-stelligen Kurzcode (Crockford-Base32) zum Abtippen. Das alte Gerät erzeugt ebenfalls ein
Wegwerfpaar und legt seinen Schlüssel ab. Erst danach deckt das neue Gerät new_pub auf. Beide Geräte
zeigen einen sechsstelligen Prüfcode:
code = u32(HKDF-SHA256(ECDH(new, old), salt = nonce,
info = enc("fm/pair-code/v1", new_pub, old_pub))[0..4]) mod 10^6Erst wenn die Person am alten Gerät „Stimmt überein“ tippt, versiegelt das alte Gerät den AK mit AES-GCM unter demselben Geheimnis, aus dem der verglichene Code stammt:
K_pair = HKDF-SHA256(ECDH(old, new), salt = 32 zufällige Byte,
info = enc("fm/pair-transfer/v1", user_id, nonce, new_pub, old_pub))Bis zum 18.10.2026 ging der AK per HPKE an new_pub. Das hätte auch der Dienst bauen können, der beide öffentlichen
Schlüssel kennt. Mit dem AK reist der Vertrauensspeicher des alten Geräts; das neue Gerät mischt ihn als
Startstand ein. Das neue Gerät übernimmt den AK erst nach seinem eigenen „Stimmt überein“ (beide Geräte
bestätigen). Beide zeigen den Hinweis, nur zu bestätigen, wenn man beide Geräte selbst in der Hand
hat; nach dieser Zahl fragt das Team nie. Danach löscht das neue Gerät die Zeile. Eine Kopplung läuft nach
10 Minuten ab.
- QR-Weg: Das alte Gerät nimmt
new_pubundnoncevom Bildschirm des neuen Geräts und prüft sie gegen das Commitment in der Zeile. Ein Schlüssel, den der Dienst unterschiebt, fällt sofort auf. - Kurzcode: Beide Schlüssel kommen über den Server. Das Commitment zwingt jede Seite, sich festzulegen,
bevor sie den Schlüssel der anderen kennt: Das neue Gerät hat sich auf
new_pubfestgelegt, bevor esold_pubsieht. Das alte Gerät legtold_pubab, bevornew_pubaufgedeckt ist, und prüft das Aufgedeckte gegen das Commitment. Wer sich dazwischenschaltet, hat genau einen Versuch auf den sechsstelligen Prüfcode (1 : 1 000 000), statt Schlüssel durchprobieren zu können (wie bei ZRTP). Eine Zeile ohne Commitment (App vor 12.10.2026) nimmt das alte Gerät beim Kurzcode nicht an. - Die Datenbank lässt jeden Wert einer Kopplung nur einmal setzen. Commitment, Nonce, Kurzcode und Ablauf
ändern sich nie, und der gepackte AK darf erst nach
old_pubin die Zeile.
5.3 Neue Mitglieder und Mitautoren
Der Dienst verschickt die Einladungsmails und entscheidet, wer Mitglied ist. Er könnte deshalb eine Einladung selbst annehmen. Darum gilt: Den Familienschlüssel bekommt eine neue Person nur, wenn ein Mensch es bestätigt.
- Die eingeladene Person nimmt an, ihr Gerät veröffentlicht ihren AK (§5.1).
- Auf den Geräten der Familie erscheint die Karte „Beitrittsanfrage von …“. Bestätigen dürfen der Eigentümer und Mitglieder, nicht Familienfreunde (viewer). Bei gemeinsamen Geschichten bestätigt die Autorin bzw. der Autor. Die Datenbank erzwingt diesen Kreis; nur diese Personen bekommen überhaupt die Liste der Wartenden.
- Wer bestätigt, kann vorher den Sicherheitscode vergleichen (§5.5). Das Gerät packt dann alle Versionen, die es hält, für den neuen AK ein, jeweils signiert. Neue Mitglieder sehen die ganze Familiengeschichte.
- Von der Liste der Wartenden verschwindet die neue Person erst, wenn ihr Gerät die Version quittiert hat oder der Ersteller der Version ihr einen Umschlag eingelegt hat. Ein Umschlag irgendeines anderen Mitglieds genügt dafür nicht mehr.
Die Einladung trägt die Signatur der einladenden Person
(Ed25519(AK, enc("fm/invite/v1", Bereich, Bereichs-id, invitation_id, E-Mail, Rolle, Ablauf))). Die Karte
zeigt deshalb „eingeladen von …“, wenn die Signatur von jemandem stammt, dem das Gerät vertraut. Eine
automatische Freigabe folgt daraus nicht mehr (bis 12.10.2026 war das so), weil die Signatur die
eingeladene Person und ihren Schlüssel nicht bindet.
Externe Mitautoren einer gemeinsamen Geschichte bekommen nur SK, nie den Familienschlüssel. Das erzwingt
die Datenbank.
5.4 Vertrauensspeicher
Jedes Gerät führt je Person einen Eintrag: Identitäts-id, öffentliche Schlüssel, Fingerabdruck, zuerst
gesehen, bestätigt ja/nein, eine geparkte Änderung und frühere Fingerabdrücke. Der Speicher liegt mit dem
DSK gepackt (AAD = enc("fm/trust/v1", user_id)).
- Herkunft je Eintrag: bestätigt, verbürgt, beim ersten Kontakt (TOFU) oder Altbestand. Einträge von vor dem 18.10.2026 sind Altbestand und zählen wie bestätigt.
- Ein AK wird beim ersten Sehen übernommen, nachdem die Selbstsignatur geprüft ist (Trust On First Use), aber nur auf einem Gerät, dessen Kontoschlüssel hier als erster entstand und das den Bereich noch nie gehalten hat. Gekoppelte, per Wiederherstellungscode oder Passkey übernommene und neu angefangene Geräte zeigen für unbekannte Absender Karten. Ein TOFU-Eintrag öffnet Umschläge, gibt aber nie still frei und bürgt nicht (§5.3). Zeilen-Unterzeichner, die das Gerät noch nicht kennt, hält es mit ihrem ersten Schlüssel fest; ein zweiter Schlüssel derselben Person gilt dann nicht.
- Stille Weitergabe nach einer Rotation: Eine neue Version gibt das Gerät nur
an Personen still weiter, die im selben Bereich die vorige Version nachweislich hielten (ein
gültig signierter Umschlag eines vertrauten Absenders für
current − 1,e2e_prior_envelopes). Sonst braucht es eine Bürgschaft für genau diesen Bereich und diese Version,Ed25519(AK, enc("fm/vouch/v2", Bereich, Bereichs-id, Version, user_id, identity_id, Fingerabdruck))(key_scope_vouches), oder eine Karte. Das gilt auch beim Eintritt in eine zweite Familie und bei der Rückkehr nach einem Austritt. - Versionsgedächtnis: Je Bereich merkt sich das Gerät die höchste gesehene
current_versionund je Version den Fingerabdruck des Schlüssels,SHA-256(enc("fm/key-commit/v1", Bereich, Bereichs-id, Version, Schlüssel)). Istcurrentniedriger als gesehen, schreibt das Gerät nicht. Ein zweiter, anderer Schlüssel für eine gesehene Version wird abgelehnt. Versionen, die nur ein früherer Schlüssel einer Person öffnet, dienen nur zum Lesen. - Ändert sich der AK einer bekannten Person, übernimmt das Gerät ihn nicht still. Er wird geparkt, und die Person erscheint mit „… braucht wieder Zugriff“, bis jemand bestätigt (§6.2).
- Kein Weg zurück: Ein Schlüssel, dessen Fingerabdruck unter den früheren einer Person steht, wird nie wieder übernommen – weder als Änderung noch über eine (ältere) Bürgschaft noch beim Abgleich mit einem eigenen Gerät; dort gewinnt die Seite, die weitergezogen ist. Sonst öffnete ein Gerät, das einmal für den alten Schlüssel gebürgt hatte, nach „Neu anfangen“ wieder Umschläge des alten Schlüssels – auch eingeschleuste aktuelle. Ein Gerät, das schon zurückgefallen war, holt den zuletzt übernommenen Schlüssel beim nächsten Laden zurück (nur einen Schlüssel des Servers mit gültiger Selbstsignatur und genau diesem Fingerabdruck).
- Eine Bestätigung gilt für die ganze Familie. Wer bestätigt, legt eine signierte Aussage ab:
Ed25519(AK, enc("fm/vouch/v1", user_id, identity_id, Fingerabdruck)). Andere Geräte übernehmen den Schlüssel, wenn sie der bestätigenden Person selbst vertrauen (deren aktueller Schlüssel) und die Signatur genau zu dieser Identität und diesem Fingerabdruck passt; nie von der Person selbst. Ablegen dürfen dieselben Personen, die freigeben dürfen; das erzwingt die Datenbank. Seit dem 18.10.2026 zählt eine solche Bestätigung nur, wenn die bestätigende Person die aktuelle Version des Bereichs hält. So fragt nicht jedes Gerät erneut, und kein Gerät verwirft später still die Umschläge einer bestätigten Person. - Abgleich zwischen eigenen Geräten über
trust_store_sync. Der Stand liegt unter einem eigenen Zufallsschlüssel, der per HPKE an den eigenen AK gepackt ist (info = enc("fm/trust-sync-key/v1", user_id, identity_id)). Jeder Stand trägt eine Versionsnummer, die nur steigt und Teil der AAD ist (enc("fm/trust-sync/v3", user_id, identity_id, Version)), und ist mit dem eigenen AK signiert:Ed25519(AK, enc("fm/trust-sync-sig/v1", user_id, identity_id, Version, Speicher)). Ein Gerät übernimmt nur einen Stand, den der eigene aktive AK signiert hat (einen unsignierten Stand im alten Format v2 nie), und keinen, der älter ist als der zuletzt gesehene. Lesen, Zusammenführen und Schreiben laufen auf dem Gerät unter einer Sperre je Person (Web Lock). Ein übernommenes Gerät meldet sich erst nach dem Abgleich als bereit. Geschrieben wird nur, wenn die Version noch die gelesene ist; sonst wird neu gelesen und zusammengeführt. Einen Stand des eigenen aktuellen Schlüssels, der nicht aufgeht, überschreibt das Gerät nicht. Ersetzt wird nur ein Stand eines früheren Kontoschlüssels.
5.5 Sicherheitscode (optional)
Wie bei Signal hat jede Person eine Hälfte aus 30 Ziffern:
Hälfte = sechs Blöcke aus je 5 Byte von SHA-512(enc("fm/safety/v1", user_id, ed25519_pub)), jeweils mod 100 000. Der Code eines Paares sind beide Hälften sortiert hintereinander, also 60 Ziffern in
Blöcken zu fünf. Die eigene Hälfte bleibt gleich, egal mit wem man vergleicht, und ändert sich nur mit
dem Kontoschlüssel. Der X25519-Teil ist über die Selbstsignatur gebunden. Wer vergleicht, setzt
„bestätigt“ im Vertrauensspeicher. Das gilt nur, solange der angezeigte Fingerabdruck noch aktuell ist.
Für den Alltag ist der Vergleich nicht nötig, gegen das Restrisiko aus §10.3 hilft er.
Die Anzeige beschriftet die beiden Hälften („Deine Hälfte“ / „Hälfte von …“), ohne die Reihenfolge zu ändern: Beide Geräte zeigen dieselben Ziffern, und bei einer Abweichung ist klar, wessen Schlüssel betroffen ist. Zu jeder Person stehen die gemeinsamen Familien und das Datum, seit dem die eigenen Geräte sie kennen (aus dem Vertrauensspeicher), damit gleichnamige Personen auseinanderzuhalten sind. Die Liste zeigt auch Familienmitglieder, deren Schlüssel das Gerät noch nicht übernommen hat, mit dem Schlüssel, den der Server gerade nennt (Selbstsignatur geprüft). Das ist nur Anzeige, kein Vertrauen und kein „bestätigt“: So kann ein neues Mitglied den Code schon vor dem ersten Umschlag vergleichen, also genau beim ersten Kontakt, gegen den §10.3 den Vergleich empfiehlt.
6. Wiederherstellung
6.1 Reihenfolge, wie die App sie anbietet
- Ein anderes eigenes Gerät (QR oder Kurzcode) → 2. Passkey → 3. Wiederherstellungscode → 4. Familie.
6.2 Rettung durch die Familie
Für Menschen, die alles verloren haben:
- Die Person meldet sich wie gewohnt an und wählt „Neu anfangen“.
- Das geht nur nach frischer Anmeldung: Die Sitzung des Tokens ist höchstens 15 Minuten alt
(
e2e_fresh_login, gemessen anauth.sessions.created_at; Auffrischen legt keine neue Sitzung an). Sonst bittet die App, sich neu anzumelden. - Ihr Gerät erzeugt einen neuen AK und meldet ihn über den Server an (
pending_identity_keys). Die Person bekommt eine Mail („Warst du das?“) mit einem Link zum Abbrechen. Kann die Mail nicht verschickt werden, gilt der Antrag nicht. Der Link trägt sein Einmal-Token im Fragment, und abgebrochen wird erst per Knopf auf der Seite, damit ein Mailscanner nichts auslöst. Eigene Geräte, die noch den alten AK haben, zeigen eine Warnung und können ebenfalls abbrechen, aber nur mit einer Signatur des aktuellen AK über den wartenden Schlüssel (fm/start-over-cancel/v1), nicht mehr mit der bloßen Sitzung. Solange ein Antrag wartet, lehnt der Server einen zweiten ab, statt ihn zu ersetzen. Mail, wartendes Gerät und die eigenen Geräte zeigen denselben Code der Anfrage: zehn Ziffern aus der Sicherheitscode-Hälfte des neuen Schlüssels (§5.5). Daran erkennt die Person, ob der wartende Antrag ihr eigener ist (Ergebnis der internen Prüfung). - Nach 24 Stunden ersetzt der neue AK den alten (Datenbankjob alle 10 Minuten). Abkürzen lässt sich die Wartezeit nicht. Bis dahin bleibt der alte gültig. Der alte wird als ersetzt markiert, und die Datenbank markiert alle Bereiche der Person zur Rotation (§7).
- Die Familie sieht: „… braucht wieder Zugriff“, mit der Bitte, sich kurz bei der Person zu melden, und optional dem Sicherheitscode.
- Ein Eigentümer oder Mitglied bestätigt (Kreis wie §5.3). Dessen Gerät packt alle Familienversionen und die Schlüssel der gemeinsamen Geschichten, in denen beide sind, für den neuen AK ein.
Ein neuer Mitgliedsschlüssel und ein geänderter Schlüssel einer bekannten Person brauchen beide einen Menschen, der „ja“ sagt. Genau dort würde ein Angreifer (A3, A9) ansetzen.
Letztes Mitglied: Ist niemand mehr da, der bestätigen kann, und gibt es weder Gerät noch Passkey noch Wiederherstellungscode, sind die Inhalte verloren. Deshalb ist der Wiederherstellungscode für Eigentümer Pflicht.
6.3 Wiederherstellungscode
- 23 Zufallszeichen (115 Bit) plus ein Prüfzeichen, angezeigt als 24 Zeichen Crockford-Base32 in sechs
Vierergruppen. Das Prüfzeichen ist
Σ wert_i · (2i + 1) mod 32und erkennt jeden einzelnen Tippfehler, bevor Argon2id läuft. Die Eingabe verzeiht Kleinschreibung, Leerzeichen, Bindestriche, O/0 und I/L/1. - Dazu ein druckbares A4-Blatt mit Name, Datum, Code und Anleitung.
- Zur Bestätigung tippt die Person zwei zufällige Gruppen ab. Gespeichert wird erst nach dieser Bestätigung. Wer mittendrin aufgibt, behält den alten Code.
wrap_key = Argon2id(Code, Salz)→ packt den AK →ak_backups(kind='recovery'). Den ersten Code legt das Gerät selbst an.- Jede Sicherungszeile (Wiederherstellungscode und Passkey) trägt eine Signatur des AK:
Ed25519(AK, enc("fm/backup/v1", user_id, kind, credential_id, identity_id, SHA-256(wrapped_ak))). Die Datenbank nimmt neue Zeilen aus dem Browser nur mit Signatur an; die Geräte zählen und bieten nur gültig signierte Zeilen an (ältere ohne Signatur zählen weiter). Eine Müllzeile, die jemand nur mit der Sitzung anlegt, lässt so weder die Erinnerung an den Wiederherstellungscode verschwinden noch erscheint sie beim Entsperren (Ergebnis der internen Prüfung). - Einen vorhandenen ersetzen geht nur über den Server: nach frischer Anmeldung (§6.2), danach bekommt die Person eine Mail, dass ihr Wiederherstellungscode erneuert wurde. Kann die Mail nicht verschickt werden, gilt der alte Code weiter. Eine Wartezeit gibt es hier nicht, denn ohne den AK lässt sich kein brauchbarer Code erzeugen, nur der echte unbrauchbar machen, und das zeigt die Mail. Der alte Code ist danach ungültig. Löschen können Nutzer Sicherungen nicht.
- Die Argon2-Parameter liest das Gerät aus der Zeile. Es akzeptiert nur Werte zwischen den eigenen Standardwerten und höchstens 256 MiB, 10 Durchläufen und Parallelität 4. So kann der Dienst sie weder abschwächen noch zum Aufhängen des Geräts aufblähen.
6.4 Passkey
- WebAuthn mit
userVerification: required,residentKey: requiredund der Erweiterung PRF. Jeder Passkey hat ein eigenes 32-Byte-Salz und eine eigene Zeile inak_backups. Beim Entsperren wirdevalByCredentialgenutzt. wrap_key = HKDF-SHA256(PRF-Ausgabe, salt = "", info = enc("fm/passkey/v1", credential_id)).- Der Passkey ist nur Entsperrweg, nicht Anmeldeweg. Die Anmeldung bleibt bei Supabase Auth.
- Höchstens zehn Passkeys je Person; jede Zeile trägt die Signatur aus §6.3.
- Passkey entfernen: in den Sicherheitseinstellungen, nur nach frischer Anmeldung (§6.2) und immer mit Info-Mail; kann die Mail nicht verschickt werden, bleibt der Passkey. Das schließt den Weg, wenn das Konto beim Passkey-Anbieter (Google, Apple) nicht mehr sicher ist. Browser selbst dürfen Sicherungen weiterhin nicht löschen.
- Vor dem Anlegen fragt das Gerät per
getClientCapabilitiesnach PRF, wo der Browser das kennt, damit möglichst kein unbrauchbarer Passkey entsteht. Liefert ein neuer Passkey trotzdem kein PRF, wird er nicht als Entsperrweg gespeichert. - In der Android-App gibt es keine Passkeys. Ihr WebView läuft unter
https://localhost; ein Passkey wäre an diesen Namen gebunden und sonst nirgends brauchbar. Passkeys entstehen und wirken im Browser. Die App entsperrt über den Keystore (§3); ein neues App-Gerät kommt per Kopplung (§5.2) oder Wiederherstellungscode (§6.3) zu seinem Schlüssel.
7. Rotation (Austritt und Entfernen)
- Jeder Weg hinaus markiert per Trigger, nicht im Code der Routen: Entfernen, Verlassen und Konto löschen
(DELETE auf
family_members) ebenso wie das Entziehen der Mitautorschaft. Gesetzt wirdkey_scope_state.rotation_pendingfür die Familie, ihre gemeinsamen Geschichten und die Profilbilder der Beteiligten, undrotation_epochzählt hoch. Ersetzt jemand seinen AK (§6.2), wird alles markiert, was die Person hält. Kein Schlüssel entsteht auf dem Server. - Das nächste entsperrte Gerät eines Eigentümers (Familie) bzw. der Autorin (gemeinsame Geschichte;
hat sie die Familie verlassen oder ist sie nur noch Familienfreundin, der Eigentümer)
erzeugt
FMK_{v+1}bzw.SK_{v+1}, packt den Schlüssel für alle verbleibenden Personen ein und signiert die Umschläge. Ein Web Lock je Bereich verhindert eine doppelte Rotation aus zwei Tabs. Rotieren darf nur ein Gerät, das die aktuelle Version hält. Dass nur Eigentümer bzw. Autorin rotieren, erzwingt die Datenbank. Eine ausgeschiedene Autorin bleibt als Autorin genannt, verwaltet den Schlüssel aber nicht mehr — sonst würde ihre Geschichte nie rotiert (Ergebnis der internen Prüfung). e2e_complete_key_version(scope, id, v+1)prüft, dass jede bereits aufgenommene Person (ihr aktueller AK hat in diesem Bereich eine Version quittiert oder einen Umschlag vom Ersteller einer Version; der Umschlag eines beliebigen anderen Mitglieds zählt nicht) auch einen für v+1 hat. Dann setzt die Funktioncurrentund löscht die Markierung, aber nur, wenn seitbeginniemand gegangen ist (key_versions.epoch = rotation_epoch). Zwei Eigentümer auf zwei Geräten bekommen v+1 und v+2, die höhere wird aktuell.- Personen, deren Schlüssel noch auf eine Bestätigung wartet (neu oder ersetzt), halten die Rotation nicht auf. Sie bekommen alle Versionen mit der Bestätigung.
- Bis zum Abschluss hochgeladene Inhalte laufen noch unter v. Das Fenster reicht bis zum Öffnen der App durch einen Eigentümer. Alte Inhalte werden nicht neu verschlüsselt.
- Tag-Indizes werden neu berechnet (§4.5).
- Profilbilder (
avatar) haben keinen Abschluss. Das eigene Gerät legt bei Markierung eine neue Version an, das sichtbare Bild bleibt unter seiner Version, das nächste Hochladen nutzt die neue. - Wird eine Familie, Geschichte oder ein Profil gelöscht, räumen Trigger Versionen, Stand und Umschläge weg.
Auf dem Server rotiert nichts mehr: Die Rotation des früheren Verfahrens in leave, remove-member und
removeCollaborator ist am 17.10.2026 entfallen.
8. Was der Server noch tut
- Speichert Chiffrat, Umschläge, öffentliche Schlüssel und gepackte Sicherungen.
- Setzt Zugriffsregeln (RLS). RLS bleibt die erste Verteidigungslinie, die Verschlüsselung die zweite.
- Prüft Größen, Kontingente, Rollen und Einladungsfristen.
- Verschickt Mails ohne Inhalt: Einladungen nennen die einladende Person, aber keinen Familiennamen und keinen Titel; die Tagesmail enthält nur Zahlen.
- Die privaten Bildschirme sind Client-Komponenten. Server-Routen nehmen Chiffrat an und prüfen nur Größe und Berechtigung.
- Fehlerberichte (GlitchTip): Vor dem Senden entfernt die App Request-Bodies, Header, Cookies,
Query-Parameter, Nutzerangaben und Konsolenspuren. Freitext wird geschwärzt (Adressen, URLs,
Link-Fragmente, Datenbankzeilen, Zitate). Zusatzfelder und
tagslaufen über je eine Positivliste, ihre Texte werden ebenso geschwärzt. Edge Functions und der Render-Dienst senden keine Fehlerberichte.
9. Ausnahmen mit Freigabe
Jede Ausnahme hat einen eigenen, beschrifteten Moment, in dem die Person sieht, was wohin geht. Kein Klartext verlässt das Gerät nebenbei.
9.1 Geteilte Download-Links
- Das Gerät erzeugt LK und verschlüsselt die Auswahl neu. Das Manifest mit Namen, Bildunterschriften,
Daten und Geschichtstext ist ein versiegelter, gepolsterter Block. Jede Datei ist ein FMV2-Objekt. Die
Schlüssel kommen aus
HKDF-SHA256(LK, salt = "fm/share/v1", info = enc("manifest", grant_id))bzw.enc("part", grant_id, Dateiname). Hochgeladen wird unter<family_id>/links/<grant_id>/. - Der Link ist
https://family-memories.eu/<sprache>/d/<token>#k=<LK base64url>. Das Fragment erreicht nie einen Server. download-grantprüft Ablauf und Abrufzahl und liefert für 2 Stunden gültige, signierte Adressen der Chiffrat-Dateien. Die Function hält für diese Links keinen Schlüssel.- Ein widerrufener Link wird sofort gelöscht, ein abgelaufener oder aufgebrauchter nach 3 Stunden, ein nie fertig hochgeladener nach 24 Stunden. Ein Aufräumlauf geht stündlich. Fotos, die später als „Nicht zum Weitergeben“ markiert oder gelöscht werden, fallen per Trigger aus bestehenden Links heraus.
Die teilende Person sieht: „Wer diesen Link hat, kann die ausgewählten Inhalte öffnen. Teile ihn nur mit Menschen, denen du vertraust.“
9.2 Übersetzung (DeepL)
- Knopf „Mit DeepL übersetzen“. Beim ersten Mal erscheint ein Dialog, dass der Text an DeepL (Köln) geht und dort nicht gespeichert wird. „Nicht mehr fragen“ gilt je Person auf diesem Gerät und wird dort gespeichert, nicht beim Dienst: Eine Zustimmung, die der Dienst selbst setzen könnte, würde den Dialog überspringen und das Motto bei jedem Besuch von allein übersetzen. Auf einem neuen Gerät kommt der Dialog deshalb noch einmal.
- Die Route
/api/translateleitet zustandslos weiter: kein Log des Inhalts, kein Speichern. Bei Fehlern wird nur der Status protokolliert. Der DeepL-Schlüssel bleibt auf dem Server, deshalb geht es nicht direkt vom Browser. Seit dem 17.10.2026 kennt die Route nur noch diese Durchreiche; die früheren Arten, die den Quelltext selbst aus der Datenbank lasen und im Klartext zwischenspeicherten, sind entfernt. - Die Übersetzung wird im Browser unter FMK verschlüsselt in
content_translationsabgelegt — als eigener Eintrag der Person, die übersetzt hat (§4.3). Die Datenbank nimmt dort nur verschlüsselte Zeilen an. - In diesem Moment sieht unser Server den Text auf dem Durchweg. Das ist die Ausnahme, der die Person zugestimmt hat.
- Mitgeschickt wird alles, was gerade übersetzt wird: bei einer Geschichte auch Kapitel, Kommentare und Bildunterschriften anderer Mitglieder. Die Zustimmung gilt je lesender Person; wer den Text schrieb, wird nicht gefragt. Der Dialog sagt das.
- Mit „Nicht mehr fragen“ übersetzt die App ohne Klick: das Familienmotto bei jedem Öffnen der Familienseite und die Texte eines Fotobuchs, dessen Sprache von der App-Sprache abweicht. Auch das steht im Dialog.
9.3 Fotobuch
- Freigabedialog vor der Bestellung: Für den Druck geht das Buch als lesbare Datei an unseren Druck-Dienst und an Gelato — mit Texten, Kommentaren samt Namen, Karten mit den Fotoorten und den Fotos.
- Der Browser schickt Inhalte und Bilder direkt an den Render-Dienst, nicht über die Website. Dafür gibt es ein 15 Minuten gültiges, an die Bestellung gebundenes Token mit einem Hash der Auswahl, den der Render-Dienst nachrechnet. Gespeichert wird bei der Bestellung nur die Auswahl (ids und Optionen).
- Der Render-Dienst hält Aufträge nur im Arbeitsspeicher und verwirft sie nach dem Rendern bzw. nach 15 Minuten.
- Das Druck-PDF liegt in R2 und wird 30 Tage nach Zustellung gelöscht (ohne Zustellmeldung 40 Tage nach Versand bzw. 50 Tage nach Einreichung, bei Abbruch sofort). Geprüft wird alle 10 Minuten zusammen mit dem Gelato-Abgleich.
- Gelato holt die Datei ab und löscht sie nach eigener Datenschutzerklärung, sobald sie nicht mehr benötigt wird; eine feste Frist nennt Gelato nicht. Der Freigabedialog sagt „bei uns 30 Tage“.
9.4 Feedback und Support mit Bildschirmfoto oder Anhang
Hinweis vor der Dateiauswahl — im Feedback, im Support-Formular und bei einer Antwort im eigenen Ticket: Das Bild zeigt, was gerade auf dem Bildschirm ist, auch Fotos der Familie, geht unverschlüsselt an unser Team und wird nach 90 Tagen gelöscht. Abwählbar.
Hochgeladen wird schon beim Auswählen, in eigene Speicherbereiche (feedback/<user>/,
ticket-attachments/<user>/). Feedback-Bildschirmfotos gehen zusätzlich als Anhang der Team-Mail über
unseren Mailversand (Mailjet); Ticket-Anhänge sieht das Team nur im Admin-Bereich. Fristen, durchgesetzt von
einem stündlichen Job: Bildschirmfotos und Anhänge 90 Tage nach der letzten Aktivität, Texte von
Feedback und Tickets 12 Monate; eine Datei, auf die nichts mehr verweist (Formular abgebrochen, Konto
gelöscht), nach 24 Stunden. Mit dem Konto gehen Feedback, Tickets, Chats und alle Dateien sofort.
9.5 Offline
Der Offline-Speicher auf dem eigenen Gerät (Texte und Orte im lokalen Speicher, Medien im Cache des Browsers) bleibt unverschlüsselt. Der Schutz auf dem Gerät ist Sache der Gerätesperre (§1.2).
Die Kopien gehören dem Konto, das sie geladen hat; /offline zeigt sie nur, solange dieses Konto als
letztes auf dem Gerät angemeldet war, und lädt ein anderes Konto selbst etwas herunter, gehen die alten
Kopien vorher. Beim Abmelden fragt die App, ob die Kopien bleiben sollen — vorausgewählt ist „Löschen“, denn
behaltene Kopien kann jede Person mit Zugang zum Gerät ohne Anmeldung ansehen. Mit dem Konto gehen sie immer.
In der Android-App liest der eigene Auswahldialog das Original eines Fotos oder Videos, damit der Aufnahmeort erhalten bleibt, und legt dafür eine unverschlüsselte Kopie samt Ort im App-Speicher ab. Die App löscht sie, sobald sie die Datei gelesen hat; was ein Absturz übrig lässt, räumen der nächste Start und das Abmelden.
9.6 Beispielfamilie
Die erfundene Beispielfamilie (/beispielfamilie) liegt unverschlüsselt und öffentlich. Die
Datenbank verhindert, dass sie je E2E wird. So braucht der Server nie einen Familienschlüssel, auch
nicht für die Demo.
9.7 Support
Das Team sieht keine Familieninhalte und kann deshalb auch nicht „nachsehen“.
- KI-Chat: Mistral (EU) sieht nur, was jemand in den Chat tippt, und hält es nach eigener Regel kurz für den Missbrauchsschutz vor. Ein fester Hinweis über dem Eingabefeld sagt das, nennt die 90 Tage Speicherung und bittet, keine Familieninhalte einzufügen. Das Team liest einen Verlauf nur, wenn die Person ihn übergibt; dann geht er als Text ins Ticket und per Mail an das Team. Ausnahme sind Besucher der Beispielfamilie, deren Verläufe das Team liest und die nach 24 Stunden mit dem Gastkonto gehen. Von den übrigen sieht das Team nur Zahlen (Anzahl, Länge, ob übergeben), keinen Text. Verläufe werden 90 Tage nach der letzten Nachricht gelöscht.
- Tickets: Texte und Verlauf bleiben 12 Monate nach der letzten Nachricht, Anhänge 90 Tage (§9.4). Schreibt jemand nicht auf Deutsch, kann das Team ein Ticket auf Knopfdruck mit DeepL übersetzen lassen; Formular und Ticketverlauf sagen das. Automatisch übersetzt wird nichts.
- Kontaktformular und Mails ans Postfach (bei Hostinger): werden nach 12 Monaten gelöscht.
- Server-Logs enthalten bei Fehlern nur Art, Code und eine geschwärzte Meldung — keine Adressen, keine Zeileninhalte, keine Antworttexte von Drittanbietern.
9.8 Karte
Die Karte entschlüsselt die Fotoorte im Browser und lädt dazu Kartenkacheln aus unserem Archiv (Cloudflare R2). Die Fotoorte selbst verlassen das Gerät nicht. Abgerufen werden Kacheln höchstens bis Zoomstufe 14, also etwa 1,5 km je Kachel; wer näher heranzoomt, sieht diese Kacheln vergrößert. So verraten die Abrufe die Gegend, aber keine einzelne Straße. Was sie noch verraten können, steht in §10.4.
10. Grenzen des Modells
10.1 Code-Auslieferung im Web (A4)
Wer die Website ausliefert, könnte veränderten Code ausliefern, der Schlüssel abgreift. Das gilt für jede
Ende-zu-Ende-Web-App. Gegen eingeschleusten fremden Code (XSS) schützt eine Content-Security-Policy: Skripte
laufen nur, wenn sie von unserer Adresse kommen oder den Nonce der jeweiligen Anfrage tragen (in der App:
die Prüfsumme des jeweiligen Inline-Skripts); 'unsafe-inline' gibt es für Skripte nicht — auch nicht als
Rückfall: Eine Seite, die ohne Nonce ausgeliefert würde, bekommt gar keine Inline-Skripte, und jede Seite unter
einem Sprachpfad läuft durch die Middleware, die den Nonce setzt (Ergebnis der internen Prüfung). Die
Richtlinie verhindert die Ausführung fremden Codes, nicht jeden Abfluss: Verbindungen zum eigenen
Supabase-Projekt und Navigationen bleiben erlaubt, weil die App sie braucht. Für Styles
bleibt es erlaubt, weil die Komponenten style-Attribute nutzen; eingeschleuste Styles führen keinen Code
aus. Dahinter stehen seit 24.10.2026 Trusted Types (require-trusted-types-for 'script'), im Web und in der
App, wo der Browser sie kennt (Chrome, Edge und die WebView der Android-App): An den Stellen, an denen aus
Text Code oder HTML wird (innerHTML, Skript-Adressen, Worker), nimmt der Browser keine bloßen
Zeichenketten mehr an. Erlaubt sind nur vier benannte Richtlinien: die beiden von Next.js, fm allein für
die Anmeldung unseres Service Workers und default, die genau durchlässt, was Bibliotheken nicht anders
lösen — den Karten-Worker aus unserem Bündel, Worker aus blob:-Adressen der eigenen Herkunft, den festen
Quellenhinweis der Karte und Reacts leeren Skript-Platzhalter. Alles andere blockiert der Browser. So wird
auch ein Fehler, der Nutzertext an eine solche Stelle bringt, kein Code (live geprüft 24.10.2026 im Web,
öffentlich und angemeldet: keine Meldung). Die Startskripte der Website (Laufzeit, Hauptbündel, Polyfills) tragen außerdem ihre Prüfsumme
(Subresource Integrity); die übrigen Seiten-Chunks nicht, weil Next.js SRI im App Router nur für diese
setzt (live geprüft 16.10.2026: 6 von 27 Skripten einer Seite). Die Richtlinie steht
im <meta>-Tag, weil unser Hoster den CSP-Header durch einen eigenen ersetzt; frame-ancestors wirkt dort
nicht, Einbetten verhindert X-Frame-Options: DENY. Gegen den Betreiber selbst helfen diese Maßnahmen
nicht. Später: reproduzierbare Builds und veröffentlichte Hashes.
10.2 Android-App
Die App bringt ihren Code mit (webDir), ist mit unserem Schlüssel signiert und bekommt Code nur über
App-Updates. Den Schutz liefert die Signatur: Bevor die App ein Update an Android übergibt, prüft sie selbst,
dass die Datei aus unserem Release-Speicher kommt, mit demselben Schlüssel signiert und neuer als sie selbst
ist; Android prüft die Signatur beim Installieren noch einmal. Die SHA-256 aus der Update-Beschreibung
(latest.json) liegt am selben Ort wie die Datei und belegt deshalb nur, dass der Download vollständig ist;
ohne passenden Wert gibt es kein Update. Für die erste Installation, bei der es noch nichts zu vergleichen
gibt, veröffentlichen wir den Fingerabdruck (SHA-256) des Signaturzertifikats auf der Download-Seite und hier:
6F:F8:11:8E:FA:0A:03:5C:9F:1A:AE:2F:A8:30:B6:8C:80:C2:5D:5B:9A:39:83:F1:FA:B2:EE:B0:CB:29:2E:5D. Die
Content-Security-Policy der App erlaubt keine Skripte von der Website.
Für die Sprach-Transkription liegt die ONNX-Laufzeit (Programmcode) in der App. Das Modell (rund 250 MB, Daten) lädt die App beim ersten Gebrauch von unserem Server, führt aber nur Dateien aus, deren SHA-256 der fest in die App eingebauten Liste entspricht; andere Dateien lädt sie gar nicht erst. Nur geprüfte Dateien kommen in ihren Zwischenspeicher. Apps bis 1.0.8 luden die Laufzeit noch von der Website; die gibt sie ihnen seit Version 1.0.17 nicht mehr frei. Sie verlieren damit nur die Transkription.
Der DSK liegt im Android Keystore und ist nur bei entsperrtem Gerät nutzbar (setUnlockedDeviceRequired,
ab Android 9). Nur wenn der Keystore diese Bindung bei entsperrtem Gerät auch im zweiten Versuch ablehnt,
und auf Android 7 und 8, die sie nicht kennen, entsteht ein Ersatzschlüssel ohne Bindung — sonst ließe sich
E2E auf dem Gerät gar nicht einrichten. Hinter der Gerätesperre entsteht er nie. Bindung und Ersatz haben
getrennte Namen im Keystore: Die App zeigt unter „Geräte und Verschlüsselung“, welchen das Gerät nutzt, und
versucht bei jedem Start erneut, den gebundenen zu bekommen. Die App-Daten sind von Android-Sicherungen und
von der Übertragung auf ein neues Gerät ausgenommen.
Solange ein Wiederherstellungscode oder Kopplungscode zu sehen ist oder eingetippt wird, sperrt die App Bildschirmfotos und das Vorschaubild unter „Letzte Apps“; viele Handys laden Bildschirmfotos von selbst in eine Cloud. Im Web geht das nicht. Ein zweites Gerät, das den Bildschirm abfotografiert, hält das nicht auf. Berührungen, die durch ein darübergelegtes Fenster einer anderen App gehen, nimmt die App nicht an.
10.3 Der Dienst als Mitgliederverwaltung und erster Kontakt (A3)
Der Dienst entscheidet weiterhin, wer Mitglied ist, und zeigt Namen an. Menschliche Bestätigung (§5.3, §6.2) und der Vertrauensspeicher verhindern, dass er still eigene Schlüssel einschleust. Es bleibt:
- Bei einer neuen Person könnte der Dienst beim allerersten Mal einen eigenen Schlüssel unter ihrem Namen vorlegen. Wer bestätigt, sieht den Namen, den der Dienst anzeigt.
- Ein neues Mitglied übernimmt den ersten gültig signierten Familienschlüssel, ohne die Absender schon zu kennen (§4.2).
Dagegen hilft der Vergleich des Sicherheitscodes (§5.5). Später: Schlüsseltransparenz, also ein öffentliches, nur anhängbares Protokoll aller Kontoschlüssel.
10.4 Metadaten
Sichtbar bleibt, wer mit wem in einer Familie ist, mit welcher Rolle, Profilnamen und E-Mail-Adressen. Ebenso sichtbar sind Zeitpunkte von Uploads und Änderungen, Anzahl und Größe der Dateien sowie der Medientyp (Foto oder Video). Der Kopf eines Videos nennt MIME-Typ und genaue Länge. Sichtbar sind auch, welche Zeilen miteinander verknüpft sind, die Zugriffsmuster und die groben Kartenabrufe (§9.8).
Dazu im Einzelnen:
- Profil: Zeitzone und Sprache (die Zeitzone verrät die Gegend), der Tag der letzten Nutzung
(
last_seen_on) und ob jemand der DeepL-Übersetzung zugestimmt hat. - Übersetzungen: welches Feld welcher Zeile in welche Sprache übersetzt wurde, und wann
(
content_translations; der Text selbst ist versiegelt). - Download-Links: was wann geteilt wurde, wie oft abgerufen, welche Teile ausgeschlossen und welche Fotos enthalten sind.
- Fotobuch-Bestellungen: ausgewählte Inhalte (ids), Seitenzahl, Produkt, Design, Buchsprache, Umschlagvariante, Lieferadresse und Name — die beiden letzten gehen auch an Gelato.
- Inhalte: ob ein Foto „nicht zum Weitergeben“ markiert ist, gelöschte Kommentare und ihre Antwortketten, Benachrichtigungen (wer wessen Inhalt kommentiert hat), ob eine Geschichte eine Aufnahme hat (über die Größe auch ihre ungefähre Länge) und die Reihenfolge der Kapitel.
Welche Spalten genau verschlüsselt sind und welche nicht, steht in der Tabelle in §4.3.
10.5 Familienfreunde (viewer)
Sie bekommen den Familienschlüssel wie Mitglieder. Leserecht heißt Schlüssel. Andere freigeben dürfen sie nicht (§5.3).
10.6 Schlüssel im Browser-Speicher
Im Web ist der DSK ein nicht extrahierbarer WebCrypto-Schlüssel in IndexedDB. Das schützt gegen Auslesen durch Skripte. Der Browser legt ihn aber in seinen Profildateien ab. Wer diese Dateien vom Rechner kopieren kann, hat damit in der Regel auch den Schlüssel. Wir behandeln das wie ein kompromittiertes Endgerät (§1.2). In der Android-App liegt der DSK im Keystore und verlässt ihn nicht; ein Browser-DSK, den die App von früher noch hat, wird nach dem Umzug in den Keystore gelöscht. Gelöscht heißt dabei: logisch. Die Datenbank der WebView schreibt eine Löschung als neuen Eintrag; die alten Bytes — der Browser-DSK und der damit verpackte AK — können in ihren Dateien bleiben, bis sie aufgeräumt werden, auf Flash-Speicher auch darüber hinaus. Wer sie mit Root- oder Forensik-Zugriff ausliest, kann den AK öffnen. Betroffen ist nur, wer die App schon vor dem Keystore genutzt hat. Weil die App die Speicherung der WebView nicht steuert und ein Löschen am Gerät nicht nachprüfbar wäre, behandeln wir das als Grenze des Modells.
10.7 Schlüssel im Arbeitsspeicher
Solange die App entsperrt ist, liegen der private AK und die geöffneten Familien- und Geschichtsschlüssel als Bytes im Arbeitsspeicher der Seite. Das ist für die Kurven-Bibliothek nötig (§4.1). Abgeleitete Schlüssel für Zeilen, Medien und Tags sind nicht extrahierbare WebCrypto-Schlüssel. Beim Abmelden werden alle Schlüssel-Caches geleert, in der App auch die Medienkopien des Auswahldialogs (§9.5).
10.8 Zurückspielen, Zurückhalten und Verknüpfungen
Ein Gerät erkennt eine zurückgespielte ältere Fassung nur bei Zeilen, deren neuere Fassung es schon gesehen hat (§4.3). Der Revisionsstand gilt je Gerät, nicht je Person: Ein neues Gerät oder ein geleerter Browser kann eine Zeile, die es zum ersten Mal sieht, nicht mit einem früheren Stand vergleichen. Daten zurückhalten oder löschen kann der Dienst immer (§1.2).
Für Verknüpfungen (§4.3) heißt das: Der Dienst kann eine Verknüpfung entfernen oder eine früher gültige, inzwischen gelöschte wieder einsetzen — ihre Signatur bleibt gültig. Das Titelbild eines Albums ist nicht gebunden, weil es beim Löschen des Fotos leer werden muss; der Dienst könnte ein anderes Foto derselben Familie als Titelbild zeigen. Und weil eine Person, die ein Gerät noch nie gesehen hat, beim ersten Kontakt übernommen wird (§10.3), könnte der Dienst unter einem solchen neuen Namen Verknüpfungen unterschieben. Texte kann er so nicht unterschieben: Dafür fehlt ihm der Familienschlüssel.
Für Medien (§4.3): Bis der Eigentümer einen Bereich als mediaBound markiert hat, öffnen Medien ohne
Bindung noch. Das Nachrüsten (§12.4) bindet, was der Dienst in diesem Moment liefert. Hat jemand mit einem
alten Schlüssel ein Objekt schon vorher getauscht, bleibt es getauscht, nun gebunden. Das Profilbild einer
Person, die die App seit dem Ausrollen nicht geöffnet hat, ist unsigniert. Es öffnet auf Geräten, die von
ihr noch kein signiertes Bild gezeigt haben.
10.9 Bestand
Texte aus der Zeit vor Format v2, deren Autorin oder Autor kein Gerät mehr hat, sind vom Eigentümer versiegelt und tragen den Vermerk „Bestand“ (§4.3). Für sie bestätigt die Signatur nur, dass der Eigentümer sie übernommen hat, nicht wer sie geschrieben hat. Der Eigentümer darf Inhalte anderer auch ändern (Signaturregel „Autorin oder Eigentümer“). Darum zeigen die Geräte unter jeder Geschichte, jedem Kapitel, Foto und Kommentar, die nicht die Autorin bzw. der Autor selbst signiert hat, „bestätigt von …“. Ein Text des Eigentümers geht so nie als Text der Autorin durch. Bei Zeilen gelöschter Konten prüft das Gerät des Eigentümers vor der Übernahme nicht, wer sie unterschrieben hatte — der Schlüssel ist mit dem Konto gelöscht. Es weiß nur, dass jemand mit dem Familienschlüssel sie geschrieben hat. Wann die Übernahme fremder Zeilen beginnt, hängt am Schalter-Zeitpunkt; den kann der Dienst vorziehen, nicht verschieben (§4.3) — er kann den „Bestand“-Vermerk also früher auslösen, aber keinen Text verfälschen.
Nach einer Eigentumsübertragung zählt die Unterschrift des früheren Eigentümers unter der Familienzeile nicht mehr. Bis das Gerät des neuen Eigentümers sie neu versiegelt hat, zeigen die Geräte der Familie Name, Motto und Farbe als „nicht geprüft“.
11. Datenbank
| Tabelle | Inhalt | Zugriff |
|---|---|---|
user_identity_keys | id, user_id, x25519_pub, ed25519_pub, self_sig, created_at, replaced_at | lesbar für Personen, die eine Familie oder gemeinsame Geschichte teilen; geschrieben nur per Funktion für sich selbst; höchstens ein aktiver Schlüssel je Person; nicht löschbar außer mit dem Konto |
ak_backups | user_id, kind (recovery/passkey), credential_id, identity_id, salt, kdf_params, wrapped_ak, sig | lesen und neu anlegen nur für sich selbst, neu nur mit Signatur, höchstens 10 Passkeys; Wiederherstellungscode ersetzen nur über den Server nach frischer Anmeldung, mit Mail (§6.3); Passkey entfernen ebenso (§6.4); sonst löschen nur mit dem Konto |
pending_identity_keys | user_id, x25519_pub, ed25519_pub, self_sig, requested_at, activates_at, cancel_token_hash | lesbar nur für sich selbst; abbrechen per Mail-Link oder mit Signatur des aktuellen AK; angelegt nur über den Server nach frischer Anmeldung, mit Mail, kein zweiter Antrag, solange einer wartet; nach 24 h aktiviert per Datenbankjob (§6.2) |
key_versions | scope, scope_id, version, created_by, created_at, epoch, commit, commit_sig, commit_identity | lesbar für Berechtigte; angelegt per e2e_begin_key_version; Prüfwert einmal vom Ersteller per e2e_set_key_commit (§4.2) |
key_scope_state | scope, scope_id, current_version, rotation_pending, rotation_epoch | lesbar für Berechtigte; current_version nur über e2e_complete_key_version |
key_envelopes | §4.2 | ein Umschlag je Absender; lesbar nur für den Empfänger; einfügen nur als Absender, wenn Absender und Empfänger berechtigt und beide Schlüssel aktiv sind; nicht änderbar, nicht löschbar (Kaskade beim Konto) |
key_receipts | scope, scope_id, version, recipient, recipient_identity, created_at | Empfangsquittung (§4.2): lesbar und anlegbar nur für sich selbst, mit dem aktiven Schlüssel und nur für eine Version mit Umschlag; nicht änderbar, nicht löschbar |
device_pairings | user_id, commitment, new_device_pub, nonce, old_device_pub, enc, ct, code, created_at, expires_at | nur für sich selbst; Ablauf 10 min; jeder Wert nur einmal, Commitment/Nonce/Code/Ablauf unveränderlich (§5.2) |
trust_store_sync | user_id, Blob, Version, identity_id, gepackter Abgleichschlüssel | nur für sich selbst; Version steigt nur (§5.4) |
key_vouches | voucher, voucher_identity, subject, subject_identity, sig | lesbar für Personen, die mit der Person einen Bereich teilen; ablegen nur im eigenen Namen, für einen aktiven Schlüssel, im Kreis aus §5.3; nicht änderbar, nicht löschbar |
Dazu die einzeilige Tabelle e2e_settings (rows_v2_since, lesbar für Angemeldete, gesetzt nur im
SQL-Editor), die Funktionen e2e_reseal_comment (Autorin oder wer umstellen darf) und
e2e_sign_timeline_link (Erstellerin, verwaist der Eigentümer; nur mit dem eigenen aktiven Schlüssel;
beide nur, solange man der Familie angehört, Links nur als owner/member), die Spalten family_invitations.inviter_sig,
story_collaborators.inviter_sig, enc/enc_key_version an den Inhaltstabellen (§4.3), sig (64 Byte) und
sig_identity an photo_tags, story_tags, album_tags und timeline_links (§4.3) und die Tabellen
der Download-Links (download_grant_items, download_link_trash). Prüf- und Hilfsfunktionen laufen als
SECURITY DEFINER mit festem search_path. e2e_may_hold gibt nur über die eigene Person Auskunft oder
über Bereiche, die man selbst hält. e2e_pending_recipients liefert nur Personen, die freigeben dürfen,
eine Liste. Sie enthält die Adresse, an die eine neue Person eingeladen wurde: Nur damit kann die
freigebende Person die Signatur des Einladenden prüfen (§5.3). Wer freigeben darf (Oberhaupt und
Mitglieder bzw. die Autorin einer gemeinsamen Geschichte), sieht deshalb diese Adresse — eine bewusste
Grenze (Ergebnis der internen Prüfung).
Jede neue Tabelle bekommt den Demo-Trigger demo_readonly_sperre und verengte Standardrechte.
family_media_keys, story_collaboration_keys und avatar_keys sind seit 17.10.2026 gelöscht.
12. Umstellung des Bestands
12.1 Ablauf, wie er gebaut ist
Abgeschlossen: Am 17.10.2026 lag in keiner Familie außer der Beispielfamilie mehr etwas auf dem alten Weg (Dateien, Ton, Familienbild, Logo, Speicherobjekte, klassische Links). Ein einziges altes Profilbild eines Kontos ohne Kontoschlüssel wurde verworfen statt umgestellt; die Person kann ein neues hochladen.
- Umschalten: Der Betrieb setzte
families.e2e_pilot(bis 17.10.2026). Ein Trigger widerrief alle klassischen Download-Links der Familie und löschte den Klartext-Übersetzungs-Cache. Ab da nahm die Datenbank keinen Klartext mehr an. - Schlüssel: Das nächste entsperrte Eigentümer-Gerät erzeugt
FMK_1und packt ihn für alle Mitglieder mit AK ein. Diesen Schlüssel kannte der Dienst nie. Mitglieder ohne bestätigten Schlüssel erscheinen als Karte (§5.3). - Neu verschlüsseln im Eigentümer-Gerät, im Hintergrund, wiederaufnehmbar, mit Fortschrittsanzeige
und einem Web Lock je Familie. Den Zustand leitet das Gerät aus den Zeilen ab: Offen ist jede Datei,
die nicht unter
….e2e.v<N>.encliegt, jede Textzeile ohneencund jedes Klartext-Tag. Je Datei: laden, mit dem alten Schlüssel öffnen, unter K_obj neu versiegeln, hochladen, Zeile bedingt auf den alten Pfad umstellen, altes Objekt löschen. Gemeinsame Geschichten stellt das Gerät der Autorin um, Profilbilder das Gerät der Person selbst. - Familienbild und Logo aus der Zeit vor der Umstellung (Klartext) versiegelt das Eigentümer-Gerät wie ein neues Bild unter FMK und löscht das alte Objekt.
12.2 Neue Familien
Neue Familien entstehen verschlüsselt. Das gründende Gerät wählt die Familien-ID, erzeugt FMK_1,
versiegelt den Familiennamen unter (family, ID, 1) und packt FMK_1 in einen Umschlag an den eigenen
Kontoschlüssel. e2e_create_family legt Familie (ohne Klartext-Namen),
Eigentümer-Mitgliedschaft, Schlüsselversion 1 und diesen Umschlag in einer Transaktion an. Der
Name erreicht den Server also nie im Klartext, und eine Familie ohne lesbaren Schlüssel kann nicht
entstehen. Gegründet wird nur mit entsperrtem Tresor; ein neues Konto bekommt seinen Kontoschlüssel dabei
automatisch (§5.1). Der alte Weg create_family lehnt ab.
12.3 Vernichtung
Ausgerollt am 17.10.2026:
family_media_keys,story_collaboration_keysundavatar_keyssind gelöscht, ebenso die Funktionen, die sie lasen, und die Einlösung klassischer Links. Die Datenbankänderung bricht ab, solange in einer Familie außer der Beispielfamilie noch etwas auf dem alten Weg liegt.- Die Functions
media-key,story-keyundavatar-keysind entfernt,download-grantkennt nur noch E2E-Links und hält keinen Schlüssel. MEDIA_KEKist aus der Edge-Umgebung und an beiden Verwahrorten gelöscht.- Die nächtliche Sicherung legt kein Schlüsselmaterial mehr ab; alle früheren
key-material-*.jsonsind gelöscht.
Läuft von selbst aus, vor Ende der Beta:
- Die Altstände im Speicher-Spiegel bei Hetzner (Karenzordner
geloescht/, 30 Tage) verschwinden bis Mitte November 2026. Ohne Schlüssel sind sie schon jetzt nicht mehr lesbar. - Die Datenbank-Sicherungen des Anbieters aus der Zeit vor der Umstellung laufen ebenso ab. Danach gibt es nirgends mehr Klartext oder auspackbare Schlüssel aus der Zeit davor.
12.4 Medien binden
Was vor dem 18.10.2026 hochgeladen wurde, hat noch kein obj (§4.3). Ein Planer auf den Geräten der Familie holt das
nach, im Hintergrund nach der Umstellung auf Format v2:
- Eigene Zeilen bindet das Gerät der Person, die sie hochgeladen bzw. geschrieben hat, sofort.
- Das Gerät des Eigentümers (gemeinsame Geschichte: der Autorin bzw. des Autors) setzt beim ersten Lauf
mediaBindSincein den signierten Kopf. Der Dienst kann diese Frist nicht verkürzen. 14 Tage später bindet es die übrigen Zeilen als „Bestand“, außerdem Familienbild und Logo. - Gebunden wird nur, was mit seinem eigenen Schlüssel öffnet: Fotos, Poster und Ton ganz, Videos Block für Block per Range-Abruf (nie ganz im Speicher); die Blockliste wird versiegelt daneben abgelegt.
- Ist nichts mehr offen (zweite Zählung), setzt das Gerät des Eigentümers
mediaBoundin den Kopf. Zeilen, die auf diesem Gerät nicht öffnen, halten die Marke auf.
13. Prüfung und Meldeweg
- Tests im Repository: Krypto-Bausteine, Umschläge, Vertrauensspeicher, Rotation, Migration und die Zugriffsregeln der Datenbank (mit PGlite). Dazu Angriffstests: manipulierte Umschläge, vertauschte oder zurückgespielte Zeilen, fremde Signaturen, Zugriffe über fremde Konten.
- Quelltext zur Einsicht: Der Krypto-Kern — die Verschlüsselung auf den Geräten (
src/lib/e2e), die Medienverschlüsselung und der Geräteschlüssel unter Android — liegt seit 27.10.2026 öffentlich unter github.com/family-memories-eu/krypto-kern: byteidentisch mit der App, zusammen mit den Tests, die ohne Datenbank laufen. Lizenz: PolyForm Strict 1.0.0 mit einem Zusatz für Sicherheitsforschung (lesen, ausführen, prüfen und Befunde mit Ausschnitten veröffentlichen ja, in eigenen Produkten verwenden nein); das ist kein Open Source.MANIFEST.jsonnennt den App-Commit, aus dem der Stand stammt; ändern sich diese Dateien, wird vor dem nächsten Deploy neu exportiert. Nicht enthalten sind die Datenbankregeln (§8, §11), und dass die Website genau diesen Code ausliefert, beweist der Export nicht (§10.1). Auf einer Seite zusammengefasst: das Faktenblatt „Was wir sehen — und was nicht“ (§15). - Prüfung nach Prüflisten für Web und Server (OWASP ASVS), Android (OWASP MASVS) und die bekannten Fallen bei Kryptografie (Nonces, Bindung, Kodierung, Herabstufung). Jeder Befund wird gegengeprüft und behoben, bevor E2E als fertig gilt.
- Klartext-Flüsse: Ein Netzwerkmitschnitt zeigt, dass außerhalb von §9 nichts im Klartext das Gerät verlässt.
- Meldeweg: Sicherheitslücken nimmt die Seite „Sicherheitslücke melden“ (
/sicherheit, in allen Sprachen der App) über ein Formular oder per E-Mail ansecurity@family-memories.euentgegen;/.well-known/security.txt(RFC 9116) verweist dorthin. Zusagen: Eingangsbestätigung innerhalb von drei Werktagen, Offenlegung nach der Behebung, längstens nach 90 Tagen, Nennung auf Wunsch. Wer die Regeln der Seite einhält und redlich meldet, muss keine rechtlichen Schritte fürchten. Eine Geldprämie gibt es nicht.
14. Offene Punkte
- Speicherbedarf auf schwachen Geräten messen (Ziel: 5 000 Fotos auf einem Tablet mit 2 GB RAM).
- Safari: Verhalten von IndexedDB bei installierter Web-App gegenüber Tab prüfen (7-Tage-Regel); Hinweis „Zum Home-Bildschirm“ gezielt anzeigen.
- WebAuthn-PRF auf den Zielgeräten testen (iOS 18+, Android mit Google Passwortmanager, Windows Hello).
15. Fragen aus der Datenschutz-Szene
Diese Fragen kommen fast immer, wenn Menschen aus der Datenschutz-Szene das Verfahren zum ersten Mal sehen. Die Antworten sind kurz; die Einzelheiten stehen in den genannten Abschnitten. Auf einer Seite zusammengefasst: das Faktenblatt „Was wir sehen — und was nicht“.
1. Warum ist der Code nicht offen? Der Krypto-Kern — alles, was Schlüssel erzeugt, verpackt und Inhalte ver- und entschlüsselt, dazu die Medienverschlüsselung und der Geräteschlüssel unter Android — liegt zur Einsicht unter github.com/family-memories-eu/krypto-kern, byteidentisch mit der App und mit den Tests, die ohne Datenbank laufen (§13). Lesen, ausführen, prüfen und Befunde mit Ausschnitten veröffentlichen ist erlaubt (PolyForm Strict 1.0.0 mit Zusatz für Sicherheitsforschung). Der Rest der App ist nicht veröffentlicht: Family Memories ist ein Ein-Personen-Projekt, das von Abos lebt. Das ist kein Open Source, und wir nennen es auch nicht so.
2. Eine Web-App kann sich selbst verraten.
Stimmt, das ist die größte Grenze jeder Ende-zu-Ende-Web-App (§10.1). Gegen fremden Code schützen eine strenge
Content-Security-Policy ohne unsafe-inline für Skripte, Trusted Types und Prüfsummen für die Startskripte —
gegen uns als Betreiber schützen sie nicht. Wer das nicht hinnehmen will, nimmt die Android-App: Sie bringt
ihren Code mit und bekommt neuen nur über signierte Updates (§10.2). Ob die Website den veröffentlichten
Krypto-Kern ausliefert, lässt sich vergleichen (Build-ID der Seite gegen MANIFEST.json), aber nicht beweisen;
reproduzierbare Builds sind geplant.
3. Welche Metadaten seht ihr? Wer mit wem in einer Familie ist und mit welcher Rolle, Profilnamen und E-Mail-Adressen, Zeitpunkte von Uploads und Änderungen, Anzahl, Größe und Typ der Dateien, welche Zeilen zusammengehören und grobe Kartenabrufe. Die vollständige Liste steht in §10.4, die verschlüsselten Spalten in §4.3.
4. Welche Anbieter, wo? Laut Datenschutzerklärung (Ziffer 14): Hostinger (Website, Deutschland), Supabase (Datenbank, Anmeldung und Dateien in Frankfurt; Anbietersitz USA, EU-Standardvertragsklauseln), Mailjet (Systemmails, EU), Hetzner (unsere Fehlerdiagnose und der Druckdienst, Deutschland), Cloudflare R2 (Kartenkacheln — dabei sieht Cloudflare die IP-Adresse — und Druckdateien; Speicherung in der EU, Anbietersitz USA), Gelato (Druck und Versand, Norwegen; Partnerdruckereien auch außerhalb der EU), Stripe (Zahlungen, Irland), sevDesk (Buchhaltung, Deutschland), DeepL (Übersetzung auf Knopfdruck, EU) und Mistral (Support-Chat Lumi, EU; sieht nur, was im Chat getippt wird). Inhalte sehen davon nur die Dienste, denen ihr ausdrücklich etwas gebt (§9).
5. Der Druck ist Klartext. Ja. Für ein gedrucktes Buch muss die Druckerei lesen können; deshalb gibt es einen eigenen Zustimmungsdialog (§9.3). Der Browser schickt die Inhalte direkt an unseren Druckdienst, der sie nur im Arbeitsspeicher hält. Das Druck-PDF wird bei uns 30 Tage nach der Zustellung gelöscht; Gelato nennt keine feste Frist.
6. Wer steckt dahinter, und was passiert, wenn das Projekt endet? Ein Einzelunternehmer aus Deutschland (Impressum). Das Geschäftsmodell ist ein Abo ohne Werbung. Endet das Projekt, kündigen wir das mindestens sechs Monate vorher an — per E-Mail an alle Konten und in der App. Bis zum letzten Tag lässt sich alles herunterladen, entschlüsselt im eigenen Browser: unter „Konto & Datenschutz“ → „Meine Daten herunterladen“ ein ZIP mit allen Familien, Geschichten und Medien (Medien bis 400 MB je Export; was darüber liegt, wird aufgelistet), für große Archive in der Familie „Alle Medien herunterladen“ als eine Datei, dazu die PDF-Chronik.
7. Und iOS? Keine eigene App. Die Web-App läuft in Safari und lässt sich über „Zum Home-Bildschirm“ wie eine App ablegen. Die Grenzen aus Frage 2 gelten dort genauso; das Verhalten von Safaris Speicher prüfen wir noch (§14).
8. F-Droid? Nein. F-Droid setzt eine freie Lizenz voraus, und die App ist nicht frei lizenziert. Die APK gibt es direkt bei uns; den Fingerabdruck des Signaturzertifikats veröffentlichen wir auf der Download-Seite und in §10.2, damit sich die erste Installation prüfen lässt.
9. Kann der Server Schlüssel unterschieben? Bei bestehenden Kontakten nicht still: Menschliche Bestätigung und der Vertrauensspeicher verhindern es (§5.3, §5.4). Beim allerersten Kontakt mit einer neuen Person könnte der Dienst einen eigenen Schlüssel unter ihrem Namen vorlegen (§10.3). Dagegen hilft der Sicherheitscode, den zwei Personen vergleichen können (§5.5) — auch schon, bevor das neue Mitglied den Familienschlüssel bekommen hat. Später: Schlüsseltransparenz.
10. Warum Werbung bei Meta?
Weil ein Ein-Personen-Projekt ohne Reichweite sonst kaum Familien erreicht, die nicht ohnehin über Datenschutz
lesen. „Kein Tracking, keine Werbung“ gilt für den Dienst: Family Memories zeigt keine Anzeigen und enthält keine
Tracker. Für den Dienst selbst werben wir trotzdem, mit bezahlten Anzeigen auf Instagram. Dieses Spannungsfeld
reden wir nicht weg.
Es gibt kein Meta-Pixel und keine Conversions-API, und wir geben Meta keine Adresslisten oder sonstigen Daten von
Menschen, die bei uns sind. Gemessen wird nur über die Kampagnenkennung im Link (utm_source, utm_campaign,
utm_content): Sie bezeichnet eine Anzeige, nie eine Person. Sie steht in der Adresszeile, wird nicht auf dem
Gerät gespeichert und kommt erst in unsere Datenbank, wenn sich jemand in die Warteliste einträgt oder eine
Familie anlegt. Von Meta bekommen wir nur zusammengefasste Zahlen wie Reichweite, Klicks und Kosten. Meta kann
dadurch nur auf Klicks optimieren, nicht auf Eintragungen.
Was Meta selbst sieht: wer die Anzeige gesehen und angeklickt hat, denn das geschieht auf Metas Plattform. An den
Link hängt Meta eine eigene Klick-Kennung (fbclid). Wir lesen sie nicht aus; wie jede aufgerufene Adresse kann
sie aber im Zugriffsprotokoll des Hosters stehen. In der Instagram-App öffnet sich die Seite meist im eingebauten
Browser. Was die App dort erfassen kann, können wir von der Seite aus nicht zuverlässig verhindern. Anmelden und
Inhalte ansehen gehört deshalb in einen eigenen Browser oder in die Android-App (§10.1, §10.2). Einzelheiten
stehen in der Datenschutzerklärung, Ziffer 25.
Eine Schwachstelle gefunden? Sicherheitslücke melden