Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions .github/workflows/dev-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,24 @@ jobs:
VCODE: ${{ steps.version.outputs.version_code }}
VNAME: ${{ steps.version.outputs.version_name }}
run: |
if [ -n "$KEYSTORE_BASE64" ]; then
echo "$KEYSTORE_BASE64" | base64 --decode > keystore.jks
./gradlew :app:assembleOpenRelease :wear:assembleOpenRelease \
-PversionCode="$VCODE" \
-PversionName="$VNAME" \
-Pandroid.injected.signing.store.file="$PWD/keystore.jks" \
-Pandroid.injected.signing.store.password="$STORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
rm -f keystore.jks
else
./gradlew :app:assembleOpenDebug :wear:assembleOpenDebug \
-PversionCode="$VCODE" \
-PversionName="$VNAME"
# No silent fallback to a debug build: assembleOpenDebug is signed with
# AGP's default debug keystore, and nothing downstream (locate, cosign,
# publish) can tell the difference, so a missing secret would ship a
# debug-signed APK under the real dev version. Fail loudly instead,
# matching tagged-release.yml.
if [ -z "$KEYSTORE_BASE64" ]; then
echo "ERROR: KEYSTORE_BASE64 secret missing - refusing to publish an unsigned/debug-signed dev build"
exit 1
fi
echo "$KEYSTORE_BASE64" | base64 --decode > keystore.jks
trap 'rm -f "$PWD/keystore.jks"' EXIT
./gradlew :app:assembleOpenRelease :wear:assembleOpenRelease \
-PversionCode="$VCODE" \
-PversionName="$VNAME" \
-Pandroid.injected.signing.store.file="$PWD/keystore.jks" \
-Pandroid.injected.signing.store.password="$STORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"

- name: Locate APKs
id: apk
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ uses semantic versioning (`MAJOR.MINOR.PATCH`).

## [Unreleased]

### Security

- **Your data no longer goes into Android's cloud backup.** The phone and watch
apps now opt out of backup and device-to-device transfer entirely. Previously
the local cache (members, groups, front history, messages), the offline switch
queue and the watch's stored session were all eligible to be copied into a
Google-held backup of your device. Nothing about your system leaves the device
now; everything is restored by signing in again, and a watch re-pairs from the
phone.

### Fixed

- **Signing into another account no longer leaks the previous account's data.**
Expand Down Expand Up @@ -92,6 +102,7 @@ uses semantic versioning (`MAJOR.MINOR.PATCH`).
one. Applies to every importer (Sheaf, PluralKit file and API, Tupperbox,
Prism, OpenPlural, PluralSpace, Simply Plural).


## [1.2.2] - 2026-07-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion sheaf/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<application
android:name=".SheafApplication"
android:allowBackup="true"
android:allowBackup="false"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher"
Expand Down
25 changes: 10 additions & 15 deletions sheaf/app/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Auto Backup rules for API <= 30. DataStore lives at
files/datastore/sheaf_prefs.preferences_pb and contains the user's auth
tokens (phone + watch companion-session) along with theme and base URL.

Excluding the whole datastore directory keeps revoked tokens from being
silently restored from Google Drive on reinstall — which otherwise
bricks watch pairing because the local "watch token already exists"
fast-path short-circuits the secondary-session mint on the phone, and
there's no way to learn server-side that the cached pair has been
revoked (the watch's 401 doesn't propagate back).

Trade-off: theme + base URL also vanish on reinstall. Acceptable; users
re-enter the server URL on first launch anyway, and theme defaults
to "system".
Auto Backup rules for API <= 30. android:allowBackup="false" already disables
backup on these releases; this file is belt-and-braces so that flipping the
flag cannot silently start shipping the cache, the offline queue or the auth
tokens to Google Drive. See data_extraction_rules.xml for the API 31+ rules
and the reasoning.
-->
<full-backup-content>
<exclude domain="file" path="datastore/" />
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</full-backup-content>
35 changes: 25 additions & 10 deletions sheaf/app/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Backup + device-transfer rules for API 31+. Same intent as
backup_rules.xml: keep auth state out of the cloud snapshot AND the
D2D transfer, so a reinstall (or new device) never lands with a
revoked token that the local fast-path treats as valid.
Backup + device-transfer rules for API 31+.

Device-transfer is excluded too on purpose: re-login is the right
behaviour when moving phones, both for security and so the new
device's install gets a fresh secondary session for its paired
watch.
The app opts out of backup entirely (android:allowBackup="false"), but that
flag only disables the cloud snapshot: device-to-device transfer on API 31+
still runs unless it is excluded here. So every domain is excluded from both.

Nothing about a plural system should leave the device in a Google-held
snapshot. The local Room cache (sheaf.db) holds members, groups, front
history and messages; the offline queue holds pending switches; DataStore
holds auth tokens. All of it is recoverable by signing in again, which is
also the right behaviour on a new device: re-login mints a fresh session
(including a fresh secondary session for a paired watch) rather than
restoring a token the server may already have revoked.

Trade-off: theme, base URL and widget config also vanish on reinstall or
device transfer. Acceptable.
-->
<data-extraction-rules>
<cloud-backup>
<exclude domain="file" path="datastore/" />
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</cloud-backup>
<device-transfer>
<exclude domain="file" path="datastore/" />
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</device-transfer>
</data-extraction-rules>
2 changes: 1 addition & 1 deletion sheaf/wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<application
android:name=".WearApp"
android:allowBackup="true"
android:allowBackup="false"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher"
Expand Down
16 changes: 9 additions & 7 deletions sheaf/wear/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Watch-side counterpart to the phone backup rules. wear_auth.xml holds
the companion-session tokens pushed down from the phone; if those are
restored after a reinstall they'll be revoked and just trigger an
endless 401 loop. wear_settings.xml is the user's pref store — also
excluded so we don't have to special-case mixed-stale states.
Auto Backup rules for API <= 30. android:allowBackup="false" already disables
backup there; this file is belt-and-braces so flipping the flag cannot start
shipping the credential store, the offline switch queue or the cached front
state off the watch. See data_extraction_rules.xml for the reasoning.
-->
<full-backup-content>
<exclude domain="sharedpref" path="wear_auth.xml" />
<exclude domain="sharedpref" path="wear_settings.xml" />
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</full-backup-content>
30 changes: 26 additions & 4 deletions sheaf/wear/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Watch-side counterpart to the phone rules. The watch opts out of backup
(android:allowBackup="false"), and everything is excluded from device
transfer as well, since that runs on API 31+ regardless of the flag.

What used to be at risk: wear_auth_secure.xml (the encrypted
companion-session tokens), wear_auth_signal.xml, tile_data.xml (offline
switch queue, front history, cached fronters), and the tile/complication
config. The old rules excluded "wear_auth.xml", which is the legacy
plaintext store that migration deletes: it named a file that no longer
exists while the live one shipped to the cloud.

Restoring any of it is also useless: the credential store is Keystore-bound,
so restored ciphertext cannot be decrypted, and restored tokens would be
revoked anyway. Re-pairing from the phone is the correct recovery path.
-->
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="wear_auth.xml" />
<exclude domain="sharedpref" path="wear_settings.xml" />
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="wear_auth.xml" />
<exclude domain="sharedpref" path="wear_settings.xml" />
<exclude domain="root" path="." />
<exclude domain="file" path="." />
<exclude domain="database" path="." />
<exclude domain="sharedpref" path="." />
<exclude domain="external" path="." />
</device-transfer>
</data-extraction-rules>
Loading