sync(chunk 2c): Keystore credentials, AccountManager, stub sync adapter
The platform half of chunk 2; the account-add UI is 2d, since it is a design task and the piece that needs an on-device review. A stub ContentProvider turned out to be required and was not in the plan: a sync adapter registers against a content authority, and we publish no provider since :provider was deleted. Without one there is nothing for contentAuthority to name, nothing for requestSync to address, and hasAuthorityAccess() makes every ContentResolver sync call a silent no-op at targetSdk 34+. - CredentialStore: Keystore AES/GCM, blob in its own DataStore file. security-crypto is formally deprecated and terminal. Decryption failure means re-authenticate, never a crash — including ProviderException, which is a RuntimeException and escapes the obvious catches. - CalDavAccounts + SyncAuthenticator: no password reaches AccountManager, which stores them as plain TEXT. The authenticator never returns null — a null is the protocol for "answering asynchronously", and nothing here does, so Settings would wait forever. addAccount refuses with a readable message until 2d ships the screen, rather than opening the home screen and hanging. - SyncAdapterService: enqueue and wait on the unique work *name*, not the request id — enqueueUniqueWork is async so the id is unknown when the wait starts, and under KEEP it may never exist at all. Being deduplicated is not a failure. - Account type and authority are per build variant, so debug and release do not fight over ownership. SyncContractTest guards the Kotlin/resValue pair. - The credential blob is the only thing excluded from backup: Keystore keys are non-exportable, so a restored ciphertext can never be decrypted. Known trade-off recorded in network_security_config.xml and SYNC-PLAN.md: the user CA store is trusted for all traffic, which chunk 5's cert4android should replace rather than sit beside. The instrumented tests here compile but have not been run — device work waits for an explicit go-ahead.
This commit is contained in:
+50
-1
@@ -58,7 +58,8 @@ Nextcloud/Radicale/Baïkal in chunk 5.
|
||||
| 1 | **Mapper** — VTODO ↔ Room | nothing | Unknown properties survive a read-modify-write cycle |
|
||||
| 2a | **Vendored DAV foundation** | nothing | Vendor a tree we can actually maintain, and know which of its defects are real |
|
||||
| 2b | **Discovery and auth protocol** | 2a | The two collection filters are inversions of the obvious rule |
|
||||
| 2c | **The Android account layer** | 2b | Never send a credential into an unvalidated redirect chain |
|
||||
| 2c | **The Android account layer** | 2b | Registering the sync adapter, or every `ContentResolver` sync call is a silent no-op |
|
||||
| 2d | **Account-add UI** | 2c | Never send a credential into an unvalidated redirect chain |
|
||||
| 3 | **Engine core** — read, write, conflicts | 1, 2 | A failed resource must not fail the collection |
|
||||
| 4 | **Incremental sync + scheduling** | 3 | Never persist a sync token before the bodies it covers are applied |
|
||||
| 5 | **Hardening, compliance, real servers** | 1–4 | Ship no licence violation, and no retry loop on a dead app password |
|
||||
@@ -228,6 +229,54 @@ resourcetype, Nextcloud's trashed calendar, the absent/empty component set, and
|
||||
|
||||
## Chunk 2c — the Android account layer
|
||||
|
||||
Everything that needs the platform and nothing that needs a screen: Keystore
|
||||
credentials, `AccountManager`, the stub sync adapter, the manifest, and the
|
||||
network security config. The account-add UI is 2d — it is a design task, it
|
||||
needs the `material-3` skill, and it is the piece that needs an on-device review.
|
||||
|
||||
### ⚠️ A stub `ContentProvider` turned out to be required
|
||||
|
||||
Not in the original plan, and it is load-bearing. A sync adapter registers
|
||||
against a **content authority**, and Agendula publishes no provider — `:provider`
|
||||
was deleted when we took our own Room store. With no authority there is nothing
|
||||
for `<sync-adapter android:contentAuthority>` to name, nothing for
|
||||
`requestSync` to address, and nothing for Settings to render a switch against.
|
||||
`SyncStubProvider` stores nothing and exists solely to hold up that end.
|
||||
|
||||
### Other things settled here
|
||||
|
||||
- **The account type and authority are per build variant**, generated by
|
||||
`resValue`. Two installs cannot own the same account type, so a debug build
|
||||
sharing the release build's would fight it. `SyncContractTest` asserts the
|
||||
Kotlin constants and the generated strings still agree — drift between them is
|
||||
invisible at build time and surfaces as an account the framework will not
|
||||
trigger.
|
||||
- **The credential blob is the one thing excluded from backup.** It lives in its
|
||||
own DataStore file for exactly that reason. Keystore keys are non-exportable,
|
||||
so a restored ciphertext is permanently undecryptable; excluding it means the
|
||||
user signs in again, which is the honest outcome. Excluding the database or all
|
||||
of DataStore would trade a latent bug for a live one.
|
||||
- ⚠️ **The network security config trusts the whole user CA store, for all
|
||||
traffic.** `SYNC.md` specifies it, and without it a correctly installed private
|
||||
CA is not trusted at all — which is the self-hosting case. But `base-config` is
|
||||
the widest form: any CA in the user store can intercept the CalDAV connection
|
||||
and read the app password from the `Authorization` header. **Chunk 5's
|
||||
cert4android should replace this block, not sit beside it** — its per-connection
|
||||
approval is the narrow version of the same capability.
|
||||
- **`FOREGROUND_SERVICE` appears in the merged manifest** without being declared,
|
||||
from `work-runtime`. That is not the FGS route: below API 31 WorkManager
|
||||
implements expedited work with a foreground service, and minSdk is 29. Noted in
|
||||
the manifest because it shows in F-Droid's permission diff.
|
||||
|
||||
**Not run:** the instrumented tests here (`CredentialStoreTest`,
|
||||
`SyncContractTest`) compile but have not been executed — `CLAUDE.md` reserves
|
||||
device interaction for when it is explicitly asked for. They need a device run
|
||||
before chunk 2 is done.
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2d — the account-add UI
|
||||
|
||||
**Goal:** the app can add a CalDAV account and list its VTODO collections. No
|
||||
syncing yet.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user