fix(data): degrade encrypted cache to plaintext on SQLCipher native-load failure
On Android 15+ / SDK 37 devices with 16 KB memory pages (e.g. Pixel 10 Pro XL, and the API-37 `google_apis_ps16k` emulator image), a native `.so` not aligned for 16 KB pages fails to load with `UnsatisfiedLinkError` at `SQLiteConnection.nativeOpen`. With the opt-in SQLCipher encrypted cache on, this crashed the app on every cold start (issue #359, x4 on-device) instead of degrading, and encryption silently never applied. Fix: DatabaseProvisioner's encryption gate now catches `LinkageError` (UnsatisfiedLinkError and related native-link failures) when opening/converting the encrypted cache and degrades cleanly instead of propagating the crash — it turns `encryptCache` off (so the next start does not re-attempt and re-wipe), clears any on-disk ciphertext the plaintext framework opener cannot parse (resetting its now-useless seals), and opens the cache unencrypted. The cache is a re-syncable copy of server mail, so clearing it loses nothing that cannot be re-fetched. PII-free AppLog.w breadcrumb on the degrade path. Dependency: no bump needed or available. The repo already pins the newest SQLCipher it references, `net.zetetic:sqlcipher-android:4.16.0`, which docs/play-compliance.md certifies (ELF p_align = 0x4000) as 16 KB-aligned on every ABI; SQLCipher has shipped 16 KB-aligned binaries since well before it, and the other two bundled `.so` files (Compose graphics-path, DataStore shared-counter) are already 16 KB-aligned per that doc. The graceful-degrade catch is therefore the actionable fix. Tests: - Unit (DatabaseProvisionerTest): a simulated native-load failure degrades to a plaintext open without crashing, turns encryptCache off, and wipes + reseals an already-encrypted cache. - Instrumented (DatabaseProvisionerInstrumentedTest): a fresh encrypt-on start loads the real SQLCipher native library and opens the keyed cache — CI's API-37 `google_apis_ps16k` 16 KB job exercises the actual `.so` load, catching any future 16 KB-alignment regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+28
@@ -142,6 +142,34 @@ class DatabaseProvisionerInstrumentedTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue #359: with `encryptCache` on and NO cache yet (a fresh install enabling encryption), the
|
||||
* provisioner must load SQLCipher's native library, report [CacheOpenMode.Encrypted], and a real keyed
|
||||
* open must then create and read the encrypted cache — i.e. `libsqlcipher.so` actually loads and runs.
|
||||
*
|
||||
* On a 16 KB memory-page device/image (Android 15+, and the CI API-37 preview `google_apis_ps16k`
|
||||
* E2E image) an `.so` not aligned for 16 KB pages fails exactly here with `UnsatisfiedLinkError` at
|
||||
* `SQLiteConnection.nativeOpen`. Running this on that image makes the 16 KB native-lib load a tested
|
||||
* invariant, so a dependency bump that regressed alignment is caught in CI rather than on-device.
|
||||
*/
|
||||
@Test
|
||||
fun freshEncryptOnStartLoadsThe16KbNativeLibAndOpensKeyedWithoutCrashing() = runBlocking<Unit> {
|
||||
every { settingsRepository.settings } returns flowOf(AppSettings(encryptCache = true, appLock = false))
|
||||
assertFalse("precondition: no cache file exists yet", dbFile.exists())
|
||||
|
||||
val mode = provisioner().prepareCache()
|
||||
|
||||
assertEquals(CacheOpenMode.Encrypted(passphrase), mode)
|
||||
// The keyed open must actually succeed on real SQLCipher — loading and using libsqlcipher.so on
|
||||
// whatever ABI / page size this device or emulator image uses.
|
||||
openEncrypted().apply {
|
||||
messageDao().insertNew(listOf(message("acct:1")))
|
||||
assertEquals(listOf("acct:1"), messageDao().observeSummaries().first().map { it.id })
|
||||
close()
|
||||
}
|
||||
assertTrue("the fresh cache was created in SQLCipher (encrypted) form", DatabaseEncryption.isEncrypted(dbFile))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encryptionTurnedOffDecryptsAnEncryptedCacheToPlaintext() = runBlocking<Unit> {
|
||||
every { settingsRepository.settings } returns flowOf(AppSettings(encryptCache = false, appLock = false))
|
||||
|
||||
Reference in New Issue
Block a user