Secretary Secrets (Android)
Android-Client des Passwort-/Schlüssel-Tresors auf dem
secretary-server (CalDAV). Jeder Eintrag ist eine
.ics-Datei (VJOURNAL) in einer Gruppen-Collection unter dem neuen
top-level secrets/-Root; Gruppen sind Collections. Maßgeblich ist die
SRS in secretary-contract/secrets/SRS.md (1.0.0, DRAFT).
What it does
- A password and key vault where groups are collections and every entry is an encrypted
.icsfile on your own server - Client-side encryption: field values, history blobs and attachments are sealed as
ss1:envelopes (AES-256-GCM with the vault master key, fresh IV per encryption) - The master key is never stored in clear text. It is wrapped by a scrypt-derived key from your recovery password (
rk1:,recovery-key.json) and by a per-device key (dk1:,device-key.json) so only enrolled devices can unseal - The server only ever sees sealed blobs - it cannot read a single secret
- Attachments and history are encrypted with the same envelope as the fields
- Grouping, search and the group picker behave like the other Secretary apps
Changelog
Every released version: Apps/secrets/changelog
Screenshots
Screenshots are on their way - see the Apps section of the wiki.
Verzeichnisstruktur
secretary-secrets-android/
├── build.gradle.kts # Root-Build
├── settings.gradle.kts
├── gradle.properties
├── CONTEXT.md
├── gradle/wrapper/
└── app/
├── build.gradle.kts
├── proguard-rules.pro
└── src/
├── main/
│ ├── AndroidManifest.xml
│ └── java/org/beowulf/secretary/secrets/
│ ├── data/
│ │ ├── SecretsCipher.kt # ss1:-Envelope (AES-256-GCM)
│ │ ├── SecretAttachment.kt # Anhänge (sha256, inline/extern)
│ │ ├── SecretEntry.kt # kanonisches Eintragsmodell (Felder)
│ │ ├── SecretGroup.kt # Gruppenmodell + group.ics-Codec
│ │ ├── SecretMeta.kt # Meta-/CustomIcons-/DeletedObjects
│ │ ├── RecoveryKey.kt # recovery-key.json (scrypt-Wrap, FR-S-204)
│ │ ├── DeviceKeyWrap.kt # dk1:-Device-Wrap + device-key.json (FR-S-202)
│ │ ├── DeviceKeyStore.kt # Android-Keystore MK-Wrap (CLI-SEC-01)
│ │ ├── VaultKeyState.kt # MK nur im RAM, Lock wischt (FR-S-203)
│ │ ├── SecretVault.kt # In-Memory-Tresor (CRUD, §6.2)
│ │ ├── SecretVJournal.kt # VJOURNAL-Codec (X-SECRETS-FIELD)
│ │ ├── SecretHistory.kt # History-Blob (kanonisches JSON, ss1:)
│ │ ├── SecretMerge.kt # Drei-Wege-Merge (Feld-/Tag-/History)
│ │ ├── SecretWire.kt # secrets/-Pfadmodell + Klassifikation (§3.3)
│ │ ├── SecretOfflineStore.kt # Offline-Cache + Pending-Queue (org.json)
│ │ ├── SecretsRemote.kt # SecretsRemote + CaldavSecretsRemote
│ │ ├── SecretsRepository.kt # DAV-Konto (AccountResolver, /secrets/)
│ │ ├── SecretSync.kt # Pull/Push/Full-Sync über CalDAV
│ │ ├── SyncRetry.kt # Backoff auf transiente Fehler (FR-S-230)
│ │ └── SecretSearch.kt # Suche über entsiegelte Felder (M3)
│ └── kdbx/
│ ├── KdbxIo.kt # LE-Primitives, Reader/Writer
│ ├── KdbxUuid.kt # UUID-Klasse
│ ├── KdbxKeys.kt # Composite/Keys, AES-KDF, Argon2d/id, HMAC
│ ├── KdbxKeyFile.kt # KeePass-Keyfile (Xml v1/v2, Bin/Hex/Hash, FR-K-09)
│ ├── KdbxOuterHeader.kt # Signatur, Version, Felder, End-of-Header
│ ├── KdbxVariantDictionary.kt # VariantMap v0x0100 (FR-K-06)
│ ├── KdbxCipherKit.kt # AES-256-CBC/ChaCha20, gzip, inner Stream
│ ├── KdbxHmacBlocks.kt # HMAC-Block-Stream (FR-K-07)
│ ├── KdbxInnerHeader.kt # Stream-ID/Key, Binär-Pool
│ ├── KdbxXml.kt # KDBX-XML-Leser/-Schreiber
│ ├── KdbxModel.kt # K0-K1-Objektmodell (Gruppen/Entries)
│ ├── KdbxMapping.kt # KDBX ⇔ Tresor-Brücke (Import/Export, §4.6/4.7)
│ ├── KdbxContainer.kt # open/save (FR-K-01..12, FR-K-31)
│ ├── KdbxIds.kt # Cipher-/KDF-UUIDs, KdbxKdfConfig
│ ├── KdbxTime.kt # KDBX4-Datum (Base64-Int64)
│ └── KdbxErrors.kt # Corrupt/Integrity/Unsupported
│ └── app/
│ ├── MainActivity.kt # Compose-Host
│ ├── VaultViewModel.kt # MK-Lebenszyklus + Import/Export/Offline
│ └── ui/SecretsApp.kt # Material3: Home/Import/Export (SAF)
│ └── ui/Browse.kt # Durchsuchen + Eintrag-Edit
└── test/
└── java/org/beowulf/secretary/secrets/
└── data/
│ ├── SecretsCipherTest.kt
├── RecoveryKeyTest.kt
├── RecoveryKeySetupTest.kt # Recovery-Wiring + scrypt-Parameter
├── SecretAttachmentTest.kt
├── SecretGroupTest.kt
├── SecretMetaTest.kt
├── SecretVaultTest.kt
├── SecretVJournalTest.kt
├── SecretHistoryTest.kt
├── SecretMergeTest.kt
├── DeviceKeyCipherTest.kt
├── DeviceKeyWrapTest.kt
├── DeviceKeyStoreKeystoreTest.kt # Keystore-Fallback (M1-Audit v54)
├── KdbxMalformedInputTest.kt # Import-Robustheit (M4 v55)
├── CrashReporterRetentionTest.kt # Absturzreport-Deckel (M5 v56)
├── DeviceKeyEnvelopeTest.kt
├── VaultKeyStateTest.kt
├── SecretWireTest.kt
├── SecretOfflineStoreTest.kt
├── SecretSyncTest.kt
├── SyncRetryTest.kt # Backoff/Statusklassifikation
└── SecretSearchTest.kt # Suche/Ranking/Snippet (M3)
└── kdbx/
├── KdbxPrimitivesTest.kt # VariantMap/Zeit/UUID
├── KdbxKeysTest.kt # Key-Ableitung inkl. Vektoren
├── KdbxKeyFileTest.kt # Keyfiles gg. KeePassXC-Vektoren
├── KdbxCryptoTest.kt # Argon2d/id-Vektoren (RFC 9106)
├── KdbxCipherKitTest.kt # Cipher/gzip/inner Stream
├── KdbxHmacBlocksTest.kt # Block-Stream-Integrität
├── KdbxContainerTest.kt # open/save-Roundtrips (alle KDFs)
├── KeePassXCOpenTest.kt # echte KeePassXC-Datei öffnen
├── KeePassXCOpenKeePassXCInteropTest.kt # save -> keepassxc-cli (FR-K-30)
├── KdbxMappingTest.kt # Import/Export-Brücke (FR-K-60..75/90)
└── KdbxMappingMergeTest.kt # Import-Merge in Tresor (FR-K-63/67/76)
Befehle
Voraussetzungen: JDK 21, Android-SDK (Platform 37, Build-Tools 34)
und Gradle 9.x. Im Repository liegt bewusst kein Gradle-Wrapper --
entweder Android Studio nutzen (bringt sein Gradle mit) oder Gradle 9 selbst
installieren; die Beispiele unten rufen deshalb gradle direkt.
export ANDROID_HOME="$HOME/Android/Sdk" # bzw. sdk.dir=... in local.properties
gradle :secrets:assembleDebug # Debug-APK
gradle :secrets:testDebugUnitTest # JVM-Tests
gradle :secrets:compileDebugKotlin # Kompilierbarkeit
Konzepte
- Envelope
ss1:: geschützte Feldwerte, History-Blobs und Attachments werden mit dem Vault-Master-Key (32 B AES-256) alsss1:+base64(iv(12) ‖ AES-256-GCM(MK, plaintext))versiegelt (IV eindeutig je Verschlüsselung, Tag 16 B, kein AAD — FR-S-117). - Recovery-Key
rk1:/recovery-key.json(FR-S-204..208): Der 32-B-MK wird mit einer vom Recovery-Passwort scrypt-abgeleiteten Schlüssels (N=2^16, r=8, p=1, dkLen=32, frisches 16-B-Salt) per AES-256-GCM alsrk1:-Envelope gewrappt und alsrecovery-key.json(SCHEMAsecrets/recovery-key/1,ALG scrypt-aes256-gcm, ScryptParams im JSON, Kanonischer-JSON-Stil der Familie) auf den Server gelegt. Der MK selbst ist nie im Klartext gespeichert; Unseal nur mit Kenntnis des Recovery-Passworts. Ebenfalls rein clientseitig — der Server sieht nur den gewrappten Blob (FR-S-205). Erzeugen und Verwenden liegen inRecoveryKeySetup(Android-frei, JVM-testbar); der Ablauf in der App (Erzeugen → bedingter Put über die Pending-Queue → Einmal-Anzeige → Entschperren mit Geräte-Neuwrap) steht inBENUTZERANLEITUNG.md. N=2^16 statt 2^20 (SRS §3.2): 2^20 braucht128*r*N= 1024 MiB und ist auf Android nicht ausführbar; beim Lesen der Parameter aus der Serverdatei gilt eine Obergrenze von 256 MiB (ScryptParams.MAX_MEMORY_BYTES). Siehe Status v58. - Device-Wrap
dk1:/device-key.json(FR-S-202, CLI-SEC-01): Der 32-B-MK wird mit einem gerätegebundenen AES-256-GCM-Schlüssel alsdk1:-Envelope versiegelt (Byte-Layout identisch zuss1:, Fehlen von AAD, IV eindeutig je Seal). Auf Android hält der Android-Keystore einen nicht-extrahierbaren AES-256-GCM-Schlüssel (secrets_vault_device_key); der gewrappte MK liegt alsDeviceKeyRecord(SCHEMAsecrets/device-key/1) indevice-key.jsonim privaten App-Verzeichnis. Der MK ist nie im Klartext gespeichert.DeviceKeyEnvelopearbeitet aufjavax.crypto.SecretKey(keystore- tauglich),DeviceKeyCipherauf 32-B-Schlüsselbytes (Tests). - DAV-Sync (
SecretSync/CaldavSecretsRemote): Laden und Speichern des gesamtensecrets/-Baums übersecretary-common-android(§3.3):listDepth1-BFS über die Collections,fetchWiremit ETag-Vergleich gegen den Offline-Cache (SecretOfflineStore, typeVersion 1; nur Geändertes wird übertragen FR-S-230/231), Codec-Valdierung beim Parsen (unlesbare Ressourcen werden übersprungen und bleiben lokal),pendingPuts/pendingDeletessind autoritativ und per Prune geschützt (FR-S-232). Push spielt die Pending-Queue in FIFO-Reihenfolge:mkcolbei Kampf-Collection,putWireConditionalmit blauem ETag →If-None-Match: *(Erzeugen) bzw. set →If-Match(Aktualisieren); 412 auf Einträgen →SecretMerge-Auflösung wie §8 (bei Konflikt bleibt die lokale Warteschlange, das Dokument hält den Server-Stand FR-S-234); 412 ohne Master-Key →locked:-Unresolved. - Nicht implementiert (K2/I/O-Restmenge): der KDBX3-Legacy-KDF, ArcFourVariant als innerer Stream sowie das Argon2-Secret/Additional-Data (KeePassXC setzt diese nicht). Die K2-Erweiterungen (AutoType, Eintrags-Icons/-Farben, OverrideUrl, QualityCheck, unbekannte XML-Subtrees, CustomData-Extras) sind seit v46 als opaker KDBX-Bag im Modell mitführbar (FR-K-65); Warnings bleiben nur für History-Snapshots mit Anhängen und Meta-CustomData-Extras.