Secretary

Apps/secrets/changelog
Login

Apps/secrets/changelog

Secretary Secrets (Android) - Changelog

Every released version of the Secretary Secrets app, newest first.

App documentation: Apps/secrets

Status

Status 2026-10-01 (66)

Secretary Secrets 66 (versionCode 66): die Sync-Aktion ist ohne Konto weg, Passwortfelder zeigen Klartext. Beides sitzt im gemeinsamen Code und trifft damit alle acht Clients.

(1) Keine Sync-Aktion ohne Konto. AppHeader (Kit.kt) blendet die Sync-Aktion bereits aus, wenn onSync null ist - das war die vorhandene Konvention, nur wurde sie an keiner Stelle benutzt. OfflineBanner (StatusBanners.kt) verlangte dagegen einen nicht-nullbaren onSync und zeichnete den Refresh-Button bedingungslos; der Parameter ist jetzt (() -> Unit)?, und ohne Konto fehlt der Knopf (die Meldung "offline" bleibt stehen). Alle Aufrufstellen reichen den Callback nur noch durch, wenn wirklich ein Konto existiert: loggedIn der jeweiligen ViewModel, im Kalender der collected isLoggedIn-StateFlow. Das Journal ist der einzige Client, bei dem das sichtbar war - es hat keine Login-Sperre und laeuft im lokalen Modus (localMode = !repo.isConfigured()) ohne Konto weiter, zeigte den Sync-Knopf aber trotzdem. Beim Secrets-Tresor war der Knopf vorhanden und nur deaktiviert (enabled = state.connected); er wird jetzt gar nicht erst gezeichnet, der Spinner waehrend state.syncing bleibt. Am Launcher aendert sich nichts: die Server-Sync-Sektion im Settings-Screen war schon an account != null gehaengt.

(2) Passwortfelder zeigen Klartext. Alle Eingabefelder fuer Passwoerter nehmen VisualTransformation.None; die Tastatur bleibt KeyboardType.Password. Betroffen sind der gemeinsame Login-Screen (SecretaryLoginScreen), die Journal-Passwortdialoge (PasswordDialogs: Setzen, Aendern, Abfragen) und das Entschperren (UnlockScreen) sowie im Tresor die Felder fuer Recovery-Passphrase, Entschperr-Passphrase, KDBX-Import und KDBX-Export (SecretsApp) und das Passwortfeld im Eintrags-Editor (Browse). Anlass war die Darstellung, nicht die Sicherheit: die Maske besteht aus U+25CF (●), und die gebundelte Typewriter-Schrift Special Elite deckt dieses Zeichen nicht ab - die Felder zeigten unlesbare Ersatzglyphen. In den Tresor-Dialogen ist der Sichtbarkeits-Umschalter damit ueberfluessig und mit ihm die Felder passwordVisible/visible, der Eye-Button bzw. der Textbutton Anzeigen/Verbergen und die Visibility/VisibilityOff-Imports weg. Der Reveal-Pfad fuer gespeicherte Secrets in Browse (FieldRow) bleibt unberuehrt: das ist eine Anzeige, keine Eingabe.

Tests: keine neuen, kein JVM-Verhalten geaendert. 662 Tests gruen (Kalender 55, common 100, Kontakte 44, Launcher 12, Secrets 405, Tasks 46), verifiziert ueber gradle test; alle neun Module kompilieren (compileDebugKotlin, offline), und die Neuuebersetzung meldet in keiner der geaenderten Dateien eine Warnung. Der Tresor trägt den groeseren Teil: hier liegen vier der Passwortfelder und der Sync-Knopf, der vorher deaktiviert statt verborgen war. versionCode 65 -> 66.

Status 2026-09-30 (65)

Secretary Secrets 65 (versionCode 65): die Namensraum-Karte im Einstellungs-Screen ist weg, die Version steht jetzt unten. Beides steckt im gemeinsamen SecretarySettingsScreen in common (SRS common 1.3.0), trifft also alle acht Clients. Der Parameter collectionNamespace und die Karte Namensraum sind raus - sie zeigte eine Konstante, die der Nutzer ohnehin in der Collection-Liste sieht. Die Konstante "/secrets/" bleibt als CalDAV-Basispfad, wird aber nicht mehr angezeigt. Neu ist die letzte Zeile des Screens: die Version der jeweils laufenden App aus der PackageInfo des eigenen Pakets, jede App zeigt so ihre eigene Nummer, ohne sie durchzureichen. versionCode 64 -> 65.

Status 2026-09-30 (64)

Secretary Secrets 64 (versionCode 64): keine Code-Aenderung - der Quelltext ist derselbe wie bei 63. Veraendert wurde in diesem Zug nur der Launcher (Korrektur am Konfigurations-Sync, dort 460). Die Nummer geht trotzdem hoch, weil ein Release alle acht Clients und beide Serverpakete zusammen ausliefert: das Buildsystem ueberspringt einen Lauf, dessen Zieldateinamen alle bereits im Archiv liegen, und ein gleichnamiges Paket mit gleichem Inhalt waere auf dem Geraet nicht von 63 zu unterscheiden - bei einer APK zusaetzlich ein versionCode, den ein Geraet nicht neu installiert. Server und check-links gehen mit auf 0.24.0. versionCode 63 -> 64.

Status 2026-09-29 (63)

Secretary Secrets 63 (versionCode 63): Abschluss des Release-Zugs, der Signiermaterial aus einer Umgebungsvariablen in eine Datei verlagert hat (siehe 62). Der Sprung ist der von 4d6b02eee5 für alle acht Module gemeinsam gesetzte: BuildManager vergleicht Dateinamen statt Inhalte, also liefert derselbe Code ohne neue versionCode denselben Namen und der Build wird als „bereits im Repo“ abgewiesen. An Secrets änderte sich in diesem Schritt kein Funktionscode. versionCode 62 -> 63.

Status 2026-09-29 (62)

Secretary Secrets 62 (versionCode 62): alle acht Clients signieren ihre Release-APKs, nicht nur der Launcher. Für Secrets war das mehr als Kosmetik: der KDBX-Release-Build ist das, was eine bestehende Tresordatei überhaupt portabel macht, und vorher entstand ausschließlich *-unsigned.apk. Zwei Nacharbeiten blieben in derselben Version: der Default-Pfad für das Keystore war um eine Ebene zu kurz, und die Zugangsdaten wurden aus dem Prozess-Environment in eine Datei verlagert, damit ein Release nicht von einer in keinem --help genannten Umgebungsvariable abhängt. versionCode 61 -> 62.

Status 2026-09-29 (61)

Secretary Secrets 61 (versionCode 61): Signiermaterial in ein eigenes Fossil-Repository signing-certs ausgelagert (vorher lag es im Secretary-Repository selbst, zusammen mit dem Quelltext und dem, was KDBX-Passwörter schon nicht mehr enthielt). An Secrets änderte das keine Zeile Funktionscode; die Version trägt die neue Signierkonfiguration. versionCode 60 -> 61.

Status 2026-09-29 (60)

Secretary Secrets 60 (versionCode 60): Konto, gemeinsamer Einstellungs-Screen und Konfigurations-Sync für den Launcher. Der Commit hielt die versionCode aller acht Module gleichzeitig fest. An Secrets änderte sich dabei keine Zeile; die Funktionsänderung betraf ausschließlich den Launcher. Die Nummer 59 wurde nie vergeben — dieser Commit sprang von 58 auf 60. versionCode 58 -> 60.

Status 2026-09-26 (58)

Secretary Secrets 58 (versionCode 58): M6 (Documentation & User Guides) vollstaendig — todo.md Abschnitt 6 ist zu Ende. Der Recovery-Key ist jetzt erzeugbar, anzeigbar und verwendbar; der User-Guide, der in v57 bewusst nicht geschrieben worden war, ist geschrieben. Kein Schema-Bruch, kein Format-Bruch; das Datenformat recovery-key.json ist unveraendert, die scrypt-Parameter darin stehen weiterhin im JSON.

Der Abschnitt war in v57 als blockiert markiert, weil nicht die Doku fehlte, sondern die Funktion. Ergaenzt wurde deshalb nicht nur ein Bildschirm, sondern der Flow als Ganzes: RecoveryKeySetup (neu, data/, ohne Android-Abhaengigkeit, damit der Pfad JVM-testbar bleibt), VaultViewModel.createRecoveryKey/unlockWithRecoveryKey/dismissRecoveryKey sowie in der UI ein Eintrag unter Einstellungen → Sicherheit, ein Erzeugungs-Dialog, die Einmal-Anzeige und ein Entschperr-Dialog auf Home. Der Screen allein haette nichts genutzt: ohne Entschperr-Pfad ist ein Recovery-Key nur ein Zettel.

Drei Fehler, die beim Bauen auffielen — zwei davon sind schwerer als der fehlende Screen, und keiner davon stand auf der Roadmap.

