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"):
- Der ETag des Bestands wandert in den
PutResource. Ohne ihn ueberschriebe ein Erneuern die Serverdatei blind; mit leerem ETag wirdIf-None-Match: *gesendet (Anlegen), mit gesetztemIf-Match(Aktualisieren), 412 loest den Merge-Pfad aus. - Die Aktion landet vorne in der Queue und ein
createersetzt alle vorherigen Aktionen auf diesem Href. Bricht ein spaeterer Put ab, ist der Recovery-Key trotzdem schon oben — und zwei konkurrierende Puts, von denen der aeltere gewinnt, waeren der denkbar schlechteste Ausgang fuer den letzten Ausweg eines Nutzers. - Nach dem Entschperren wird der
dk1:-Wrap neu geschrieben — ohne das waere der Tresor nach dem naechstenlock()wieder zu und der Key einmal verbraucht, ohne dass etwas gewonnen waere. Er geschieht bewusst vorunlock(): sonst gaebe es den Zustand „entsperrt, aber nicht mehr speicherbar".
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:
[server] hostnameist Pflicht ohne Default. Ohne das Feld bricht der Start mitConfig-Parsefehler: missing field 'hostname'ab, ohne den fehlenden Wert zu nennen.storage_pathmuss absolut sein.normalizeBaseUrlergaenzt ein fehlendeshttps://(CaldavClient). Gibt man127.0.0.1:8443ein, versucht die App TLS gegen einen Klartext-Server und der Login scheitert mit einem Netzwerkfehler, der nach Server-Problem aussieht. Genau deshalb traegt das ManifestusesCleartextTraffic="true"— der Preis ist inSECURITY.md§4.2 offengelegt.- Der Server ignoriert den Benutzernamen.
parse_basic_authliefert ihn, geprüft wird nur das Passwort (src/lib.rs); der eigene Benutzer kommt aus$USER. Verifiziert: jeder Benutzername funktioniert.
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:
snapshotFromVaultlegt…/group.icsan (Schlüssel = voller href).find(href)schlägt für diese Gruppe fehl (groups[uuid]ist leer) — der ETag-/Body-Abgleich beim Push bekommt nichts zu sehen.pull/putWireConditionallegt dieselbe Gruppe zusätzlich unteruuidab. Weil die Map unter einem anderen Schlüssel geschrieben wird, ersetzt der neue Eintrag den alten nicht, er kommt daneben.buildVaultiteriertgroups.valuesohne Dedupe und materialisiert beide —SecretVaultbekommt 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:
snapshotFromVaultschreibt den kanonischen Schlüssel (Gruppen-UUID); der href bleibt inCachedBody.href, damitfindweiterhin dagegen prüfen kann.canonicalGroups()inSecretOfflineStorenormalisiert beim Lesen und beim Schreiben. Das ist der entscheidende Detailpunkt: Der Cache trägt eine SHA-256 über genau seine Serialisierung, undloadrechnet sie aus dem geparsten Snapshot nach. Normalisierte man nur beim Lesen, läge der Snapshot auf der Platte kanonisch, im Speicher aber doppelt — und der Integritätsvergleich hätte fehlgeschlagen,loadhätte den Cache als manipuliert eingestuft und den Tresor als leer geliefert. Genau das ist beim ersten Versuch passiert (alle Werte 0) — der Prüfsummenvergleich hat den Widerspruch korrekt bemerkt. So bleibtparse -> encodeein Fixpoint.buildVaultdedupliziert zusätzlich überdistinctBy { uuid }. Das heilt Caches, die von einem älteren Build bereits doppelt auf der Platte liegen, im Speicher — ohne Migration, und ohne dass der Nutzer seinen Tresor neu importieren muss.
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
- Legacy-Queue ohne Namen,
SecretOfflineStoreTestLegacy-Roundtrip,PendingLocalEditsTestCollection-Name). Suite 339 Tests gruen (:common75 +:secrets264, 0 Fehler; verifiziert via:common:testDebugUnitTest :secrets:testDebugUnitTest, offline). versionCode 49 -> 50. Doc + App + Tests.
Status 2026-09-22 (48)
Secretary Secrets 48 (versionCode 48): **Eintrag-Editor KeePass-kompatibel
- Aktionen ins Kontextmenue der Liste**. (1) Der Eintrag-Dialog deckt jetzt die
KeePass-Standardflaeche ab: URL (neues Feld, Sequenz nach Password),
Tags (kommasepariert, kanonische Liste ueber
SecretEntry.tags) und Ablaufdatum (Ablauf-Checkbox + ISO-ZeitJJJJ-MM-TTTHH:MM:SS). Das Ablaufdatum lebt in den Times des opaken KDBX-Bags (FR-K-65):KdbxBagbekommt die Expiry-BrueckeisoToBase64/base64ToIsoundentryBagWithExpiry/entryBagExpiry— sie setzenexpires/expiryBase64imTimesBag, ohne die restliche K2-Flaeche anzutasten; Tresor-eigene Eintraege (ohne Bag) bekommen beim ersten Ablauf einen Bag, beim Loeschen verschwindet er wieder (kein Still-Default).createEntry/applyEntryEdituebernehmen URL/Tags/Ablauf (FIELD_EDITABLEumURLerweitert; fremde Felder bleiben weiterhin erhalten). (2) Die drei fetten Vollbreite-Knoepfe (Bearbeiten/Verschieben/Loeschen) sind aus dem Eintrag-Detail entfernt und in ein Kontextmenue gewandert: jede Eintragszeile der Liste hat jetzt einen Ueberlauf-Knopf (⋮, wie beim Gruppen-Menue) mit den drei Aktionen; das Detail zeigt nur noch die (entsiegelbaren) Felder sowie Tags und Ablauf (schreibgeschuetzt). 6 neue JVM-Tests (KdbxBagTest): ISO<->Base64-Roundtrip, ungueltige Eingaben, Setzen+Erhalten des Bags, Loeschen des Ablaufs, Neuanlage/Entfernung auf bag-losen Eintraegen, ungueltiges ISO behaelt den alten Ablauf. Suite 263 Tests gruen (257 -> 263, verifiziert via ContainercompileDebugKotlin+testDebugUnitTest). versionCode 47 -> 48. Doc + App + Tests.
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
- Push/Pull der Pending-Queue (FR-S-230..236)**. Die App bekommt jetzt das
Konto und kann den Tresor mit dem
secrets/-Server synchronisieren.
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.