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:
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
The account type is generated per build variant by `resValue` in
|
||||
app/build.gradle.kts, so the debug and releaseTest builds get their own and
|
||||
can sit alongside the real app without their accounts colliding. It must stay
|
||||
in step with SyncContract.ACCOUNT_TYPE.
|
||||
-->
|
||||
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@string/account_type"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:smallIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name" />
|
||||
@@ -19,4 +19,15 @@
|
||||
<include domain="database" path="agendula-tasks.db-wal" />
|
||||
<include domain="database" path="agendula-tasks.db-shm" />
|
||||
<include domain="file" path="datastore/" />
|
||||
<!--
|
||||
The one thing that must not travel. Keystore keys are non-exportable, so
|
||||
a restored ciphertext can never be decrypted again — it would surface as
|
||||
an account that silently stops syncing with no way to tell why. Excluding
|
||||
it means the user signs in again on a new device, which is the honest
|
||||
outcome. Note this is the *only* exclusion that is right here:
|
||||
docs/SYNC.md is explicit that dropping the database or all of DataStore
|
||||
from backup would trade a latent bug for a live one, since Auto Backup is
|
||||
Local mode's only automatic safety net.
|
||||
-->
|
||||
<exclude domain="file" path="datastore/agendula_credentials.preferences_pb" />
|
||||
</full-backup-content>
|
||||
|
||||
@@ -11,11 +11,14 @@
|
||||
<include domain="database" path="agendula-tasks.db-wal" />
|
||||
<include domain="database" path="agendula-tasks.db-shm" />
|
||||
<include domain="file" path="datastore/" />
|
||||
<!-- See backup_rules.xml: a restored ciphertext is undecryptable. -->
|
||||
<exclude domain="file" path="datastore/agendula_credentials.preferences_pb" />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<include domain="database" path="agendula-tasks.db" />
|
||||
<include domain="database" path="agendula-tasks.db-wal" />
|
||||
<include domain="database" path="agendula-tasks.db-shm" />
|
||||
<include domain="file" path="datastore/" />
|
||||
<exclude domain="file" path="datastore/agendula_credentials.preferences_pb" />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Since Android 7 a user who correctly installs their private CA into the
|
||||
system store is *still* not trusted by apps — user CAs are excluded from the
|
||||
default trust anchors. That breaks exactly the self-hosting audience this app
|
||||
is for, so they are added back here.
|
||||
|
||||
Cleartext stays off. It has been the default since API 28, and CalDavDiscovery
|
||||
refuses a typed http:// address for the same reason: credentials are never
|
||||
sent over an unencrypted connection. Any escape hatch has to be a narrow,
|
||||
warned, per-account opt-in — Play's User Data policy requires modern
|
||||
cryptography in transit — and this file is not the place for it.
|
||||
|
||||
⚠️ KNOWN TRADE-OFF, and it is the widest form of this. base-config applies to
|
||||
*all* traffic, so any CA in the user store — a corporate MDM profile, a "free
|
||||
VPN" app's certificate, one installed during some earlier debugging — can
|
||||
transparently intercept the CalDAV connection and read the app password out
|
||||
of the Authorization header. docs/SYNC.md specifies this file, and without it
|
||||
a correctly installed private CA is simply not trusted, which is the
|
||||
self-hosting case the app exists to serve.
|
||||
|
||||
The narrower posture is cert4android's: trust nothing extra by default, and
|
||||
ask the user per connection via its bound service + notification. That is
|
||||
SYNC-PLAN.md chunk 5, and it is what should replace this block rather than
|
||||
sit alongside it.
|
||||
-->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
userVisible="false" keeps our authority out of the per-item sync list in
|
||||
system Settings — there is only one thing to sync and a switch for it adds
|
||||
nothing. The cost is that Settings' "Sync now" stays greyed out, because
|
||||
enabledSyncNowMenu() needs at least one checked authority switch, which is
|
||||
why the app ships its own sync button.
|
||||
|
||||
supportsUploading="true": this is a two-way sync, and declaring otherwise
|
||||
stops the framework requesting a sync on local changes.
|
||||
-->
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:contentAuthority="@string/sync_authority"
|
||||
android:accountType="@string/account_type"
|
||||
android:userVisible="false"
|
||||
android:supportsUploading="true"
|
||||
android:allowParallelSyncs="false"
|
||||
android:isAlwaysSyncable="true" />
|
||||
Reference in New Issue
Block a user