From 90d6eedc901c9dc3f30588d7b2230ccaed817c49 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sun, 16 Aug 2026 08:08:30 +0530 Subject: [PATCH 1/3] Keep app-private data out of backup and device transfer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The extraction rules excluded exactly two SharedPreferences files, and one of them is no longer written: EncryptionService moved its key store from "bitchat_crypto" to "bitchat_crypto_secure". Because these lists are allow-by-default, everything not named is eligible — which is the live key store, SecureIdentityStateManager's "bitchat_identity", the geohash alias and conversation registries, and private_conversations.db, the whole private message history. android:allowBackup="false" covers cloud backup. It does not settle device-to-device migration: for apps targeting Android 12 or higher the platform documents that D2D transfer cannot be disabled on some manufacturers' devices, so the section is the only thing between a phone migration and a copy of the identity keys and every private message. Make both files deny-by-default across every storage domain instead of naming individual files. A name-based list has to be updated every time storage is renamed or added, and this one already wasn't. --- app/src/main/res/xml/backup_rules.xml | 19 ++++++++-- .../main/res/xml/data_extraction_rules.xml | 36 ++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index 4307b01c..d1ac6ee5 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -1,5 +1,20 @@ + - - + + + + + + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml index 3093aa7f..296fc686 100644 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -1,11 +1,39 @@ + - - + + + + + + + + + - - + + + + + + + + + From ad7236b2ba41612d7833565c19b09b624c7c1c6c Mon Sep 17 00:00:00 2001 From: Taksh Date: Sun, 16 Aug 2026 08:08:31 +0530 Subject: [PATCH 2/3] Pin the backup rules to whole-domain exclusion Asserts every domain is excluded in both sections and in the legacy rules, that nothing is opted back in, and that the manifest still wires both files up. Against the previous rules three of the five fail. Whole-domain exclusion is the invariant worth pinning: unlike a list of file names, it cannot go stale when storage is renamed. --- .../android/BackupExclusionContractTest.kt | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 app/src/test/kotlin/com/bitchat/android/BackupExclusionContractTest.kt diff --git a/app/src/test/kotlin/com/bitchat/android/BackupExclusionContractTest.kt b/app/src/test/kotlin/com/bitchat/android/BackupExclusionContractTest.kt new file mode 100644 index 00000000..d8bc6915 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/BackupExclusionContractTest.kt @@ -0,0 +1,97 @@ +package com.bitchat.android + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File + +/** + * bitchat's app-private storage — the Noise/Nostr key material, the identity store and the + * private conversation database — must never be handed to Auto Backup or to a device-to-device + * transfer. + * + * These rules used to be an allow-by-default list naming two files, and it went stale: it + * excluded "bitchat_crypto" after EncryptionService had already moved to + * "bitchat_crypto_secure". So the invariant pinned here is deny-by-default over whole domains, + * which cannot go stale when storage is renamed or added. + */ +class BackupExclusionContractTest { + + private val resourcesDirectory = File("src/main/res") + private val manifest = File("src/main/AndroidManifest.xml") + + private val allDomains = setOf( + "root", + "file", + "database", + "sharedpref", + "external", + "device_root", + "device_file", + "device_database", + "device_sharedpref" + ) + + private fun rulesFile(name: String) = File(resourcesDirectory, "xml/$name").readText() + + /** Domains excluded at path "." within [section], or across the file when it has no sections. */ + private fun excludedDomains(xml: String, section: String?): Set { + val scope = if (section == null) { + xml + } else { + Regex("""<$section[^>]*>(.*?)""", RegexOption.DOT_MATCHES_ALL) + .find(xml) + ?.groupValues + ?.get(1) + ?: fail("missing <$section> section") + } + return Regex("""""") + .findAll(scope) + .map { it.groupValues[1] } + .toSet() + } + + private fun fail(message: String): Nothing = throw AssertionError(message) + + @Test + fun `cloud backup excludes every storage domain`() { + assertEquals( + allDomains, + excludedDomains(rulesFile("data_extraction_rules.xml"), "cloud-backup") + ) + } + + @Test + fun `device to device transfer excludes every storage domain`() { + // allowBackup=false does not reliably disable D2D migration on Android 12+, so this + // section is the only thing protecting a phone migration. + assertEquals( + allDomains, + excludedDomains(rulesFile("data_extraction_rules.xml"), "device-transfer") + ) + } + + @Test + fun `legacy auto backup rules exclude every storage domain`() { + assertEquals(allDomains, excludedDomains(rulesFile("backup_rules.xml"), null)) + } + + @Test + fun `no rule opts any path back in`() { + for (name in listOf("data_extraction_rules.xml", "backup_rules.xml")) { + assertFalse( + "$name must not re-include anything", + rulesFile(name).contains("