(1) clearMessage() war unendlich selbstrekursiv (VaultViewModel). Der Rumpf war clearMessage() — also sich selbst. SecretsApp ruft die Funktion nach jeder Snackbar auf (LaunchedEffect(state.messageSeq) → showSnackbar → clearMessage()), also StackOverflowError und App-Absturz bei jeder Meldung, u. a. nach jedem lock() („Tresor gesperrt"). Ein Live-Crash, nicht nur ein totes Feature; gefunden, weil der Recovery-Flow dieselbe Meldung brauchte und der Testlauf sofort rot wurde.

(2) scrypt N=2^20 war auf Android nicht ausfuehrbar. Der Speicherbedarf von scrypt ist 128 * r * N; fuer N=2^20, r=8 sind das 1024 MiB — auf dem Build-Rechner gemessen (~4,3 s), gegen einen Android-Prozess-Heap von typisch 256-512 MiB. Die App konnte also nie in den Besitz eines erzeugten Keys kommen. Die Notiz in CONTEXT.md v25 („N=2^20=1 MiB") war um Faktor 1024 falsch: 2^20 ist die Rundenanzahl, nicht der Speicher. Der erste Lauf des neuen Tests endete deshalb in 13 von 13 OutOfMemoryError — der Parameter hat den Testlauf nicht ueberlebt, nicht der Test. Auf N=2^16 umgestellt (64 MiB, ~270 ms, der von RFC 7914 §12 fuer interaktive Anwendungen genannte Bereich); r/p/dkLen bleiben SRS-konform. Als Abweichung von SRS §3.2 dokumentiert (SECURITY.md §5.2 mit der Messtabelle, §8.1 als Erratum-Vermerk) statt stillschweigend angepasst — die Entscheidung liegt beim Betreiber, der Messwert ist aber belegt statt behauptet. Rueckwaertskompatibilitaet: die Parameter stehen in jedem erzeugten Key-JSON und werden von dort gelesen, nicht aus dem Default. Nebenwirkung, die ausdruecklich zu nennen ist: ScryptParams ist eine gemeinsame Klasse, also bewegt der geaenderte Default auch den dp1:-Pfad (DeviceKeyPassphraseCipher.wrap, CLI-SEC-01) von 1024 MiB auf 64 MiB. Dort war es derselbe Defekt — der Weg ist allerdings bis v58 aus der UI unerreichbar gewesen (siehe Nebenbefund), es kann also kein dp1:-Datensatz mit N=2^20 aus einer ausgelieferten App stammen.

(3) Die Scrypt-Parameter kommen aus der Datei — und die Datei kommt vom Server. ScryptParams.fromJson prueft jetzt den Speicherbedarf gegen MAX_MEMORY_BYTES = 256 MiB. Ohne diese Grenze waere eine manipulierte recovery-key.json ({"scrypt":{"n":1073741824}}) ein Denial-of-Service: OutOfMemoryError statt einer lesbaren Fehlermeldung. Der alte Default N=2^20 liegt ueber dieser Grenze und wird deshalb beim Lesen abgewiesen — folgenlos, weil seal in v25..v57 nie produktiv aufgerufen wurde; diese Annahme ist in RecoveryKeySetupTest.createVerifiesTheProductionScryptParams festgenagelt. Ausserdem normalisiert RecoveryKey.fromJson die Parse-Grenze jetzt auf RecoveryKeyFormatException, weil ein org.json.JSONException aus einem Serverdokument sonst als undurchsichtiger Parser-Fehler in der UI landete.

Nebenbefund am Rande, derselbe Fehlertyp wie (1) — ein implementierter, aber unerreichbarer Pfad: unlockWithPassphrase (v51) wird von der UI nirgends aufgerufen, und currentMasterKey() entriegelt still ueber den Keystore nach. Ein passphrase-geschuetzter Tresor kam deshalb bei der ersten MK-bennoetigenden Operation (Suche, Import, Sync) in eine IllegalArgumentException ohne Bedienung, statt zu fragen. Der neue Entschperr-Dialog macht alle drei Wege erreichbar und bietet nur die an, die fuer diesen Tresor existieren (passphraseProtected, recoveryConfigured) — ein Weg, den es nicht gibt, wird nicht als Auswahl angeboten, sonst probiert der Nutzer ihn durch.

Datenweg (RecoveryKeySetup, bewusst zwei Details mehr als „MK wrappen"):

Bewusst offengelegt (SECURITY.md §5.5, README §7): Die Recovery-Passphrase wird nicht auf Qualitaet geprueft (nur Mindestlaenge 12 Zeichen, kein Woerterbuch-/Breach-Abgleich). Der Key wird ueber die Zwischenablage ausgegeben — der komfortabelste Weg in einen anderen Passwortmanager und zugleich der einzige, den die App anbietet; Android zeigt den Inhalt in der Vorschau. Es gibt keinen Ausgabepfad in eine eigene Datei und keine Biometrie als dritte Huerde. Der Screen fuer den Key ist ein Dialog, kein dauerhafter Screen — Absicht: ein abrufbarer Key im UI waere ein zweiter Weg an der Absicherung vorbei; wer ihn verpasst, erzeugt einen neuen.

Tests: +19. Suite 405 Tests gruen (:secrets, 386 -> 405), 606 gruen ueber alle fuenf Module. RecoveryKeySetupTest neu (16): Key liegt im Snapshot, genau ein PutResource, ETag wandert mit, Erneuern laesst keinen Stale-Put zurueck, Recovery-Put steht vorne, andere Ressourcen unberuehrt, zu kurze Passphrase und falsche MK-Groesse abgelehnt, kein Klartext-MK im JSON (Base64 und Hex), Roundtrip, Wiederherstellung aus einem nur per Pull bezogenen Snapshot, falsche Passphrase laesst WrongKeyException (bewusst kein recovery-eigener Typ), fehlender Key meldet „keiner" statt „falsch", leerer Body ist „keiner" und nicht „kaputt", falsches JSON faesst RecoveryKeyFormatException statt JSONException, Produktionsparameter festgenagelt. RecoveryKeyTest um 3: Default-Params inkl. der Zeile, um die es ging (64 MiB, nicht 1 GiB), staerkere Legacy-Parameter bleiben lesbar, der alte N=2^20-Default wird beim Lesen abgewiesen. Ein bestehender Test musste angepasst werden: das recoveryBody()-Fixture in SecretSyncTest trug hart n=1048576 und wurde vom Pull jetzt zurecht als unlesbar eingestuft — die Interaktion zwischen neuer Obergrenze und Serverdaten fiel also sofort auf. Lint: 0 Fehler, 4 Warnungen (unveraendert nur GradleDependency 2x / NewerVersionAvailable / ObsoleteSdkInt). Debug-APK assembliert, versionCode 58 verifiziert. versionCode 57 -> 58. Doc: SECURITY.md §5 vollstaendig neu geschrieben (5.1 Was der Key ist, 5.2 scrypt-Kosten mit Messtabelle, 5.3 Erzeugen, 5.4 Verwenden, 5.5 was das weiterhin nicht schuetzt), §4.2 um die zwei neuen offengelegten Luercken ergaenzt, §8.1 als Erratum-Vermerk zur Abweichung von SRS §3.2; SECURITY.pdf entsprechend neu erzeugt (9 Seiten A4, Inhaltsverzeichnis enthaelt die neuen Unterabschnitte, verifiziert per pdftotext). BENUTZERANLEITUNG.md neu. todo.md Abschnitt 6 auf DONE, CONTEXT.md Konzepte-Abschnitt auf N=2^16 korrigiert. Die Seitenzahlen-Nennungen in den historischen v57-Eintraegen wurden bewusst nicht angefasst — sie beschreiben den Stand von damals.

Doc + App + Tests.

Status 2026-09-26 (57)

Secretary Secrets 57 (versionCode 57): M6 (Documentation), erster Teil — todo.md Abschnitt 6. Zwei der drei Punkte erledigt, der dritte ist blockiert, weil nicht die Doku fehlt, sondern die Funktion. Kein Schema-Bruch, kein Format-Bruch, kein Wire-Format-Bruch; kein App-Code geaendert.

(1) Der In-App-Guide ist blockiert, weil der Recovery-Key nicht erzeugbar ist (RecoveryKey). Das ist der eigentliche Fund dieses Abschnitts, und er faellt nicht beim Schreiben, sondern beim Nachsehen: RecoveryKeyCipher ist vollstaendig implementiert und getestet (RecoveryKeyTest, Roundtrip/Falsch-Passwort-Ablehnung/frisches Salt+IV je Seal/JSON-Roundtrip/Schema-Guards), wird aber von keinem Produktionscode aufgerufen. grep ueber src/main und src/test findet RecoveryKeyCipher.seal ausschliesslich in RecoveryKeyTest; die app/-Schicht (VaultViewModel, ui/) enthaelt keinen einzigen Treffer auf recovery. Es gibt keinen Bildschirm, der einen Recovery-Key erzeugt, anzeigt oder speichert, und keinen Weg, einen zur Entsperrung einzuspeisen. SecretSync kann eine recovery-key.json hoch- und herunterladen (SecretWire.Kind.RECOVERY, recoveryHref(), offer(live.recovery, …)) — nur erzeugen kann sie niemand.

Konsequenz fuer den Nutzer: wer den MK verliert, verliert den Tresor. Und die Verwechslungsgefahr ist real, weil der Passphrase-Fallback existiert und ähnlich klingt: wrapMasterKeyWithPassphrase() (v51) schuetzt den MK auf dem Geraet unter dp1: — er bringt ihn nicht zurueck. Wer RELEASE.md oder dem Passphrase-Text vertraut, haelt eine Wiederherstellungsmoeglichkeit fuer vorhanden, die es nicht gibt.

Konsequenz fuer diesen Abschnitt: es wurde kein Guide geschrieben. Ein Text, der einen nicht existierenden Flow beschreibt — Schritt 1 „Recovery-Key erzeugen", Schritt 2 „Passphrase setzen", Schritt 3 „sicher ablegen" — waere als Nutzerdokument schlimmer als gar keiner, weil er die Luecke zudeckt statt sie zu benennen. Die Empfehlung steht in todo.md Abschnitt 6: den Flow als eigenes Release bauen, mit Bildschirm und Entsperr-Pfad. Das ist der einzige Punkt der Roadmap, bei dem ein Fehler nicht Daten verliert, sondern Nutzer in der Illusion einer Absicherung laesst.

(2) Developer-README (README.md, neu): Bauen (inkl. des Buildsystems Vertrag, R8-Fallstrick), Tests mit echtem Stand (587/386), lokaler secretary-server, Full-Stack-Testbetrieb. Der Grundsatz dabei war, jedes Kommando vor dem Niederschreiben auszufuehren statt es aus CONTEXT.md abzuschreiben — und das hat drei Fallstricke gefunden, die in keiner Doku standen:

Verifiziert wurden ausserdem: MKCOL mit Displayname-Body → 201, PUT → 201, PROPFIND Depth 1 → 207 mit secrets/<uuid>/ und secrets/<uuid>/group.ics, GET → 200, Auth 207/401/401 (richtig/falsch/ohne), und das Plattenlayout inklusive .metadata/ .synctoken/.sync-log/.storage.lock. cargo build --release laeuft aus dem Cache in 0,61 s durch.

Der Full-Stack-Abschnitt sagt ausdruecklich, was es nicht gibt: keinen automatisierten Ende-zu-Ende-Test. Die Sync-Logik laeuft gegen einen FakeRemote; echt ist nur der KDBX-Interop gegen keepassxc-cli, falls installiert (FR-K-30), und cargo test (141 Unit + 59 Integration) auf der Serverseite. Als Ersatz steht ein reproduzierbares curl-Verfahren drin — mit erwarteten Statuscodes und erwartetem Plattenlayout, damit es als Test taugt und nicht als Lesebeispiel.

(3) Security-Whitepaper (SECURITY.md + SECURITY.pdf, 7 Seiten A4 mit Inhaltsverzeichnis): kryptografisches Design (Envelope-Layout base64(iv(12) ‖ AES-256-GCM), Allowlist-Semantik, Keystore ohne stillen Downgrade, MK-Lebenszyklus, Ciphertext-Fingerprint), Server (Escrow- Schluesseldienst nach Model B, Auth, descriptor-relatives Dateisystem ohne TOCTOU-Fenster), Bedrohungsmodell als beide Listen — adressiert und bewusst nicht adressiert —, Seitenkanaele als Bedingungspruefung statt Timing-Test, Versionierungsstrategie. Der Whitepaper nennt die eigene Luecke ausdruecklich (§5 „Recovery-Key: implementiert, aber nicht erreichbar") und zaehlt usesCleartextTraffic, fehlendes Pinning, fehlende Biometrie und den fehlenden E2E-Test unter „nicht adressiert": ein Sicherheitsdokument, das die eigenen Luecken verschweigt, ist wertlos.

Reproduzierbarkeit statt Einmal-PDF: doc/build-whitepaper.sh erzeugt das PDF aus der versionierten Quelle (pandoc → weasyprint; LaTeX ist absichtlich nicht verwendet, weil es nicht installiert ist und ein Sicherheitsdokument aus den tatsaechlich vorhandenen Werkzeugen reproduzierbar sein soll). doc/whitepaper.template.html statt pandocs Standard-Template, weil pandoc --toc nur ueber ein Template erzeugt — im reinen Fragment wird es stillschweigend verworfen, der erste Build hatte deshalb ein PDF ganz ohne Inhaltsverzeichnis (7 Seiten, nur beim Seitenzahl-/Textvergleich aufgefallen, nicht beim Byte-Vergleich). Das CSS kommt per --variable inline, damit das PDF nicht von einem Pfad abhaengt.

Nebenbei korrigiert (Fehler aus v56, hier aufgefallen): der MediaStore-Import wurde mit der oeffentlichen CrashReporter-Kopie in v56 entfernt, install/buildReport brauchen aber nur noch Context und PackageManager — der Absatz behauptete weiterhin MediaStore. Und die Formulierung „Lint: 0 Fehler, 0 Warnungen" war falsch: es sind 0 Fehler und 4 Warnungen (GradleDependency 2x, NewerVersionAvailable, ObsoleteSdkInt). Dieselbe falsche Formulierung steht in den v54-/v55-Eintraegen und bleibt dort unangetastet — sie beschreibt den Lauf von damals, und die tatsaechliche Zahl ist dort nie erhoben worden.

Tests: keine neuen, kein App-Code geaendert. versionCode 56 -> 57. Doc.

Status 2026-09-26 (56)

Secretary Secrets 56 (versionCode 56): M5 (Release-Pipeline) — todo.md Abschnitt 5. Vier der sechs Punkte sind erledigt, zwei bleiben nach ausdruecklicher Entscheidung des Betreibers offen (Signatur, CI). Der schwerste Fund ist aber keiner der Roadmap-Punkte, sondern das Ergebnis des CrashReporter-Audits.

(1) R8 war aus (build.gradle.kts, proguard-rules.pro). release hatte isMinifyEnabled = false — das Release-APK war unminifiziert, und proguard-rules.pro enthielt den Kommentar „none needed for now", ohne dass je jemand geprueft haette, ob sie noetig sind. Aktiviert (plus isShrinkResources) laeuft minifyReleaseWithR8 mit einer Regelliste durch, die erst nach einem Audit entstanden ist und entsprechend kurz ausfaellt: kein Class.forName, kein ServiceLoader, kein newInstance auf eigenen Typen, kein valueOf-Dispatch ueber Enums; org.json stammt aus der Plattform (Boot-Classpath, den R8 nie strippt — das Maven-Artefakt ist ausschliesslich testImplementation und steckt gar nicht im Release-APK, ein Konflikt mit der Plattform-Implementierung kann also nicht entstehen); BouncyCastle wird nur ueber die statisch referenzierte Lightweight-API genutzt (SCrypt, Argon2BytesGenerator, ChaCha7539Engine, Salsa20Engine, AESEngine), ohne Security.addProvider. Der einzige reflektierende Factory-Aufruf ist XmlPullParserFactory.newInstance(), das ist vertraglich reflektierend, laeuft aber gegen eine Framework-Klasse.

Belege statt Hoffnung: kein missing_rules.txt (R8 musste nichts unterdruecken), lintVitalRelease gruen, und im usage.txt nach dem Bau nachgeschlagen, dass keine einzige App-Klasse entfernt wurde. Diese Gegenprobe war noetig, weil ein erster Blick in mapping.txt drei Klassen als „fehlend" meldete — mapping.txt listet aber nur umbenannte Klassen, also war das ein Fehlalarm. usage.txt zeigte stattdessen, dass SecretOfflineStore, SecretSync und CrashReporter sehr wohl erhalten sind und nur ungenutzte Member verloren haben (clearIssues(), das ein test-only toter Member ist — kein Bug, aber der Grund, warum R8 ihn streichen durfte).

Einzige neue Regel: -keepattributes SourceFile,LineNumberTable. Ohne sie verliert CrashReporter in den Stack-Traces jede Zeilennummer, weil R8 sie als ungenutzt einstuft — ein realer Verlust ausgerechnet fuer das Werkzeug, das die Absturzberichte erzeugt. Kein Geheimnisleck: Klassen- und Methodennamen bleiben obfuskiert. 21,7 MiB -> 1,7 MiB.

(2) Der Absturzbericht ging in den oeffentlichen Speicher (CrashReporter, AndroidManifest.xml). Der Roadmap-Punkt verlangte einen Upload; der Betreiber hat stattdessen ein Audit ohne Upload entschieden — und das Audit hat mehr gefunden als der Upload gezaehlt haette: persist() schrieb jeden Report zusaetzlich nach Download/, ohne jede Berechtigung. Damit wurde genau die Haertung aus v53 umgangen: data_extraction_rules.xml und backup_rules.xml schliessen alle Backup-Domains aus, aber eine Datei in Download/ gehoert gar nicht zu den Domains der App — sie taucht im Dateimanager auf, wird von Cloud-Dateisicherung erfasst und ist fuer jede App mit Speicherzugriff lesbar. Bei einem Passwortmanager ist das inakzeptabel, denn der Report enthaelt den Thread-Dump aller Threads und die Exception-Kette inklusive message: eine NumberFormatException nennt die fehlerhafte Zeichenkette (das kann ein Feldwert eines Eintrags sein), eine DavException den Server-Href. Die oeffentliche Kopie ist entfernt (samt ContentValues/Environment/MediaStore-Imports, die nur dafuer da waren); der Report bleibt in filesDir/crashreports/, also im App-Sandbox und ebenfalls von jeder Sicherung ausgenommen, abrufbar ueber run-as … ls files/crashreports. Zusaetzlich gedeckelt (MAX_REPORTS = 10, aelteste zuerst, gekuerzt nach dem Schreiben, damit ein Fehler beim Kuerzen nicht den frischen Report kostet): ein Absturz beim Start — etwa ein unlesbarer Cache — wiederholt sich sonst bei jedem Startversuch und vervielfacht den Speicherbedarf. Ferner ist RunCatching drumherum, weil ein Kuerzfehler nie den eigentlichen Crash-Report verhindern darf.

(3) Versionspflege automatisiert (build.gradle.kts): bumpVersion (versionCode +1, versionName folgt), showVersion, fossilTag (legt secrets-<versionCode> an). Nicht als Property-Umleitung, sondern als Schreib-Task, weil das Buildsystem per Regex genau versionCode\s*=\s*(\d+) aus der committeten build.gradle.kts liest, um secretary-secrets-<VER>.apk zu benennen — eine indizierte Property haette den Debian-Build still auf 0 gesetzt. Mit dem Regex des Buildsystems nachgewiesen. fossilTag verweigert bei uncommitteten Aenderungen: ein Release-Vermerk gehoert auf einen committeten Stand. ProcessBuilder statt Gradles exec, weil ExecOperations in einem doLast-Block der Kotlin-DSL nicht im Scope ist (das war der erste, scheinbar unerklaerliche Build-Fehler).

(4) Berechtigungen (AndroidManifest.xml): READ_ACCOUNT entfernt. Diese Berechtigung wird in keinem Manifest des Repositories als <permission> deklariert, auch nicht in :common — sie existiert nicht, eine uses-permission darauf ist auf Android wirkungslos und stand nur als Optik aus den anderen Modulen, die denselben Eintrag ebenfalls tragen (:calendar sogar mit einem Tippfehler: secretaryaccount ohne Punkt, also eine andere undefinierte Berechtigung). Es bleiben genau die zwei geforderten: INTERNET (DAV-Sync) und GET_ACCOUNTS (AccountResolver), am gebauten APK per aapt2 dump badging verifiziert.

Bewusst nicht geaendert (Entscheidungen des Betreibers): Release- Signatur — es gibt keinen Release-Schluessel, und ein Schluessel gehoert nicht in dieses Repository; das Release-APK bleibt secrets-release-unsigned.apk. Fuer die Debian-Auslieferung unkritisch, das Buildsystem baut assembleDebug und signiert wird das .deb. CI — die Roadmap-Punkt beschreibt eine GitHub-Actions-Workflow, aber es gibt keine GitHub-Remote (Fossil auf tremere:~/Repositories); Testlauf ist lokal gradle test. Datenschutz-URL — in RELEASE.md als offener, bewusst nicht erfundener TODO(master)-Punkt vermerkt. Kein Abhaengigkeits-Bump in diesem Release-Change: core-ktx 1.19.0 -> 1.19.1 waere harmlos, aber navigation-compose 2.8.0 -> 2.10.2 aendert echtes Navigationsverhalten, und die Navigation ist offline nicht durch einen UI-Test abgesichert — das gehoert in einen eigenen, verifizierten Schritt. Ausserdem: der R8-Build braucht org.jetbrains.kotlin:compose-group-mapping, das nicht im Offline-Cache lag (AGP laesst produceReleaseComposeMapping ohne laufen). Das trifft das Buildsystem nicht (nur assembleDebug), wurde aber fuer die lokale Validierung besorgt.

Tests: +5. Suite 386 Tests gruen (:secrets, 381 -> 386), 587 gruen ueber alle fuenf Module. CrashReporterRetentionTest neu (5): Kuerzen unterhalb/oberhalb/genau am Limit, Sortierung nach lastModified, der realistische Dauerzustand (25 Crashes -> 10 Reports, die juengsten bleiben), fehlendes Verzeichnis ist kein Fehler. Nur das Kuerzen ist JVM-testbar; install/buildReport brauchen Context und PackageManager (der MediaStore-Import ist mit der oeffentlichen Kopie in (2) entfallen — er existierte nur dafuer) und damit ein Geraet oder Robolectric (offline nicht beschaffbar). Lint: 0 Fehler, 4 Warnungen (nur GradleDependency (2x) / NewerVersionAvailable / ObsoleteSdkInt (hier nachweislich falsch) — also 0 Fehler und vier rein informative Hinweise; die frueher hier und in v54/v55 notierte Formulierung „0 Fehler, 0 Warnungen" war falsch). Debug- und Release-APK assembliert, versionCode 56 in beiden verifiziert. versionCode 55 -> 56. Doc + App + Tests.

Status 2026-09-26 (55)

Secretary Secrets 55 (versionCode 55): M4 (Test Expansion), erster Teil — todo.md Abschnitt 4. Die Tests haben dabei einen echten, bei jedem Tresor auftretenden Bug gefunden; die beiden anderen offenen Punkte dieses Abschnitts sind nach Prüfung schon erfüllt (siehe unten).

(1) Jede Gruppe doppelt nach dem ersten Sync (SecretSync, SecretOfflineStore) — der Fund dieses Abschnitts, und er betrifft nicht nur grosse Tresore, sondern jeden: Der Offline-Cache kenntte fuer Gruppen zwei Schlüsselformen. snapshotFromVault schrieb sie unter dem vollen href (/secrets/<uuid>/group.ics), SecretSync.pull und der Put-Pfad unter der Gruppen-UUID — und Snapshot.find erwartet ausgerechnet die UUID (groups[groupUuidOf(href)]). Daraus lief in jedem Sync eine Verdopplung:

  1. snapshotFromVault legt …/group.ics an (Schlüssel = voller href).
  2. find(href) schlägt für diese Gruppe fehl (groups[uuid] ist leer) — der ETag-/Body-Abgleich beim Push bekommt nichts zu sehen.
  3. pull/putWireConditional legt dieselbe Gruppe zusätzlich unter uuid ab. Weil die Map unter einem anderen Schlüssel geschrieben wird, ersetzt der neue Eintrag den alten nicht, er kommt daneben.
  4. buildVault iteriert groups.values ohne Dedupe und materialisiert beide — SecretVault bekommt jede Gruppe zweimal, im Import-Baum sichtbar.

Bei 12 Gruppen ergaben sich 24, groupNames1 war jede Gruppe genau zweimal gelistet. Der Cache wuchs mit jedem Sync weiter (24 → 48 → …), group(uuid) und die Geschwister-Namensprüfung arbeiteten auf einer mehrdeutigen Liste.

Fix an drei Stellen, weil jede allein nicht trägt:

Ergebnis: 12 Gruppen bleiben 12, der erste Sync holt 0 statt 3 Ressourcen unnötig nach, der zweite Sync ist ein echter No-op (0 PUTs, 0 Fetches).

(2) Import-Robustheit (KdbxMalformedInputTest, neu): Der SAF-Picker akzeptiert jede Datei, KdbxContainer.open bekommt also ungeprüfte Byte-Arrays. Der Aufrufer zeigt eine Meldung — was nur stimmt, wenn jede kaputte Eingabe in einer KdbxException landet; ein durchgerutschtes IndexOutOfBoundsException oder eine OutOfMemoryError wäre ein Import-Crash. Die Matrix (leer, 3 Byte, Zufallsbytes, ASCII-„KDBX", XML, gzip ohne KDBX, falsche Signatur, Version 0/0xFFFFFFFF, leere Feldtabelle, Feldlänge 2^31-1 und 2^32-1) landet ausnahmslos in KdbxCorruptException/KdbxUnsupportedException. Der Feldexkluder-Deckel ("header field length too large") war bereits da und ist jetzt festgenagelt — ohne ihn hätte eine 600-Byte-Datei eine 2-GiB-Allokation ausgelöst. Nebenbefund für die Diagnose: KDBX 4.1 ist im Wire-Format minor=1/major=4 als ein u32 little-endian (01 00 04 00), nicht als zwei Bytes — die ersten drei Probe-Versuche mit vertauschter Byte-Reihenfolge liefen deshalb in „bad signature".

(3) Grosser Tresor (SecretSyncTest, 12 Gruppen × 50 Einträge = 600): jeder Eintrag wird gepusht (613 PUTs, 12 Collections, leere Pending-Queue), der zweite Sync ist ein No-op, zwei transiente Netzfehler mitten im Walk werden gerettet (600/600 im Tresor, Cache konsistent), und bei Dauerausfall bleiben Cache und Pending-Queue unangetastet. Der Test war der Auslöser für (1).

(4) Zyklische Gruppenverschiebung (SecretVaultTest, +5): moveGroup hat den Zyklus-Schutz (cannot move group into its own subtree), aber kein Test hielt ihn fest — genau der Punkt, den todo.md nennt. Eine spätere Refactorierung hätte die Prüfung stillschweigend entfernen können, und groupPath/isDescendant wären in eine Endlosrekursion gelaufen. Neu festgenagelt: Umhängen in den eigenen Nachfahren (direkt und über die Wurzel), Verschieben in sich selbst (bewusst No-op, damit Aufrufer nicht abfangen müssen), in eine nicht existierende Gruppe, Root-Gruppe (No-op) und auf null (die zweite „Elternteil ist die Wurzel"-Kodierung, dieselbe wie beim KDBX-Import).

„Crypto side-channel tests" bewusst nicht als Timing-Test umgesetzt. Ein Timing-Test ist für diese Frage das falsche Werkzeug: er ist auf einer Shared-CI-Maschine verrauscht und würde eine Zufallseigenschaft messen, keine Eigenschaft des Codes. Geprüft wurde stattdessen die tatsächliche Bedingung — dass kein Geheimnisvergleich mit == läuft. Ergebnis: alle vier Verifikationsstellen (Header-SHA-256, Header-HMAC, Block-MACs beim Lesen und Schreiben) gehen über das bereits vorhandene hmacEquals, das XOR-akkumuliert und erst am Ende vergleicht. Passphrase- und Recovery-Weg nutzen scrypt + AES-256-GCM; die Tag-Prüfung macht der JCE-Provider, nicht unser Code. Die einzige verbleibende contentEquals auf einem Prüfwert ist der ungeheimmte, trunkierte SHA-256 eines Keyfiles gegen dessen eigenes Attribut — ein Angrefer kontrolliert beide Seiten und kann den Wert ohnehin vorrechnen, es gibt also nichts zu gewinnen; sie constant-time zu machen wäre Cargo-Culting.

Bewusst nicht umgesetzt: Robolectric-Tests (UI-Crash-Safety) — die Abhängigkeit ist im Offline-Cache nicht vorhanden und lässt sich nicht beschaffen. End-to-End-Sync gegen einen echten secretary-server in der CI — setzt einen laufenden Server plus CI voraus; die Items in Abschnitt 5 („Git-Tags", „GitHub Actions") beschreiben zudem eine GitHub-Workflow, die es hier nicht gibt: das Projekt liegt in einem Fossil-Repository auf tremere:~/Repositories, der Commit oben ist bereits dorthin synchronisiert. Diese Punkte gehören auf eine bewusste Bauentscheidung (Beschaffung der Abhängigkeit, Server-Harness, Release-Signing), nicht in einen Testlauf.

Tests: +19. Suite 381 Tests grün (:secrets, 362 -> 381), 582 grün über alle fünf Module (SecretsCipherTest unverändert; SecretSyncTest +6, KdbxMalformedInputTest neu mit 7, SecretVaultTest +5). Lint: 0 Fehler, 0 Warnungen (nur die bekannten GradleDependency/ObsoleteSdkInt). Debug-APK assembliert, versionCode 55 verifiziert. versionCode 54 -> 55. Doc + App + Tests.

Status 2026-09-26 (54)

Secretary Secrets 54 (versionCode 54): Nachbesserung des M1-Krypto-Audits (drei Punkte aus todo.md, Abschnitt 1) — kein Schema-Bruch, kein Format-Bruch, kein Wire-Format-Bruch. Die drei Punkte waren in v51 umgesetzt, aber jeweils mit einem Rest, der den eigentlichen Zweck verfehlte.

(1) Envelope-Allowlist statt „irgendeine Version" (SecretsCipher): versionOf akzeptierte in v51 jede formal korrekte Version einer bekannten Familie — ss9: galt als gueltig. Da alle vier Familien bewusst dasselbe Byte-Layout teilen (base64(iv(12) ‖ AES-256-GCM(ct))), laeuft ein ss9:-Envelope nicht in einen Parse-Fehler, sondern bis zum GCM-Tag und endet dort als WrongKeyException — die Meldung „falsches Passwort" fuer ein reines Kompatibilitaetsproblem (ein spaeterer Build, Algorithmuswechsel in v2). Jetzt eine explizite Allowlist SUPPORTED_FAMILIES (Familie -> einzige Version) mit SUPPORTED_PREFIXES, isSupportedPrefix(prefix) und isKnownFamily(prefix). Die Praefix-Konstanten (RECOVERY_PREFIX, DEVICE_KEYSTORE_PREFIX, DEVICE_PASSPHRASE_PREFIX) stehen in SecretsCipher und werden von RecoveryKey.ENVELOPE_PREFIX sowie DeviceKeyRecord.ENVELOPE_PREFIX/PASSPHRASE_ENVELOPE_PREFIX referenziert, statt eigene Literale zu fuehren — sonst koennte die Allowlist z. B. dp1: kennen, waehrend DeviceKeyRecord etwas anderes versiegelt (wrapPrefixesAndAllowlistCannotDriftApart nagelt das fest). sealWith verweigert nicht erlaubte Praefixe nun schreibend und bricht davor ab: sonst koennte die App einen device-key.json/recovery-key.json erzeugen, den exakt dieser Build nicht mehr oeffnen kann, und der Fehler faellt erst spaeter beim Entsiegeln auf. Abgrenzung beim Lesen: bekannte Familie mit fremder Version (ss9:) ⇒ UnsupportedEnvelopeVersionException („incompatibel"), fremde Familie (zz1:, anderes Tool) ⇒ MalformedEnvelopeException — das ist kein Nachfolger-Format von ss1:, sondern etwas voellig anderes. In beiden Faellen nie ein Schluesselfehler; genau daran wird im Test futureVersionIsReportedAsIncompatibilityNotAWrongKey geprueft, dass die Meldung nicht „authentication failed" enthaelt.

Dabei fiel ein Fehler in der Allowlist selbst: SUPPORTED_FAMILIES baut sich ueber familyOf() auf, und dessen Regex war darunter deklariert. Properties eines object werden in Deklarationsreihenfolge initialisiert — der Regex war beim Bau der Map noch null, !! warf, und SecretsCipher starb mit ExceptionInInitializerError in jedem Test und in der App. Der Regex steht jetzt davor, mit Kommentar, damit die Reihenfolge nicht wieder verrutscht.

(2) Keystore-Fallback war unerreichbar (DeviceKeyStore): v51 hat den Fehlerfall richtig erkannt (kein stiller Downgrade auf einen extrahierbaren Software-Schluessel, KeystoreUnavailableException) — aber am falschen Ort. KeyStore.getInstance("AndroidKeyStore").apply { load(null) } stand als val im Konstruktor. Auf einem Geraet ohne nutzbaren Keystore warf das bereits beim Erzeugen der Klasse, also praktisch beim Start des VaultViewModel. Damit waren genau die Wege unerreichbar, die ohne Keystore auskommen sollten: der ausdrueckliche Passphrase-Fallback wrapMasterKeyWithPassphrase(), hasWrappedRecord(), protection() und das Entsiegeln eines PASSPHRASE-Datensatzes. Auf dem Geraet, fuer das der Fallback gedacht war, war er tote API. Jetzt by lazy mit gecachtem null; der Fehler greift erst dort, wo wirklich mit dem Keystore gearbeitet wird (deviceKey(), und getKey/generateKey wurden ebenso abgefangen, nicht nur getInstance). Folgen: hasDeviceKey() liefert false statt zu werfen, unlock() meldet auf einem Keystore-losen Geraet schlicht „nicht entsperrt" statt abzustuerzen, und ein KEYSTORE-Datensatz ohne Geraeteschluessel meldet ehrlich KeystoreUnavailableException statt der irrefuehrenden IllegalArgumentException „no device-key record present" (die Datei ist da, der Schluessel fehlt). Die Entscheidung „kein Keystore ist ein erwarteter Zustand, kein Defekt" steckt in openKeyStoreOrNull(open) — absichtlich eine eigenstaendige interne Funktion, damit sie ohne Android-Geraet testbar ist; in einem Konstruktor versteckt waere sie es nicht (DeviceKeyStoreKeystoreTest, neu: kein Provider, IOException beim load, unerwartete Fehler propagieren weiter, getInstance genau einmal).

(3) wrap() hat die Passphrase des Aufrufers vernichtet (DeviceKeyPassphraseCipher): das finally fuellte neben dem abgeleiteten Schluessel auch das eingereichte CharArray mit NUL-Bytes. Nach einem Wrap war die Passphrase unbrauchbar — obwohl die UI sie unmittelbar danach noch braucht (Bestaetigungsdialog, erneutes Speichern, Tippfehler korrigieren). Genau das ist der Grund, warum man Passphrasen in Java als char[] statt String uebergibt: nicht implizit kopierbar und gezielt loeschbar. Jetzt bleibt das Array unangetastet; verwischt werden ausschliesslich die selbst erzeugten Zwischenpuffer (derived und der UTF-8-Puffer encoded). Wer loeschen will, ruft selbst passphrase.fill('\u0000') — der einzige Produktionsaufrufer (VaultViewModel.unlockWithPassphrase) tat das ohnehin schon vorher selbst. Passphrasen werden als Argument weiterhin CharArray bleiben: die Ownership-Regel ist jetzt dokumentiert statt stillschweigend.

Bewusst nicht geaendert: SealedObject-Grenzen wurden nicht eingefuehrt (Android hat dafuer keine API, die Guertel-Semantik fuer SecretKey waere). isSupportedPrefix bleibt eine synchrone, reine Funktion und damit in der Allowlist anwendbar, wo eine asynchrone Pruefung nicht hineinpasst.

Tests: +18 JVM-Tests. Suite 362 Tests gruen (:secrets, 344 -> 362), 517 gruen ueber alle Module mit Tests (:common 75, :secrets 362, :calendar 55, :contacts 25). SecretsCipherTest um 8 (Allowlist-Vollstaendigkeit, Praefix-Herkunft aus den anderen Klassen, bekannte Familie/fremde Version, fremde Familie, Schreibsperre, futureVersion ≠ falscher Schluessel, familyOf, nicht-zyklische/ungueltige Praefixe — inkl. ss:, "" und ss99999999999999999999: als Endlosziffern-Fall), DeviceKeyPassphraseCipherTest um 4 (Array nach wrap/unwrap unveraendert, ein Array fuer Wrap+Unwrap+Wrap, Loeschen ist Sache des Aufrufers), DeviceKeyStoreKeystoreTest neu (5). Zwei bestehende Tests wurden den scharferen Regeln angepasst: foreignVersionedPrefixRejected erwartet nun UnsupportedEnvelopeVersionException statt MalformedEnvelopeException, und unknownPrefixFamilyRejectedByGenericApi baut sein zz1:-Envelope per Retagging, weil sealWith es jetzt verweigert. Lint: 0 Fehler, 0 Warnungen; nur die bekannten GradleDependency (navigation-compose 2.10.0, offline nicht behebbar) und ObsoleteSdkInt (hier nachweislich falsch, siehe 53).

Nebenbei behoben: der seit v52 gemeldete vorbestehende, unabhängige Compilefehler in :tasks (RecurrenceTest.kt:262, LocalDate.toIcsDate() ohne import org.beowulf.secretary.common.toIcsDate) — die :common-Fassung des Tests braucht den Import nicht, weil sie im selben Paket liegt, die :tasks-Fassung schon. Nur der fehlende Import ergänzt, kein Test angefasst. Damit läuft gradle test zum ersten Mal vollständig durch: 563 Tests grün über alle fünf Module (:common 75, :secrets 362, :calendar 55, :contacts 25, :tasks 46). versionCode 53 -> 54. Doc + App + Tests.

Status 2026-09-26 (53)

Secretary Secrets 53 (versionCode 53): UI/UX-Härtung (M3 der Produktions-Roadmap, todo.md) — kein Schema-Bruch, kein Format-Bruch. Localization bewusst zurückgestellt (Begründung unten).

(1) Keine !! mehr im App-Code. Die Suche fand zwei echte Stellen, die nicht im Plan standen: SecretMerge.decide hat plainOf(server, mk)!! abgefragt — nur sicher, weil identicalPlaintext an anderer Stelle zufällig false liefert, wenn nichts lesbar ist; jetzt ?: Decision.Server statt NPE. SecretsRemote.put hat eine MIME-Konstante zugesichert, jetzt toMediaType() in einem companion object. VaultViewModel.applyChanges gibt Boolean zurück, Browse schließt seinen Dialog nur bei Erfolg — eine Regelverletzung lässt ihn mit deutschem Grund offen.

(2) Eindeutige Geschwisternamen (SecretVault): addGroup, renameGroup und moveGroup prüfen getrimmt/case-insensitiv gegen die Ebenbürtigen und werfen DuplicateGroupNameException (mit Elternlabel für die Meldung). Dabei fiel ein latenter Bug auf: children() verstand nur eine von zwei Kodierungen für „Elternteil ist die Wurzel" — addGroup/moveGroup speichern parentUuid = null, der KDBX-Import (KdbxMapping.toVault) die explizite Wurzeluuid. Bei KDBX-importierten Tresoren lieferte children(rootGroup.uuid) deshalb leer, der Export schrieb alle direkten Wurzelkinder weg (aufgedeckt durch KdbxMappingTest.exportRebuildsEquivalentDatabase: 1 statt 2 Gruppen, 162 statt 340 Einträge). Beide Kodierungen werden jetzt gelesen, per Test festgenagelt.

(3) Suche (SecretSearch, rein/JVM-testbar, injiziertes reveal): Title/UserName/URL/Notes + Tags über entsiegelte Felder, ein bester Treffer je Eintrag, Titel-Treffer zuerst, Snippet mit Fundkontext, Deckel MAX_HITS. VaultViewModel debounced 300 ms (collectLatest verwirft eine laufende Suche); gesperrter Tresor → keine Treffer aus geschützten Feldern, mit Hinweis. Passwörter werden bewusst nicht durchsucht (ein Treffer wäre nutzlos und kopierte Klartext in die Ergebnisliste). Browse ersetzt die Baumansicht durch die flache Trefferliste, Klick öffnet den Eintrag.

(4) Import/Export-Fortschritt: KdbxMapping.ProgressListener (fun interface, default-inert) zählt über importKdbx/mergeIntoKdbx/ exportDatabase/exportContainer Gruppen und Einträge gemeinsam, monoton und nie über total. VaultViewModel.IoProgress + IoProgressBar (Material3-LinearProgressIndicator, Prozent, contentDescription) auf Import- und Export-Screen. Export warnt über KdbxMapping.EXPORT_SIZE_WARN_BYTES (5 MiB, im Wesentlichen Anhang-getrieben) und bleibt auf dem Screen, statt zurückzunavigieren. formatBytes/exportSizeWarning liegen bewusst im kdbx-Layer, damit sie ohne Android-Robolectric testbar sind.

(5) Sichtbarkeit je geschütztem Feld: FieldRow maskierte nur das Feld namens "Password"; API-Token und 2FA-Geheimnisse standen im Klartext in der Detailansicht. Jetzt f.protected als Kriterium, Aufklappen pro Feld (visibleFields, beim Verlassen verworfen).

(6) Snackbar doppelt auslösbar (messageSeq): LaunchedEffect hing am message-Text — dieselbe Meldung zweimal in Folge (z. B. zwei fehlgeschlagene Syncs) zeigte nur einen Snackbar. UiState.messageSeq zählt Mitteilungen, das Zeilen-Setzen läuft zentral über UiState.setMessage.

(7) Theming + Backup-Härtung (aus :secrets:lintDebug): Das Fenster-Theme war unbedingt @android:style/Theme.Material.Light.NoActionBar, obwohl Compose mit DarkColors zeichnete — Status-/Navigationsleiste blieben im Dunkelmodus hell. Neu Theme.Secrets mit values-night-Variante, windowBackground/statusBarColor/ navigationBarColor aus @color/window_background (identisch zu LightColors.background bzw. DarkColors.background). Und: allowBackup="false" unterbindet ab Android 12 nicht den Geräte-zu-Geräte-Transfer — ein Tresor wäre bei einem Gerätewechsel mitkopiert worden. Jetzt data_extraction_rules.xml (12+) und fullBackupContent (8..11) schließen alle Domains aus. Adaptive Icon um <monochrome> ergänzt.

Bewusst nicht umgesetzt: Localization. Es gibt kein values/- Verzeichnis, keine localeConfig und keine Übersetzung in eine zweite Sprache; 47 Strings über ~90 Aufrufe zu ziehen wäre reine Infrastruktur ohne sichtbaren Nutzen. Gehört zur ersten echten Zweitsprache, nicht davor. ExportedService auf dem gemeinsamen AccountAuthenticatorService bleibt trotz Lint: exported="true" ist Pflicht, damit der System-AccountManager binden kann, und eine Signatur-Berechtigung besitzt der Systemprozess nicht — sie würde die Kontoeinrichtung unbrauchbar machen; mit Begründung per tools:ignore unterdrückt. GradleDependency (navigation-compose 2.10.0) offline nicht behebbar. ObsoleteSdkInt (Ordner mipmap-anydpi-v26 → mipmap-anydpi) wurde ausprobiert und baut kaputt (AAPT: resource mipmap/ic_launcher not found) — der Lint-Hinweis ist hier falsch.

Tests: +29 JVM-Tests. Suite 344 Tests grün (:secrets, 315 → 344), 499 grün über alle Module mit Tests (:common 75, :secrets 344, :calendar 55, :contacts 25). SecretSearchTest neu (14), SecretVaultTest um 8 (Eindeutigkeit über anlegen/umbenennen/verschieben, Case/Trim, verschiedene Eltern erlaubt, children()-Regression für beide Wurzel-Kodierungen), KdbxMappingTest um 6 (Fortschritt monoton und nie über total, ohne Listener identisch, formatBytes mit deutschem Dezimalkomma, Schwellenwarnung). Lint: keine Befunde zu Accessibility, Backup oder Export; Lint-Build grün. versionCode 52 -> 53. Doc + App + Tests.

Status 2026-09-26 (52)

Secretary Secrets 52 (versionCode 52): Sync-Zuverlaessigkeit (M2 der Produktions-Roadmap, todo.md) — gleiche Datenformate, kein Schema-Bruch ausser dem Cache-Wrapper (v1 → v2, lesend kompatibel).

(1) Retry mit Backoff (SyncRetry, neu): jeder SecretsRemote-Aufruf (list/fetch/put/delete/createCollection) laeuft durch SyncRetry.withRetry: 4 Versuche, exponentielles Backoff 250 ms → max. 4 s plus voller Jitter (base * 2^attempt, gedeckelt, Jitter ≤ Cap/2). Retrybar sind Verbindungsabbruch (IOException) und 408/425/429/500/502/503/504. Nicht retrybar und unveraendert propagierend: 401/403/404/405/409/412 — die when-Reihenfolge ist entscheidend, weil DavException (android/common) IOException erweitert: mit vertauschter Reihenfolge waere jeder Serverfehler als Netzfehler retrybar. Der letzte Fehler wird unveraendert weitergereicht (kein Wrapping), damit Aufrufer DavException vs. IOException unterscheiden koennen. RetryPolicy ist konfigurierbar, SyncRetryTest deckt Statusklassifikation, Backoff-Kurve, Jitter-Klemmung und Weitergabe ab.

(2) Keine falsche Offline-Meldung (SecretSync.push): der push-Pfad fing nur IOException ab. Da DavException davon erbt, wurde ein echter Serverfehler (403 auf delete, 500 auf createCollection) als offline = true gemeldet — der Server war erreichbar — und die restliche Pending-Queue abgebrochen. Jetzt faengt push DavException vor IOException ab und protokolliert dav<code>:<href> unresolved, ohne die Queue zu stoppen. 404/410 auf delete gilt als erfuellt (idempotent). pull() verhielt sich bereits korrekt (Reihenfolge DavException → IOException).

(3) ETag-Fallback auf Body-Vergleich (SecretSync.pull): ETags gelten nur als Aenderungsnachweis, wenn der Server einen echten Validator liefert (isUsableEtag(): nicht leer/null/*). Liefert der Server fuer list und fetch denselben unbrauchbaren Wert, waere existing.etag == node.etag dauerhaft wahr — der Cache waere still fuer immer veraltet und serverseitige Aenderungen kaemen nie an. Dann greift der Body-Vergleich (FR-S-231). Ein Content-Hash als Conditional-Request-Validator ist bewusst nicht umgesetzt: ein serverberechneter ETag laesst sich nicht clientseitig fabrizieren, der Fallback bleibt auf dem Lesepfad.

(4) Cache-Integritaet (SecretOfflineStore v2): {"v":2,"sha256":…, "payload":{…}}, SHA-256 ueber die kanonische Serialisierung. encode ist eine reine Funktion des Snapshots mit fester Schluesselreihenfolge und sortierten Maps, damit parse → encode ein Fixpoint ist (Regressionstest). Ein halb geschriebener oder von aussen veraenderter Cache faellt auf, statt still einen halben Tresor zu liefern. atomicWrite fsynct jetzt, damit ein Powerloss die Pending-Queue nicht verliert.

(5) Kein stiller Datenverlust (Kernfix): load() schrieb zuvor bei unlesbarem Cache einen frischen, leeren Snapshot ueber die Datei — nicht hochgeladene Pending-Aktionen waren danach endgueltig weg. Jetzt wird das Original nach <datei>.corrupt quarantaniert und als CacheIssue (Corrupt/Integrity/FutureVersion, inkl. Quarantäne-Pfad) gemeldet; die App zeigt eine Warnung (CacheIssueBanner) mit Dateinamen, statt einen leeren Tresor als normalen Zustand zu praesentieren. V1-Caches (flach, ohne Pruefsumme) lesen weiterhin und werden beim naechsten save auf v2 gehoben.

(6) Sync-Fortschritt: SecretSync.SyncProgress(phase, done, total) mit SCANNING/FETCHING/PUSHING/PULLING als optionaler Callback (SecretSync(..., progress = {}), default-inert, damit bestehende Aufrufer unveraendert kompilieren). UiState.progress fuehrt ihn, der Header zeigt ihn als Subtitle. Ein linearer Material3-Indikator fehlt noch (Text + Spinner decken es vorlaeufig).

Bewusst nicht umgesetzt: Zwei-Phasen-Push (HEAD-Dry-Run) kann keine Atomaritaet liefern — der Server kennt keine Transaktion, ein Abbruch zwischen zwei PUTs laesst den Tresor halb aktualisiert zurueck, waehrend die HEADs jeden Request verdoppeln. Die Pending-Queue liefert bereits die relevante Eigenschaft (Idempotenz + Wiederaufsetzbarkeit). Ein getrenntes conflictQueue wuerde eine zweite Wahrheit neben der bereits persistierenden, FIFO-haltenden Queue modellieren; der naechste sinnvolle Schritt ist ein explizites resolveConflict(keepLocal/useServer) statt eines zweiten Zustands.

Tests: +19 JVM-Tests. Suite 315 Tests grün (:secrets, 296 → 315): SyncRetryTest neu (8), SecretOfflineStoreTest erweitert (Quarantäne, Pruefsummenmanipulation, v1→v2, Idempotenz, Issue-Reset), SecretSyncTest erweitert (403-ist-kein-Netzfehler, idempotentes Löschen, Retry im Pull, Offline bleibt Offline, ETag-loser Server erkennt Änderungen, Fortschritt). Drei davon wurden gegen den zurückgedrehten Fix verifiziert (schlagen fehl, wenn der alte Code wieder aktiv ist). Hinweis: :tasks hat weiterhin einen vorbestehenden, unabhängigen Compilefehler (RecurrenceTest.kt:262 ohne import …toIcsDate). versionCode 51 -> 52. Doc + App + Tests.

Status 2026-09-26 (51)

Secretary Secrets 51 (versionCode 51): Krypto-Härtung (M1 der Produktions-Roadmap, todo.md) — kein Schema-Bruch, kein Format-Bruch.

(1) Envelope-Versionierung (SecretsCipher): VERSION = 1, ALG = "aes-256-gcm" und versionOf(prefix) machen das Präfix zum Versions-Marker. unsealWith akzeptiert jede formal korrekte Version einer bekannten Familie (ss1:, rk1:, dk1:, dp1: — die Wrap-Familien teilen das base64(iv(12) ‖ AES-256-GCM(ct))-Layout weiterhin bewusst), ein unbekanntes oder unlesbares Präfix fällt als UnsupportedEnvelopeVersionException auf. Neue getippte Fehler: SecretsCipherException (Basis, weiterhin IllegalArgumentException, damit bestehende Aufrufer nicht brechen) → MalformedEnvelopeException / UnsupportedEnvelopeVersionException / WrongKeyException; ein GCM-Tag-Fehler ist jetzt WrongKeyException statt einer generischen IllegalStateException.

(2) MK-Lebenszyklus (VaultKeyState): unlock() übernimmt eine Kopie, masterKey() gibt eine Kopie heraus — der Aufrufer kann den verwalteten Schlüssel weder kippen noch nach lock() weiterverwenden. lock() verwischt den verwalteten Puffer wie bisher (FR-S-203). unlock() verwischt einen restlichen alten Wert beim Wechsel.

(3) Keystore-Fallback ohne stillen Downgrade (DeviceKeyStore, DeviceKeyWrap): ist kein Android-Keystore nutzbar, wirft wrapMasterKey jetzt KeystoreUnavailableException, statt auf einen extrahierbaren Software-Schlüssel zurückzufallen. Der schwächere Passphrase-Weg ist explizit: wrapMasterKeyWithPassphrase(passphrase) → DeviceKeyPassphraseCipher (scrypt N=2^20/r=8/p=1/dkLen=32 über UTF-8-Passphrase, AES-256-GCM, dp1:-Envelope, frisches 16-B-Salt + frische IV je Seal, kein AAD; abgeleiteter Schlüssel und Zwischenpuffer werden verwischt). Der Datensatz trägt protection (KEYSTORE | PASSPHRASE | UNKNOWN) plus scrypt/salt nur im Passphrase-Fall; fromJson erzwingt die Präfix-Zuordnung (PASSPHRASE ⇒ dp1:, KEYSTORE ⇒ dk1:) gegen Verwechslung, DeviceKeyStore.protection() liefert den Schutzgrad an die UI.

(4) Bestandskompatibilität: device-key.json ohne protection-Feld liest als UNKNOWN und wird wie KEYSTORE behandelt — bestehende Tresore entsperren nach dem Update unverändert. Das DeviceKeyRecord-Wire-Format für Keystore-Datensätze bleibt unverändert (kein scrypt/salt).

(5) App (VaultViewModel): unlock() bleibt lenient für Bestandsdatensätze, neu unlockWithPassphrase(passphrase) für Passphrase-geschützte Tresore; der Frisch-Import fängt KeystoreUnavailableException ab und meldet „Keystore nicht verfügbar — Tresor bitte mit Passphrase schützen", ohne den Import abzubrechen (der MK lebt bis dahin nur im RAM).

Tests: +32 JVM-Tests (SecretsCipherTest: Präfix/Version/Layout/ Typisierte-Fehler, DeviceKeyPassphraseCipherTest neu mit 17 Fällen, VaultKeyStateTest: Wipe per Reflection, Aliasing, Kopie-Semantik). Suite 296 Tests grün (:secrets), 451 grün über alle Module mit Tests (:common 75, :secrets 296, :calendar 55, :contacts 25). Hinweis: :tasks hat einen vorbestehenden, unabhängigen Compilefehler (RecurrenceTest.kt:262 ohne import org.beowulf.secretary.common.toIcsDate), unabhängig von dieser Änderung. versionCode 50 -> 51. Doc + App + Tests.

Status 2026-09-25 (50)

Secretary Secrets 50 (versionCode 50): Gruppen-Namen werden beim Anlegen einer Collection an den Server geschickt (MKCOL-Displayname). CaldavClient.mkcol(href, displayName) sendet jetzt einen echten DAV:mkcol-Body mit D:resourcetype/collection und D:displayname (XML-escaped, Content-Type: application/xml) statt eines leeren Requests; SecretsRemote.createCollection nimmt den Namen entgegen. SecretAction.CreateGroup persistiert das neue optionale Feld displayName (nur bei nicht-leerem Namen), SecretSync.push reicht den Gruppennamen aus dem Vault durch, und pendingLocalEdits/upSyncPending führen Collection-Namen mit, damit renamed/verschobene Gruppen ihren Namen behalten. Queue-Kompatibilität: bereits gequeued CreateGroup-Aktionen ohne displayName (altes Schema) laden weiterhin ("" als Default) und bekommen ihren Namen beim Push aus dem Offline-Snapshot (SecretMeta für die Wurzel, SecretGroupJournal für Gruppen, beide Cache-Key-Formen); erst danach greift der Href-Segment-Fallback. 5 neue/angepasste JVM-Tests (CaldavClientTest MKCOL-Body, SecretSyncTest Displayname-Propagation

Status 2026-09-22 (48)

Secretary Secrets 48 (versionCode 48): **Eintrag-Editor KeePass-kompatibel

Status 2026-09-22 (47)

Secretary Secrets 47 (versionCode 47): Home bereinigt: Sync in den Kopfzeilen-Topbar. (1) Die SyncStatusCard (Verbindungsstatus, "n ausstehend"-Badge, Konto-Knopf) ist vom Hauptbildschirm entfernt — die Verbindungs- und Kontoverwaltung gehört in die Einstellungen (Konto-Screen), nicht auf die Tresor-Startseite. (2) Der Sync-Knopf ist jetzt in der ScreenHeader-Aktion des Home untergebracht: ein TonalIconButton mit dem Sync-Symbol neben den Einstellungen (Spinner solange state.syncing, deaktiviert ohne verbundenes Konto); die Snackbar meldet weiterhin das Ergebnis (FR-S-230..236 unverändert). Reiner UI-Bereich, Sync-Orchestrierung/UiState bleiben unverändert. JVM-Suite unveraendert 257 Tests gruen (verifiziert ueber compileDebugKotlin + testDebugUnitTest). versionCode 46 -> 47. Doc + App.

Status 2026-09-21 (46)

Secretary Secrets 46 (versionCode 46): KDBX-Bag: K2-Erweiterungen verlustfrei (FR-K-65) — Eintrags-Icons/-Farben, OverrideUrl, QualityCheck, AutoType (inkl. Associations), unbekannte XML-Subtrees, die Zeit-Extras (LastAccess/Expiry/Expires/UsageCount/LocationChanged) sowie die Gruppen-Erweiterungen (DefaultAutoTypeSequence, EnableAutoType/EnableSearching, LastTopVisibleEntry, PreviousParentGroup, CustomData-Extras) gehen nicht mehr als Warnungen verloren, sondern werden als opaker, kompakter kanonischer JSON-Bag (KdbxBag) auf SecretEntry.kdbxBag/SecretGroup.kdbxBag mitgeführt. Wire-Form: X-SECRETS-BAG;ENCODING=json am Eintrag bzw. X-SECRETS-GROUP-BAG;ENCODING=json an der Gruppe (Escaped/Folded), die History-Snapshots tragen den Bag im kanonischen JSON, der Offline-Cache über die VJOURNAL-/group.ics-Codecs (kein Schema-Bruch). Es werden nur Nicht-Default-Werte gespeichert: Import → Export ist verlustfrei, Tresor-eigene Einträge (ohne Bag) exportieren kanonische Defaults.

KdbxMapping: Import baut den Bag pro Eintrag/Gruppe (fehlende timesBag-Brücke und die KdbxBag-Datei waren der noch freie Teil des WIP), Export wendet ihn wieder an — Eintrag (Icon/CustomIcon/Farben/ OverrideUrl/QualityCheck/AutoType/Unknown/Zeiten) UND Gruppe (DefaultAutoTypeSequence/AutoType/Searching/LastTopVisible/PreviousParent/ Unknown/Zeiten). SecretMerge: mergeOpaque merged den Bag mit Feld-Semantik (§8): einseitige Änderung gewinnt, beidseitig gleich → Server, beidseitig verschieden → bag-Konflikt (Dokument hält den Server-Stand, FR-S-234), einseitiges Entfernen verwirft. Warnings bleiben nur für das nicht Tragbare (History-Snapshots mit Anhängen, Meta-CustomData-Extras).

5 neue JVM-Tests: KdbxMappingTest K2-Roundtrip (Bag → Export → gleiche Icons/Farben/AutoType/Unknown/Zeiten, ersetzt den alten Warn-Fall) + assertEquivalent prüft jetzt auch die K2-Fläche der Einträge; SecretMergeTest 4 Bag-Fälle (einseitig, gleich, Konflikt, Entfernung). Suite 257 Tests grün (253 → 257). versionCode 45 -> 46. Doc + App + Tests.

Status 2026-09-21 (45)

Secretary Secrets 45 (versionCode 45): Bugfix: Import-Vorschau/**Tresor nach KDBX-Import nutzlos — nach dem Import erschien ein einziger leerer Root-Knoten statt der echten Gruppen/Eintraege. Root-Cause: Die Root-Identitaet ging am Snapshot-Uebergang verloren — beim KDBX-Import ist vault.uuid ein frischer Wert, aber rootGroup.uuid ist die KDBX-Root-uuid (secrets-meta.ics traegt standardmaessig KEINE Root-Gruppe, SecretMetaJournal schrieb nur null). buildVault konstruierte dann eine fabrizierte Root "DataBase" mit dem falschen Wert (fehlender meta.rootGroup) — die Kinder zeigen aber per parentUuid/groupUuid auf die echte KDBX-Root-uuid → groupChildren()/ groupEntries() verglichen Root-uuid != KDBX-Root-uuid → 0 Gruppen, 0 Eintraege unter der Root, der Baum war nur noch ueber die Move-/Path- Interna erreichbar (reproduziert: Test.kdbx, 162 Root-Eintraege + 13 Gruppen → REBUILT ROOT 0/0).

Fix: snapshotFromVault persistiert die Root-Gruppe (uuid + Name) immer in secrets-meta.ics (meta.copy(rootGroup = vault.rootGroup)); buildVault gewinnt damit die echte Root-uuid + -Name zurueck und legt Meta ggf. neu an. Zusaetzlich Härtung für Alt-Caches (vor v44): inferRootUuid leitet die Root-uuid aus den Kindern ab (Root-Eintraege / oberste Gruppen zeigen auf eine uuid, die keiner bekannten Gruppe gehoert — das ist der Root-Ordner), Name aus KDBX.DatabaseName. Browse zeigt jetzt im Root-Header den echten Tresor-Namen statt hartem "Tresor". Nach dem Fix: Test.kdbx-Rebuild liefert Root beowulf mit 13 Gruppen + 162 Eintraegen direkt unter der Root wie das Original. 3 neue JVM-Regressions-Tests (VaultSnapshotRoundTripTest): Import-Root-Identitaet uebersteht den Snapshot (uuid/Name Kinder-Bindung), Alt-Cache ohne meta.rootGroup wird geheilt, Bucketing unveraendert. Suite 253 Tests gruen. versionCode 44 -> 45. Doc + App + Tests.

Status 2026-09-21 (44)

Secretary Secrets 44 (versionCode 44): **DAV-Sync-Einbindung — Konto-Setup

Sync-Orchestrierung (SecretSync.sync(), FR-S-230..236): sync() läuft als Up-Sync vor Pull, damit ein frisch importierter Tresor nicht beim ersten Pull als "Server-Gelöschtes" geprunt wird (pull() prunet lokale Ressourcen, die der Server nicht listet — bei leerem Server wäre das sonst ein Datenverlust): ① BFS über die Collections (listRemoteTree) liefert alle serverseitigen Hrefs; ② das reine, JVM-testbare upSyncPending reicht für jede lokal vorhandene, auf dem Server nicht gelistete Ressource einen PutResource nach (fehlende Gruppen-Collections als CreateGroup vor den Puts) ohne die schon bestehende Pending-Queue anzutasten; ③ push(mk) spielt die Queue inkl. der neuen Aktionen aus (mkcol-405, conditional Put, 412→Merge/Conflict); ④ pull() holt Serverseitiges/ETags. Ergebnis als SyncReport(offline, createdCollections, flushedPuts, deletedResources, conflicts, unresolved, skipped). Neuer SecretsRepository (Spiegel von NotesRepository): Credentials im gemeinsamen secretary-Account via AccountResolver, CaldavClient(baseCollectionPath = SecretWire.ROOT).

App: VaultViewModel neu mit login(url,user,pass) (speichert Konto + führt sofort ersten Sync), logout(), refreshAccount() (MainActivity. onResume, Konto-Änderungen über AccountManager werden übernommen) und sync() (MK nur entsiegelt, wenn geraetegebundener Wrap vorliegt oder der Tresor entsperrt ist — syncMasterKeyOrNull, sonst locked:-Unresolved). UiState um connected/syncing/pendingCount/offline/server/accountUser erweitert; commitImport startet nach einem Import automatisch den Up-Sync (frisch importierter Tresor landet beim ersten Sync auf dem Server).

UI: Home zeigt eine SyncStatusCard (Verbindungsstatus, "n ausstehend"- Badge aus der Pending-Queue, Offline-Hinweis, Synchronisieren-/Konto-Knopf; ohne Konto "Konto verbinden"). Neuer Konto-Screen (Route account) über das gemeinsame SecretaryLoginScreen (Login mit Server/User/Passwort) bzw. Konto-Info + Synchronisieren + Abmelden. Die Einstellungen nutzen die gemeinsame SecretarySettingsScreen (Konto-Sektion, Namensraum /secrets/, Abmelden) mit Import/Export-nun als content. Snackbar verdrahtet UiState.message (Sync-Bestätigungen/Fehler). AndroidManifest: INTERNET, GET_ACCOUNTS, READ_ACCOUNT, usesCleartextTraffic, gemeinsamer AccountAuthenticatorService.

Tests: 5 neue JVM-Tests (SecretSyncTest): syncUploadsFreshLocalVaultToEmptyServer (2 Collections + 5 Puts, Pending leer danach), syncSecondRunIsNoOpWhenServerMatches (kein zweiter Write), syncGoesOfflineWhenServerUnreachable (Cache unberührt), syncKeepsServerAuthoritativeWhenServerHasChangesWithoutPending (Pull übernimmt Server, Up-Sync lädt nur lokal fehlende), syncSurfacesRealPushConflictAndKeepsPending (412→Merge-Konflikt bleibt Pending, Server-Stand im Dokument). Suite 251 Tests grün. versionCode 43 -> 44. Doc + App + Tests.

Status 2026-09-20 (43)

Secretary Secrets 43 (versionCode 43): Import-Vorschau statt Warn-Flut. Der Import war korrekt (echter KeePassXC-Test.kdbx: 14 Gruppen, 340 Eintraege komplett uebernommen inkl. verschluesselter Passwort-Felder, inkl. der im KDBX vorhandenen History-Snapshots in den Eintraegen), aber die PreviewCard listete alle per-Eintrag-Hinweise einzeln auf (Icons, Farben, overrideUrl, AutoType — reine K2-Praesentations-Luecken des Tresor- Modells, FR-K-65) → die Vorschau sah aus wie "alles nicht uebernommen". Jetzt aggregiert die Karte die Hinweise nach Grund mit Anzahl (z. B. "entry foregroundColor not carried — 340x") in absteigender Haeufigkeit, Konflikte bleiben einzeln; die Zusammenfassung nennt nur noch die Kleinzahl der Hinweise, nicht mehr die Zahl als Schockwert. Reiner UI-Fix, Mapping unveraendert. JVM-Suite unveraendert 245 Tests gruen. versionCode 42 -> 43. App.

Status 2026-09-20 (42)

Secretary Secrets 42 (versionCode 42): Import-Crash durch verschachtelte Scroll-Container behoben (der Crash von 41 trat bei der Vorschau auf): PreviewCard hatte ein eigenes verticalScroll-Column innerhalb des bereits scrollenden Import-Columns → Compose-IllegalStateException ("Vertically scrollable component was measured with an infinity maximum height constraints"). Das innere Column scrollt nicht mehr; die Vorschau laesst sich wie der Rest des Screens im aussen Column scrollen. (Der erste Crash-Report von 41 liegt bereits in crashreports/ und bestaetigt den CrashReporter aus 38 auf dem Geraet.) JVM-Suite unveraendert 245 Tests gruen. versionCode 41 -> 42. App + Doc.

Status 2026-09-20 (41)

Secretary Secrets 41 (versionCode 41): Passwortfelder mit Sichtbarkeits-Umschalter. Import („Passwort") und Export („Neues Passwort") tragen jetzt einen Eye-Icon-Button (Visibility/VisibilityOff) zum Anzeigen/Verbergen — verdaechtige "header MAC mismatch"-Fehler (meist falsches KDBX-Passwort) lassen sich damit sichtbar pruefen statt zu raten. Nebeneffekt von 40: der frische Import wuerde beim ersten KDBX- Versuch direkt den MAC-Fehler zeigen, wenn das eingegebene Passwort nicht stimmt. JVM-Suite unveraendert 245 Tests gruen. versionCode 40 -> 41. App + Doc.

Status 2026-09-20 (40)

Secretary Secrets 40 (versionCode 40): Frisch-Import erzeugt den Master-Key statt ihn zu entsiegeln. Bug: previewImport rief currentMasterKey() (Entsiegeln des geraetegebundenen dk1:-Wraps via DeviceKeyStore.unwrapMasterKey) vor der Frisch-Import-Branch ab — auf einem Geraet ohne Tresor (leerer Offline-Cache, kein device-key.json) warf das "no device-key record present" und der erste KDBX-Import crashte. Jetzt wird der Master-Key nur bei einem Merge-Bestand (bestehender Tresor) entsiegelt; der Frisch-Import erzeugt einen neuen 32-B-Master-Key (SecretsCipher.newMasterKey), versiegelt ihn sofort als dk1:-Record (DeviceKeyStore.wrapMasterKey, keine bedingte hasWrappedRecord-Pruefung mehr — bei fehlendem Record wird er immer geschrieben) und entsperrt damit. JVM-Suite unveraendert 245 Tests gruen. versionCode 39 -> 40. App + Doc.

Status 2026-09-20 (39)

Secretary Secrets 39 (versionCode 39): Startup-Fix + Import-Files-Fix. (1) 38 hatte einen Startup-Crash (ClassNotFoundException für SecretarySecretsApp): die beiden neuen Quelldateien SecretarySecretsApp.kt und crash/CrashReporter.kt waren beim Commit nie fossil added worden (nur der Manifest-Verweis .SecretarySecretsApp war unter Versionierung) — der Container-Build aus dem sauberen Fossil-Checkout hatte die Klasse also gar nicht im APK. Beide Dateien sind jetzt fest eingecheckt; das gebaute APK enthaelt die Application-Klasse samt CrashReporter. (2) Import-Flow fix: der Flie-Name wurde aus dem Content-Resolver-Cursor gelesen, ohne moveToFirst() → der Rückgabewert des SAF-Dateipickes crashte mit CursorIndexOutOfBoundsException: Index -1. Jetzt if (c.moveToFirst()) vor dem getString(DISPLAY_NAME). JVM-Suite unveraendert 245 Tests gruen. versionCode 38 -> 39. App + Doc.

Status 2026-09-20 (38)

Secretary Secrets 38 (versionCode 38): Crash-Reporting + App-Name "Secrets". (1) Neuer crash/CrashReporter (Spiegel von Contacts): globaler UncaughtExceptionHandler schreibt bei jedem Absturz synchron einen JSON-Report (id, Zeit, app, versionName/Code, debuggable, Thread, Thread-Dump, OS/API, Gerät, ABIs, Speicher, Exception-Kette) in filesDir/crashreports/ und per MediaStore (API 29+) nach Download/ (kein Permissions-Aufwand) und ruft danach den alten Handler weiter — der Prozess stirbt wie gewohnt. Installation über die neue Application-Klasse SecretarySecretsApp (android:name=".SecretarySecretsApp"). (2) App-Name in Android/Launcher ist jetzt nur noch "Secrets" (android:label + ScreenHeader-Untertitel), statt "Secretary Secrets"; PRODID hält weiter "Secretary Secrets" (interop-relevanter Produkt-Identifier). JVM-Suite unveraendert 245 Tests gruen. versionCode 37 -> 38. App + Doc.

Status 2026-09-20 (37)

Secretary Secrets 37 (versionCode 37): Secretary-Look + Import/Export in die Einstellungen + Import fragt erst nach der Datei. (1) Das App-Theme ist jetzt das gemeinsame Familien-Theme (secretaryTypography() mit der Typewriter-Schrift Special Elite, Palette/Farben wie Contacts/Notes/Links, dynamische Material-3-Farben ab Android 12); die Screens nutzen den gemeinsamen ScreenHeader (Wash-Hintergrund, Untertitel, Aktionen) statt der nackten TopAppBar-Skafolds — Home trägt im Header die Einstellungen-Aktion. (2) Import/Export wandern von der Home-Seite in einen neuen Einstellungen-Screen (Route settings): zwei Karten "KDBX-Datei importieren" (mit Merge-/Frisch-Hinweis) und "Als KDBX exportieren". (3) Der Import-Flow wurde repariert: Beim Öffnen der Import-Seite startet sofort der SAF-Dateidialog (die Datei wird zuerst gewählt), erst danach erscheinen Passwortfeld und Vorschau — nicht mehr umgekehrt. JVM-Suite unveraendert 245 Tests gruen. versionCode 36 -> 37. App + Doc.

Status 2026-09-20 (36)

Secretary Secrets 36 (versionCode 36): Erster Release-Build ueber das Buildsystem. Seit v15 wurde kein APK mehr gebaut/ausgeliefert (v16–v35 nur lokal als Debug-Assemble); diese Version ist der Stand von v35 (volles Eintrags-/Gruppen-CRUD, KDBX-Bruecke, Pending-Queue) ohne Funktionsaenderung, gebaut als secretary-secrets-36.apk und auf tremere hochgeladen. JVM-Suite unveraendert 245 Tests gruen. versionCode 35 -> 36. Build + App.

Secretary Secrets 35 (versionCode 35): Volles Eintrags-Management in der App — Browse ergaenzt um komplette CRUD-Drehungen, alle als lokale SecretVault-Mutation ueber applyChanges (Pending-Queue via pendingLocalEdits, siehe Status 34): Eintrag anlegen (vier Standard-Felder, in der aktuellen Gruppe bzw. Root), bearbeiten, verschieben (Dialog listet Root + alle Gruppen, moveEntry setzt previousParentGroupUuid), loeschen (Ledger-Tombstone via removeEntry); Gruppe anlegen (in aktueller Gruppe/Root), umbenennen (UUID stabil, FR-S-214) und loeschen (rekursiv mit Ledger-Tombstones, FR-S-218) ueber das Gruppen-UeberlaufMenu. VaultViewModel neu: createEntry, removeEntry, moveEntry, addGroup, renameGroup, removeGroup — jede als applyChanges(message) { patch }, der MK wird nur per currentMasterKey() zur Feld-Versiegelung im RAM angefragt. JVM-Suite unveraendert 245 Tests gruen (kein neues JVM-Verhalten, App-Erweiterung), Debug-APK assembliert. versionCode 34 -> 35. Doc + App.

Status 2026-09-20 (34)

Secretary Secrets 34 (versionCode 34): Tresor durchsuchen + Eintrag bearbeiten — Home zeigt jetzt den Gruppen-Baum aus dem Offline-Cache (Group -> Untergruppen/Einträge, Zurueck-Navigation), Eintrag-Detail listet die Felder in seq-Reihenfolge (Passwort maskiert mit Anzeigen/Verbergen, entsiegelt nur im RAM via VaultViewModel.reveal) und ein Bearbeiten-Dialog editiert die vier Standard-Felder Title/UserName/Password/Notes. Aenderungen laufen als lokale Drehung ueber SecretVault.putEntry und werden im Offline-Cache persistiert; neu dazu kam SecretSync.pendingLocalEdits (Inverses-Muster zu pending/push): alte Pending-Aktionen auf betroffenen Hrefs werden verworfen, dann aufgereiht — CreateGroup (vor den eigenen Ressourcen) bei neuen Gruppen, PutResource mit bisherigem ETag (leer bei Neu) fuer geaenderte/neue Gruppen und Eintraege, DeleteResource fuer geloeschte und bewegte (Delete alt + Put neu). 4 JVM-Tests (PendingLocalEditsTest): CreateGroup-Reihenfolge + leere ETags, geaenderter Eintrag behaelt ETag, Loeschen verwirft stale Put und behaelt unbetroffene Queue, Move = Delete(alt)+Put(neu). Suite 245 Tests gruen, Debug-APK assembliert. versionCode 33 -> 34. Doc + App + Tests.

Status 2026-09-20 (33)

Secretary Secrets 33 (versionCode 33): App-Einbindung der KDBX-Brücke (Material3, SAF) — erster lauffähiger Compose-Shell (wayland-Laptop/Handy, Material3, Kotlin-UI-Toolkit der neuesten Generation): AndroidManifest mit Launcher-MainActivity, Paket app/ mit VaultViewModel und ui/SecretsApp. VaultViewModel orchestriert den kompletten Import/Export/ Merge-Fluss (FR-K-60..90) über die vorhandenen Schichten: MK nur im RAM (VaultKeyState), geraetegebunden als dk1:-Wrap im Android-Keystore (DeviceKeyStore), Tresor-Zustand im SecretOfflineStore; neu hinzu kam SecretSync.snapshotFromVault (Inverses von buildVault): schreibt einen SecretVault als Snapshot (VJOURNAL/group.ics/secrets-meta.ics) und traegt Pending-Queue + fremde Ressourcen weiter. Drei Material3-Screens mit Navigation: Home (Tresor-Status, Eintraege-/Gruppenzaehler, Sperren/Entsperren), Import (KDBX via SAF OpenDocument, Passwort, Vorschau = importContainer frisch bzw. mergeContainer mit Konflikt/Warn-Liste, Bestaetigung persistiert via snapshotFromVault), Export (SAF CreateDocument, exportContainer). Der Export meldet Warnings statt stiller Verluste. 2 neue JVM-Tests (VaultSnapshotRoundTripTest): SecretVault ⇔ Snapshot-Roundtrip inkl. Protected-Felder/Meta/Ledger, Pending-/Attachment-Weitertrag und Root-Bucketierung. Suite 241 Tests gruen, Debug-APK assembliert (22,5 MB). versionCode 32 -> 33. Doc + App + Tests.

Status 2026-09-20 (32)

Secretary Secrets 32 (versionCode 32): Import-Merge in einen bestehenden Tresor (mergeIntoKdbx, FR-K-63/67/76/77/79, §8) — reines, JVM-testbares Orchestrierungs-Stück der KdbxMapping-Brücke. Mergt eine geöffnete KdbxDatabase per UUID-Identität in einen bestehenden SecretVault: gruppenweise Baum-Union (unbekannte UUIDs werden angehängt, existierende Gruppen bleiben, Unterschiede bei Name/Notes/IconID/ CustomIconUUID/Tags/CustomData → ImportConflict(kind="group")), eintragsweise Drei-Wege-Merge über SecretMerge mit leerer Basis (importierte Seite = server, bestehende = client, FR-K-76): identische Plaintext-Werte werden übernommen und als Protected-Feld frisch versiegelt, einseitig Geändertes gewonnen, beidseitig Geändertes → Konflikt (entry-field), das Dokument hält den Import-Stand (FR-K-77/§8). History als set union (nichts geht verloren, FR-K-79), per Import erfasste Attachment-/History-Anzahlen im Summary. Die Import-Root wird auf die Tresor-Root umverparentet (groupUuid/PreviousParentGroup), Meta (customIcons, DeletedObjects-Ledger, Skalare) ist die Union mit Tresor-Vorrang, der Vault-Master-Key bleibt unberührt (FR-K-67). importContainer/mergeContainer öffnen die KDBX-Container-Bytes mit echten Credentials. Ergebnis als ImportMergeResult mit ImportMergeSummary (newGroups, newEntries, mergedEntries, importedAttachments, importedHistoryItems, conflicts, warnings). 7 JVM-Tests (KdbxMappingMergeTest): Neuzugänge + einseitige Änderungen ohne Konflikt, Feld-Konflikt-Meldung (Importwert gewinnt), History-Vereinigung verlustfrei, Root-Umverparentung inkl. verschachtelter Gruppe, Gruppen-Kollision behält Tresor-Gruppe, MK-/Protected-Field-Integrität nach Merge, Merge über echte Container-Bytes. Suite 239 Tests grün. versionCode 31 -> 32. Doc + Tests.

Status 2026-09-20 (31)

Secretary Secrets 31 (versionCode 31): KDBX ⇔ Tresor-Brücke (KdbxMapping, §4.6/4.7, FR-K-60..75/90) — reine, JVM-testbare Kopplung des KDBX-I/O (§28/30) an das kanonische Tresor-Modell. importKdbx bildet eine KdbxDatabase auf Gruppenbaum + Einträge ab: geschützte Werte werden als ss1:-Envelopes mit dem Vault-Master-Key neu versiegelt (FR-K-66, nie Klartext im Modell), Feld-Reihenfolge bleibt erhalten (seq), Tags/Zeiten (createdRaw/modifiedRaw, iCal-UTC), PreviousParentGroup, Anhänge werden content-adressiert (SHA-256) versiegelt und inline gehalten, die komplette History als SecretHistory.sealedJson-Blob mit HistoryCount. Gruppen behalten UUID, Name, Notes, IconID/CustomIconUUID, Parent, Times, Expanded, Tags und CustomData; Meta wird über einen KDBX.*-Skalar-Namespace verlustfrei transportiert (DatabaseName/Description, DefaultUserName, Color, MemoryProtection, MasterKeyChanged, RecycleBin, History-Limits, SettingsChanged, CustomData, unbekannte XML-Subtrees als FR-K-29-Bag) plus customIcons (uuid → base64) und das DeletedObjects-Ledger. exportDatabase baut die KdbxDatabase zurück: geschützte Felder werden mit dem MK entschlüsselt und als normale KDBX-Inner-Stream-Protected-Strings geschrieben (FR-K-74), Anhänge aus den Envelopes in den Binär-Pool, History Item für Item wiederhergestellt, Meta aus den Skalaren rekonstruiert. Warnungen statt stiller Verluste (FR-K-65): Modell-Lücken (Eintrags-Icon/-Farben, AutoType, OverrideUrl, QualityCheck, unbekannte Eintrags-XML, CustomData-Extras, History-Snapshots mit Anhängen) werden als Warnings gemeldet, nichts wird unbemerkt verworfen. toVault-Assembly, importContainer/exportContainer als Convenience. 8 JVM-Tests (KdbxMappingTest, u. a. der echte KeePassXC-Test.kdbx-Roundtrip: 162 Einträge gehen verlustfrei durch Import→Export): FR-K-66-Sealing, Gruppen/ Meta-Mapping, Anhänge+History, äquivalente Re-Erzeugung, Container-Roundtrip verlustfrei, Warn-Generation, ss1:→Protected-String-Export. Suite 232 Tests grün. versionCode 30 -> 31. Doc + Tests.

Status 2026-09-20 (30)

Secretary Secrets 30 (versionCode 30): Reverse-Interop verifiziert (FR-K-30) + XML-Dialekt-Bugfixes gegen echte KeePassXC. Neuer KeePassXCOpenKeePassXCInteropTest (2 JVM-Tests, übersprungen ohne keepassxc-cli): unsere KdbxContainer.save-Datei wird per keepassxc-cli ls/export geöffnet — Gruppe/Einträge/History gelistet, und das von KeePassXC wieder exportierte XML enthält Tresor-/Eintragsnamen, geschützte Werte (Passwort/Title/UserName/URL), History-Passwort, Attachment-<Binary Ref>-Referenz, Custom-Icon + CustomIconUUID, Auto-Type-DefaultSequence und Meta-CustomData; ohne skipCurrentElement/ Crypto-Fehler (FR-K-30). Fund & Fix dabei – drei echte XML-Dialekt-Bugs des Writers (und Reader-Verweigerung für moderne Schreibweisen): ① geschützte Werte schrieb ProtectInMemory="True", KeePassXC ehrt aber nur Protected="True" (werte kamen als Roh-Base64 durch); ② AutoType-Associations kamen in der Legacy-Hülle <Associations> (KeePassXC skippt sie mit Warnung) — jetzt <Association> direkt unter <AutoType> wie im keepassxc-specs-XSD; Reader liest beide Formen; ③ CustomIcons wurden als <Item> statt <Icon> geschrieben (wurden kommentarlos verworfen) — Reader akzeptiert <Icon> und <Item>. Bereinigt wird auch der Salsa20-Inner-Stream: KeePassXC leitet den Stream-Key mit SHA-256 ab (CryptoHash::hash(key, Sha256)), wir nutzten das rohe Key. Suite 224 Tests grün (Interop-Test schlägt bei Abwesenheit von KeePassXC nicht fehl). versionCode 29 -> 30. Doc + Tests.

Status 2026-09-20 (29)

Secretary Secrets 29 (versionCode 29): Key-File-Unterstützung (KdbxKeyFile, FR-K-09) — exakt implementiert nach KeePassXC FileKey::load: Erkennungsreihenfolge ① KeePass-2-XML-Keyfile (Root <KeyFile>) mit Version 1.00 (Base64-<Data>, Typ KeePass2Xml) und 2.0 (Hex-<Data> + Hash- Attribut = SHA-256(Data)·0..3, Checksumme wird verifiziert, Typ KeePass2XmlV2) — unbekannte Version → KdbxUnsupportedException, defekte Base64/Hex/Checksumme → KdbxCorruptException (niemals stilles Hashing eines echten <KeyFile>); ② feste 32-B-Binär (Typ FixedBinary); ③ 64-Hex-Zeichen (Typ FixedBinaryHex); ④ beliebige Datei → SHA-256 (Typ Hashed). Fremdes/ arbiträres XML ohne <KeyFile>-Root fällt wie bei KeePassXC auf Hashing zurück. KdbxCredentials.composite() nutzt jetzt KdbxKeyFile.deriveKey statt des naiven size==32-Hacks; FR-K-09 R = SHA-256(SHA-256(pw)||keyfile||provider) gilt damit auch für echte KeePass-2-/KeePassXC-Keyfiles. newXmlV2(key) erzeugt ein KeePassXC-kompatibles .keyx (Schreibformat createXMLv2). Vektoren: alle KeePassXC-Referenzdateien (FileKeyBinary/Hex/Xml/XmlV2, HashFail/BrokenHex/ BrokenBase64/UnsupportedVersion/Empty) als JVM-Tests abgebildet plus End-to-End- Roundtrips save→open mit Binary-/XML-v2-/Hashed-Keyfile und Falsch-Keyfile-Ablehnung (HMAC-Mismatch). 17 neue JVM-Tests (KdbxKeyFileTest) — Suite 222 Tests grün. versionCode 28 -> 29. Doc + Tests + App.

Status 2026-09-20 (28)

Secretary Secrets 28 (versionCode 28): KDBX-4-I/O implementiert (§4) — org.beowulf.secretary.secrets.kdbx liest/schreibt KDBX 4.0/4.1 (FR-K-01..12, FR-K-31): KdbxContainer.open/save verifiziert vor jeder Entschlüsselung Header-SHA-256 und Header-HMAC sowie jeden Block-MAC des HMAC-Block-Streams (FR-K-07). KdbxOuterHeader (Signatur 9AA2D903/B54BFB67, Felder 0/2/3/4/7/11/12, End-of-Header 00‖Int32(4)‖0D0A0D0A), KdbxVariantDictionary als VariantMap v0x0100 (Keys R/S/P/M/I/V/ $UUID; KeePassXC- und KeePass-2.x-Schreibweisen), KdbxKdfConfig mit AES-KDF (6000-Runden-Vektor) und Argon2d/id (RFC-9106-Vektoren, bcprov). Schlüsselbildung exakt nach KeePassXC: Composite SHA256(SHA256(pw)), AES-KDF/Argon2-Transform, encKey=SHA256(seed‖T), hmacBase=SHA512(seed‖T‖0x01), Header-MAC-Key SHA512(0xFFFFFFFFFFFFFFFF‖hmacBase); ACHTUNG: HMAC läuft über die rohen Header-Bytes, nicht über den SHA-256! Äußere Cipher AES-256-CBC/ChaCha20 + gzip, innerer Header (Salsa20/ChaCha20) mit Binär-Pool, KdbxXmlReader/Writer inkl. Configuration-Attribute/Subtree, MemoryProtection, AutoType mit Associations, History, CustomData (Items mit fremden Extras wie KeePassXC LastModificationTime werden verlustfrei bewahrt, FR-K-29) und KdbxUnknownEl-Bag für unbekannte Elemente. Interop: echte KeePassXC-Datei (~/Downloads/Test.kdbx, KDBX 4.1, AES-KDF, gzip, AES-256-CBC; 453.763 B) wird mit Passwort vollständig geöffnet — Header-Hash/HMAC, alle Block-MACs, Entschlüsselung, XML; 162 Einträge in Root-Gruppe beowulf, Passwort-Protected-Werte lassen sich de-chiffrieren; unabhängige Python-Reimplementation bestätigt die Kette. Wichtigste Kompatibilitäts-Bugfixes: VariantMap-valueSize-Feld für jede Typ (auch fixe), VariantMap-Version LE 00 01, End-of-Header-/innerer-END- Feld-Bytes, Header-HMAC-Message = rohe Header-Bytes, KDF-Param-Keys KeePassXC-Stil, CustomData-Item-Extras. 49 KDBX-JVM-Tests (u. a. KDBX4- Vektoren, Tamper-/Wrong-Credential-Ablehnung, Roundtrips AES+Argon2) — Suite 205 Tests grün. versionCode 27 -> 28. Doc + Tests + Main (kdbx-Package; App-Anbindung/Import folgt).

Status 2026-09-20 (27)

Secretary Secrets 27 (versionCode 27): Offline-Cache + DAV-Sync über secretary-common-android (v0.33.0) — §3.3, FR-S-230..236. Neuer SecretsRemote-Adapter (CaldavSecretsRemote): list (Depth-1), fetch, put (conditional), delete, createCollection. SecretWire modelliert den secrets/-Pfad (§3.3): Kind META/RECOVERY/GROUP/ENTRY/ ATTACHMENT/COLLECTION/UNKNOWN, Node.groupDir/fileName, href-Builder, classify, contentType, toSecretNode (fremde/unklare Pfade → UNKNOWN). SecretOfflineStore (typeVersion 1, org.json) hält Snapshot mit vaultUuid, meta, recovery, groups, entries, attachments und der Pending-Queue (SecretAction: create_group/put_resource/ delete_resource); load() persistiert den frischen Zustand (stabile vaultUuid). SecretSync.pull läuft als BFS über die Collections, vergleicht ETags (nur Geänderte/Neue werden geholt, FR-S-231), validiert je Ressource über die vorhandenen Codecs (unlesbare → skipped) und prunet blind Serverseitig Gelöschte, sofern nicht Pending (FR-S-232); Pending-Puts/-Deletes sind autoritativ und bleiben erhalten. push(mk) replayt die Queue FIFO: mkcol (405 = existiert bereits), Puts als conditional Create/Update, 412 auf Einträge → SecretMerge mit Basis aus dem Cache (Konflikte bleiben Pending, Dokument hält Server-Stand, FR-S-234); ohne Master-Key → locked:-Unresolved. buildVault setzt aus dem Snapshot den SecretVault zusammen (Root/Meta aus secrets-meta.ics, Gruppen aus group.ics, Einträge mit groupUuid aus dem Verzeichnis). testImplementation kotlinx-coroutines-test. Suite um 26 JVM-Unit- Tests erweitert (SecretWireTest 7, SecretOfflineStoreTest 6, SecretSyncTest 13 inkl. FakeRemote): erste Sync baut Cache+Vault, zweite ist offline-frei, ETag-No-Op, Server-Löschungen werden geprunt, malformed Einträge übersprungen, Offline → Cache unberührt, Pending- Autorität, Push-Flush (Neu/Update/Löschen), Mkcol-405, 412-Reihe (Rein-Merge, echter Konflikt, locked), Collection-Walk mit verschachtelten Gruppen. 156 Tests grün. versionCode 26 -> 27. Doc + App.

Secretary Secrets 26 (versionCode 26): Gerätegebundener MK-Wrap (DeviceKeyWrap/DeviceKeyStore, FR-S-202, CLI-SEC-01, §3.2). DeviceKeyRecord (SCHEMA secrets/device-key/1, ALG aes-256-gcm) hält den mit dem Geräteschlüssel versiegelten MK als JSON (device-key.json); Schema-Major-/Alg-/Feld-Guards wie in der Familie. DeviceKeyEnvelope versiegelt/entsiegelt den 32-B-MK als dk1:-Envelope (AES-256-GCM, 12-B-IV, Tag 128 Bit, kein AAD, Byte-Layout identisch zu ss1:, FR-S-117) über ein javax.crypto.SecretKey — damit ohne Schlüssel-Extraktion auch Hardware-/Keystore-Schlüssel nutzbar. DeviceKeyCipher (32-B-Schlüssel- Bytes, Test-/Software-Pfad) liefert dasselbe Layout. DeviceKeyStore bindet den Android-Keystore an: nicht-extrahierbarer AES-256-GCM- Schlüssel secrets_vault_device_key, Wrap legt den Record persistent ab (wrapMasterKey), Unseal nur über den Keystore-Schlüssel; der MK ist nie im Klartext gespeichert. VaultKeyState hält FR-S-203: der MK existiert nur im RAM solange der Tresor entsperrt ist, lock() verwischt ihn. Suite um 30 JVM-Unit-Tests erweitert (DeviceKeyCipherTest 8, DeviceKeyWrapTest 10, DeviceKeyEnvelopeTest 6, VaultKeyStateTest 6): Round-Trip, frische Envelope je Seal, Falsch-Schlüssel-Ablehnung, Größen-Guards, JSON/Schema-/Alg-/Präfix-Guards, Keystore-Layout- Kompatibilität, Lock-Wipe/Idempotenz/Re-Unlock. 130 Tests grün. versionCode 25 -> 26. Doc + App.

Status 2026-09-19 (25)

Secretary Secrets 25 (versionCode 25): MK-Wrap für Wiederherstellung (RecoveryKey, FR-S-204..206, §3.2). RecoveryKeyCipher verschlüsselt den 32-B-Tresor-Master-Key mit einem vom Recovery-Passwort abgeleiteten Schlüssel: scrypt (BC <Scrypt>; N=2^20=1 MiB, r=8, p=1, dkLen=32, frisches 16-B-Salt pro Seal) → AES-256-GCM als rk1:-Envelope (RecoveryKeyCipher.ENVELOPE_PREFIX, FR-S-204). RecoveryKey (SCHEMA secrets/recovery-key/1, ALG scrypt-aes256-gcm, ScryptParams n/r/p/dkLen, salt b64, wrappedKey) wird als kanonisches JSON (recovery-key.json, FR-S-206) kodiert: Schema-Major-Guard (unbekannte Major/alg/missende Felder → ablehnung), Salt/WrappedKey-B64-Validierung, frische IV und frisches Salt je Seal (gleicher MK+Pass → verschiedener Blob, FR-S-205), deterministische Schlüsselableitung bei gleichem Salt+Params. SecretsCipher um sealWith/unsealWith/sealAsync/ unsealAsync (parametrisierbares Präfix bel. Kodierungen) erweitert; Live-Key-Umschlag hält sich an FR-S-208 (kein persistierter Passwort- Umschlag auf dem Gerät; stattdessen Device-Wrap via Android-Keystore — folgt). Suite um 12 JVM-Unit-Tests erweitert (RecoveryKeyTest): Round-Trip, Falsch-Passwort-Ablehnung, frische Salt+IV je Seal, deterministische Ableitung, Salt-Größe/-Eindeutigkeit, JSON-Round-Trip (Params konserviert), Default-Params (§3.2: 2^20/8/1/32), Schema-/Alg-/ Feld-Guards, korruptes Salt, fremder Envelope, bel.-lange MK-Blob-Round- Trip. 100 Tests grün. versionCode 24 -> 25. Doc + App.

Status 2026-09-19 (24)

Secretary Secrets 24 (versionCode 24): In-Memory-Tresor (SecretVault) — §6.2, FR-S-210..218. SecretVault bündelt rootGroup + Gruppenliste + Einträge + SecretMeta als unveränderlicher Tresor-Zustand (rein, JVM-testbar, noch ohne Sync). API: group()/entry()/children()/groupPath()/ entriesInGroup(), addGroup/renameGroup (UUID unverändert, FR-S-214)/ moveGroup (unterstes Re-Root der Subtree, Zyklus-Schutz)/ removeGroup (rekursiv, Ledger-Tombstone für alle Gruppen- und Eintrags-UUIDs, FR-S-218), createEntry (frische UUID, FR-S-212) / putEntry/moveEntry (setzt previousParentGroupUuid = alte Gruppe, FR-S-215) / removeEntry (Ledger-Anhang, FR-S-217), purgeDeleted (expliziter Purge), Parent-Validierung überall. groupPath baut /Internet/Email ohne Root-Namen (Passend zur kanonischen groupPath), Root-Uuid → /. 12 JVM-Unit-Tests (SecretVaultTest): Baum/Paths, CreateEntry, Move (Group/Entry, Meta-Parent), Remove-Entry/Group-Tombstones, Rename-Keeps-Uuid, Cyclic-Move-Reject, Invalid-Ops, PutGroup-Validate, Purge, Root-Operationen. Suite 89 Tests grün. versionCode 23 -> 24. Doc + App.

Status 2026-09-19 (23)

Secretary Secrets 23 (versionCode 23): secrets-meta.ics (SecretMeta/SecretMetaJournal) — §5.8, FR-S-217/218. SecretMeta hält die operationalen Meta-Teile des Tresors: KDBX Meta-Skalare (generische Map), CustomIcons (uuid → base64 PNG), das DeletedObjects-Ledger (uuid + deleted-Zeit; Append-only durch withDeleted, idempotent, withoutDeleted für Purge) und die Root-Gruppen-Metadaten (rootGroup). SecretMetaJournal kodiert secrets-meta.ics als VJOURNAL mit festem UID:secrets-meta@secrets.org.beowulf und kanonischem JSON-Blob im Body (DESCRIPTION, Escaped+Folded über die gemeinsame VJournal-Hilfe); nichts davon ist standardmäßig geschützt (§5.8). JSON-Roundtrip mit org.json, Unicode-/Sonderzeichen-Sicherheit (\;, \,, \\), Langtext-Faltung (>2000 B), Stable-UID. 7→9 JVM-Unit-Tests (SecretMetaTest): Voll-/Leer-Round-Trip, Ledger Append/Idempotenz/Remove/Contains, Ledger über Wire, Folding, Sonderzeichen, UID, fehlender Body → malformed. Suite 76 Tests grün. versionCode 22 -> 23. Doc + App.

Status 2026-09-19 (22)

Secretary Secrets 22 (versionCode 22): Gruppenmodell + group.ics (SecretGroup/SecretGroupJournal) — §5.8, FR-S-211/214/215. SecretGroup (uuid, name, parentUuid, tags, notes, iconId, customIconUuid, times, expanded, customData) bildet jede Nicht-Root-Collection des secrets/-Baums ab. SecretGroupJournal kodiert group.ics: UID = base64url(uuid)+@secrets.org.beowulf (reversibel), SUMMARY = Name (autoritativ, wie §5.8), CATEGORIES = Gruppen-Tags, X-SECRETS-GROUP-PARENT = Eltern-UUID (nach Move für KDBX-Round-Trip), X-SECRETS-GROUP-NOTES/-ICON/-CUSTOM-ICON/-EXPANDED und X-SECRETS-GROUP-DATA;NAME=<pct> je CustomData-Eintrag; Nutzung der gemeinsamen SecretVJournal.parseProperties (Faltung/Param-Erhalt wie im Eintrags-Codec). SecretEntry um previousParentGroupUuid erweitert (FR-S-215: Move setzt X-SECRETS-PREV-PARENT-GROUP, KDBX PreviousParentGroup bleibt erhalten), im Eintrags-Codec durchgelesen. 7 JVM-Unit-Tests (SecretGroupTest): Voll-/Minimal-Round-Trip, Name mit Sonderzeichen, Kind-/Eltern-Name getrennt, Prev-Parent-Round-Trip, UID-Reversibilität, Merge erhält Gruppenfelder. Suite 67 Tests grün. versionCode 21 -> 22. Doc + App.

Status 2026-09-19 (21)

Secretary Secrets 21 (versionCode 21): Anhänge (SecretAttachment) — §5.4, FR-S-120..123. SecretAttachment ist die Referenz (sha256, name, size, memprotect, external); Bytes werden vor Speicherung mit dem MK als ss1:-Envelope versiegelt (FR-S-121), content-adressiert per Klartext- SHA-256 (FR-S-122). Wire-Form (§5.2/5.7): kleine Anhänge inline ATTACH;VALUE=BINARY;ENCODING=BASE64:<ss1-envelope>, große (Default- Threshold 256 KiB) ATTACH;VALUE=URI:a-<sha256>.bin; Helfer X-SECRETS-ATT-NAME/SHA256/SIZE/MEMPROTECT/ALG;SEQ=n (SEQ = Anhangs-Index, ALG=aes-256-gcm verpflichtend, Abweichung → malformed). SecretEntry um attachments + inlineAttachmentValues (sha → Envelope für Decrypt-on-Demand, Codec bleibt MK-frei) erweitert; History-Snapshots tragen Anhangs-Referenzen (Bytes content-adressiert, nicht dupliziert — FR-S-134). SecretMerge merge Anhänge per §8: set union nach sha256, Entfernung nur auf der ändernden Seite, Inline-Envelope-Union. SecretAttachments: sha256Hex, seal/unseal, isLarge, refOf. 12 JVM- Unit-Tests (SecretAttachmentTest): SHA-Vektor, Seal/Unseal, Threshold, Ref, Inline-/Extern-/Mixed-Round-Trip, Merge-Union/-Removal, Snapshot-Referenzen, fehlender Inline-Envelope → Fehler, ungültiges ALG → malformed. Suite 60 Tests grün. versionCode 20 -> 21. Doc + App.

Status 2026-09-19 (20)

Secretary Secrets 20 (versionCode 20): Drei-Wege-Merge-Engine (SecretMerge) — §8, FR-S-230..236. merge(base, server, client, mk) ist rein clientseitig (§8: Eingaben = ETag-Basis, Server-Stand, lokaler Arbeitsstand); Felder werden pro Name verglichen: client == base → Server behalten, server == base → Client übernehmen, beides geändert → Konflikt (FR-S-233). Änderungserkennung läuft auf Fingerprints ohne Decrypt: geschützte Felder über den stabilen ss1:-Ciphertext (FR-S-118), gleiche Ciphertexte = unverändert (FR-S-231). Geänderte geschützte Felder beider Seiten werden bei Bedarf entschlüsselt und auf Plaintext verglichen (verschiedene IVs desselben Klartextes → kein Konflikt, Sieger wird mit frischer IV neu versiegelt, §8); echter Konflikt → beide Kandidaten + Zeiten bleiben in MergeConflict erhalten, das Merge-Dokument hält den Server-Stand (FR-S-234). Lösch-Vs-Änderung je Seite → Konflikt, Löschung allein → anwendbar. Tags: Schnittmenge + Add-Deltas, Entfernungen nur wenn auf der ändernden Seite entfernt (§8), konfliktfrei. History: set union je (uuid, Zeit, Inhalt) — contentEquals-dedupliziert, kein Item geht verloren (FR-S-138/235); unlesbarer Server-Blob wird verlustfrei übernommen. Ergebnis-Zeit = neuester modifiedRaw, SEQ regeneriert. 15 JVM-Unit-Tests (SecretMergeTest): disjunkte Feld-Änderungen, gleiches Feld → Konflikt, einseitig geänderte Werte, geschützter gleicher Klartext (Re-Encrypt), stabiler Ciphertext bleibt, Add-beidseitig, Lösch-Semantik, Tag-Union/Removal, History-Union/-Dedup, Zeit-Gewinner, Frisch-Entry-Merge. Suite 48 Tests grün. versionCode 19 -> 20. Doc + App.

Status 2026-09-19 (19)

Secretary Secrets 19 (versionCode 19): Eintrags-Historie (SecretHistory) — §5.5, FR-S-131..137. SecretHistory verwaltet die immer-vollständige Historie eines Eintrags als einen ss1:-Blob in X-SECRETS-HISTORY;ENCODING=ss1;COUNT=n (§5.5). Inside: kanonisches JSON (org.json, Familienstil wie Journal) — Array aus Entry-ähnlichen Snapshots gleicher Feld-Shape ohne verschachtelte History: uuid, modifiedRaw (Modifikationszeit des dargestellten Zustands), Felder (geschützte als ss1:-Envelope), Tags; SEQ-Reihenfolge kanonisiert. append(before, after, mk) erweitert die Historie um den Snapshot des Vor-Zustands (before.snapshotOf()) und liefert after mit neuem Blob; No-Op-Edits (Content identisch) erzeugen keinen Eintrag, doppelte Snapshots werden abgewiesen; contentEquals() vergleicht uuid/times/ fields/tags en bloc (Basis für Merge-Historie-Union, FR-S-138). SecretEntry um historyEnvelope/historyCount erweitert; SecretVJournal überträgt beide durch (Count-Param wird gelesen/ geschrieben). 10 JVM-Unit-Tests (SecretHistoryTest): JSON-Round-Trip, Unicode-Escaping, Seal/Unseal, Append-Semantik (Snapshot-Vor-Zustand, Stapelung, No-Op, Duplikat-Abweisung), VJOURNAL-Round-Trip mit History. testImplementation("org.json:json:20250517") (Device: android.jar). Gesamt-Suite 33 Tests grün. versionCode 18 -> 19. Doc + App.

Status 2026-09-19 (18)

Secretary Secrets 18 (versionCode 18): Kanonisches Eintragsmodell (SecretEntry) + VJOURNAL-Codec (SecretVJournal). SecretField (Name/Wert/protected/seq) und SecretEntry (uuid, group, Felder, Tags, Zeitstempel, Schema secrets/1) bilden das kanonische Modell (§5.6); Feld-Editoren halten FR-S-118 ein — protectField() verschlüsselt ein unverändertes geschütztes Feld nicht neu (stabiler Ciphertext als Änderungs-Fingerprint), unprotectField()/setFieldPlain()/ removeField() runden die Feld-Operationen ab. SecretVJournal kodiert das Modell als VJOURNAL (§5.2/§5.3): X-SECRETS-FIELD;SEQ=n;NAME=<pct>; PROTECTED=TRUE|FALSE;ENCODING=ss1|text in SEQ-Reihenfolge, pro geschützte Felder ss1:-Envelope über SecretsCipher, RFC-3986-pct-Namen für beliebige KeePass-Feldnamen, UID = base64url(uuid)+@secrets.org.beowulf (reversibel), X-SECRETS-SCHEMA, X-SECRETS-TAG (autoritativ) + CATEGORIES-Spiegel, SUMMARY/DESCRIPTION-Mirror (beim Lesen ignoriert, wenn das Feld vorhanden ist; sonst Fallback), Zeilenfaltung/unfolding, MEMPROTECT/History folgen. Schema-Guards: fehlende X-SECRETS-SCHEMA → MalformedSecretVJournalException, neuere Major-Version → UnsupportedSecretSchemaException (FR-S-107); geschützte Felder ohne ss1-Encoding werden abgelehnt. 13 JVM-Unit-Tests (SecretVJournalTest), Gesamt-Suite 23 Tests grün. versionCode 17 -> 18. Doc + App.

Status 2026-09-19 (17)

Secretary Secrets 17 (versionCode 17): Start der Umsetzung — Core ss1:-Envelope (SecretsCipher). Erster Quellcode im Scaffold: SecretsCipher (AES-256-GCM, 12-B-IV/Tag-16-B, kein AAD) versiegelt und entschlüsselt geschützte Werte als ss1:-Envelope (FR-S-117, §3.2/§5.3/ §10.3), newMasterKey() erzeugt den 32-Byte-Vault-Master-Key (FR-S-200). Zehn JVM-Unit-Tests (SecretsCipherTest): Round-Trips (Text/Binary/leer), Präfix, eindeutige IVs (wiederholtes Sealen ergibt andere Envelope), Falscher-Key-/Manipulations-Ablehnung (GCM-Tag), Malformed-Rejects, Größen- Guard und Zufalls-Master-Key. Reines javax.crypto + java.util.Base64, JVM-testbar. AndroidManifest.xml minimal angelegt. versionCode 16 -> 17. Doc + App.