Skip to content

fix(security): keep system data out of Android backup, fail dev builds without a keystore#33

Merged
SiteRelEnby merged 3 commits into
mainfrom
fix/backup-and-dev-signing
Jul 14, 2026
Merged

fix(security): keep system data out of Android backup, fail dev builds without a keystore#33
SiteRelEnby merged 3 commits into
mainfrom
fix/backup-and-dev-signing

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Collaborator

First of three PRs from the latest review. This one is the security batch.

Backup was shipping system data to Google

allowBackup="true" on both modules, with exclusion rules that covered almost nothing:

Eligible for backup today Where
sheaf.db (members, groups, front history, messages) phone, domain="database", never excluded
pending offline switch / removal queue phone, same DB
widget_config.xml phone
wear_auth_secure.xml (encrypted companion session) watch
wear_auth_signal.xml, tile_data.xml (switch queue, history, cached fronters) watch
tile_config.xml, complication_config.xml watch

Only datastore/ (the phone token store) was excluded.

The watch rules did exclude wear_auth.xml - but that is the legacy plaintext file that
migrateLegacyPlaintextIfNeeded() deletes. The exclusion list was never updated when the store was
encrypted and renamed to wear_auth_secure.xml, so the rules named a file that no longer exists
while the live credential store went to the cloud.

Restoring any of it would also be broken, not just leaky: the wear store is Keystore-bound, so
restored ciphertext cannot be decrypted on the new device, and a restored token would have been
revoked server-side anyway.

Fix: opt out entirely. allowBackup="false" on both modules, and exclude every domain from both
<cloud-backup> and <device-transfer>. The second part matters: allowBackup="false" only kills the
cloud snapshot, D2D transfer still runs on API 31+ unless the extraction rules say otherwise.

Trade-off, worth being explicit about: theme, base URL and widget config no longer survive a reinstall
or a device transfer. Everything that actually matters is server-side and comes back on sign-in, and the
watch re-pairs from the phone. For an app whose local cache is a list of who is in someone system, a
Google-held copy of that is not a trade I think we want to make for the convenience of not retyping a
server URL.

Dev releases could ship debug-signed

dev-release.yml fell through to assembleOpenDebug when KEYSTORE_BASE64 was absent. AGP signs debug
variants with the default debug keystore, and nothing downstream could tell: the locate step globs any
APK it finds, cosign signs it, and it gets published to the dev release with the real dev versionCode.
Now it hard-fails, the way tagged-release.yml already does. Also moved the keystore cleanup to a trap
so a failed build cannot leave the decoded keystore on disk.

Verification

  • :app:assemblePlayRelease :wear:assembleRelease green.
  • Worth a device pass: install, sign in, confirm nothing regressed (the manifest change has no runtime
    behaviour beyond backup eligibility).

Follow-ups (tracked, not in this PR)

  • Verify no leftover backup data on already-affected devices; Android drops the app from backup once
    allowBackup=false ships, but existing cloud snapshots are not retroactively purged. Might be worth a
    note in the release blurb.

…s without a keystore

Backup was on for both modules and the exclusion lists barely covered
anything, so a Google-held backup of the device could contain:

- sheaf.db, the local cache: members, groups, front history, messages
- the pending offline switch/removal queue
- on the watch, wear_auth_secure.xml (the encrypted companion session),
  wear_auth_signal.xml, and tile_data.xml (queue, history, cached fronters)

The watch rules excluded "wear_auth.xml", which is the legacy plaintext store
that migration deletes. So the rules named a file that no longer exists while
the live credential store shipped to the cloud. Restoring it would not even
work: the store is Keystore-bound, so the ciphertext is undecryptable on the
restoring device, and any restored token would have been revoked anyway.

Opt out entirely: allowBackup=false on both modules, and exclude every domain
from both cloud-backup and device-transfer (allowBackup=false does not disable
D2D transfer on API 31+, only the cloud snapshot). Everything here is
recoverable by signing in again, and the watch re-pairs from the phone.

Separately, dev-release.yml quietly fell back to assembleOpenDebug when
KEYSTORE_BASE64 was missing, which AGP signs with the default debug keystore.
Nothing downstream distinguished it: the locate step globbed any APK, cosign
signed it, and it got published under the real dev version. Fail hard instead,
matching what the tagged-release workflow already does.
@SiteRelEnby
SiteRelEnby enabled auto-merge July 14, 2026 14:28
@SiteRelEnby
SiteRelEnby merged commit 0bd295e into main Jul 14, 2026
1 check passed
@SiteRelEnby
SiteRelEnby deleted the fix/backup-and-dev-signing branch July 14, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant