Skip to content

fix(android): recover when the keychain file is restored without its Keystore key - #713

Merged
kipavy merged 2 commits into
devfrom
fix/android-keychain-restore
Oct 24, 2026
Merged

kipavy merged 2 commits into
devfrom
fix/android-keychain-restore

Conversation

@kipavy

@kipavy kipavy commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Problem

After uninstalling the GitHub/Obtainium APK and installing the Play Store build, entering the master password fails with:

Keychain read error: Platform failure: keychain get: Java exception was thrown

Confirmed on a OnePlus (Android 15) over adb:

  • Both builds are signed with the same key (FB:08:A9:35…), so Google backup restored the app's data at install (restore_package: [com.voltius.app,19308]).
  • Uninstalling deleted the Keystore master key, which is never backed up. The restored shared_prefs/voltius_secrets.xml can't be decrypted with the new key:
javax.crypto.AEADBadTagException
  at com.google.crypto.tink.KeysetHandle.decrypt
  at androidx.security.crypto.EncryptedSharedPreferences.create
  at com.voltius.app.VoltiusKeychain.prefs
Caused by: android.security.KeyStoreException: Signature/MAC verification failed (... VERIFICATION_FAILED)

Fix

  • Manifest: allowBackup="false", plus data_extraction_rules.xml excluding voltius_secrets.xml from cloud backup and device transfer. On Android 12+, allowBackup="false" alone doesn't stop device-to-device transfer.
  • VoltiusKeychain.prefs(): on GeneralSecurityException / IOException, rename the file to voltius_secrets.xml.<unix_millis>.bak (same naming and keep-newest-3 rule as secrets_quarantine), then open a fresh store. Nothing is deleted and the Keystore alias stays, so a set-aside file can still be recovered if the failure was temporary. @Synchronized so parallel reads can't set aside a store another thread just created.
  • android_ctx::with_env: errors now name the Java exception and its causes instead of "Java exception was thrown".
  • Splash: a secrets.enc with no account_id in the keychain went to the password form, which could only fail with "No account found", because the password key is derived from that id. It now goes to the existing vault-unreadable panel ("Set aside and start fresh"). Logout deletes secrets.enc, so normal flows never reach this state.

Why recover in Kotlin rather than return a typed error to the frontend: about 15 callers read the keychain, and the restored data can never be decrypted (its key never left the old install), so asking the user wouldn't change the outcome.

Testing

On the phone, debug build, with the exact broken state recreated (log in → save app data → uninstall → reinstall → restore the data):

  • The keychain file is set aside (VoltiusKeychain: Could not decrypt voltius_secrets, set it aside as voltius_secrets.xml.1792841115638.bak) and the app starts without a keychain error.
  • The splash shows the vault-unreadable panel. "Set aside and start fresh" → cloud sign-in → hosts synced.
  • After an app restart the session is restored from the keychain.
  • With the set-aside forced to fail (shared_prefs made read-only), keychain_get returns Keychain read error: Platform failure: keychain get: javax.crypto.AEADBadTagException, caused by: android.security.KeyStoreException: Signature/MAC verification failed (...).
  • vitest (splash + account suites, 179 tests including 2 new) and tsc --noEmit pass.

Users already on 0.63.0 still have backup enabled, so they need this release before an uninstall/reinstall is safe. Until then: Settings → Apps → Voltius → Storage → Clear storage.

🤖 Generated with Claude Code

https://claude.ai/code/session_016Q4DupsYZYovyJ677XMLXB

…eystore key

Uninstalling deletes the Android Keystore master key, which is never backed
up, but Auto Backup restored shared_prefs/voltius_secrets.xml on reinstall.
EncryptedSharedPreferences.create() then threw AEADBadTagException and every
keychain read failed with "Java exception was thrown".

- Disable Auto Backup and exclude voltius_secrets.xml from cloud backup and
  device transfer.
- When the file can't be decrypted, rename it to
  voltius_secrets.xml.<unix_millis>.bak (newest 3 kept) and open a fresh
  store. The Keystore key is left in place.
- JNI errors now name the Java exception and its causes.
Once the keychain is reset, secrets.enc can remain without the account_id its
password key is derived from. The unlock form could only fail with "No account
found"; the vault-unreadable panel lets the user set it aside and sign in.
@kipavy
kipavy merged commit 229e8a4 into dev Oct 24, 2026
5 checks passed
@kipavy
kipavy deleted the fix/android-keychain-restore branch October 24, 2026 12:19
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