AEADBadTagException AndroidX Jetpack Security startup crash
Help us help you
We're using Tink via AndroidX Jetpack Security v1.1.0-alpha06 and using the non-deprecated EncryptedSharedPreferences.create method that avoids the race condition when creating a global MasterKey.
The crash happens on: Device - Pixel 7 pro, OS - Android 14
Describe the bug:
The crash isn't consistently reproducible we have crash reports from different devices and across different Android SDK versions.
But according to this stack trace, it's happening on startup during Application.onCreate.
What was the expected behavior?
No crashes.
How can we reproduce the bug?
We're creating an EncryptedSharedPreference instance using the below snippet.
class SharedPreferencesDataSource(
context: Context,
filename: String
) {
companion object {
fun getEncryptedSharedPreferences(context: Context, filename: String) =
EncryptedSharedPreferences.create(
context,
"${filename}_secure",
getOrCreateKey(context),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
private fun getOrCreateKey(context: Context) =
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
}
}
Do you have any debugging information?
I've posted a reproducer below.
What version of Tink are you using?
Jetpack Security is using Tink v 1.8.0 behind the scenes.
Can you tell us more about your development environment? N/A
Is there anything else you'd like to add?
I've read this comment, and mentioned
a) the encrypted keyset has been modified. b) the current master key in keystore is not the same as the master key that was used to encrypt the keyset.
I'm not sure, though, how this can be caused? We're creating EncryptedSharedPreferences instances, but we never touch the MasterKey used to encrypt the data there.
Fortunately, I've managed to reproduce the issue. The issue is related to backup. Ideally it should be solved gracefully handled from within the app, without the need to allowBackup=false
Here's a reproducer project (tested against Android 13)
- Run the project on the device
- Then run
./test_d2d.sh com.example.jetpacksecurityreproducer(Note the backup script is taken from Google's guide to test device 2 device backup. - Try running the app, crashe happens, you'll find the below stacktrace.
I believe a quick solution is to disallow backups; however, IMO, it's not feasible to ask every app that uses Tink/Jetpack security to disallow backups and make the user lose a useful feature such as backups.
FATAL EXCEPTION: main
Process: com.example.jetpacksecurityreproducer, PID: 15116
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.jetpacksecurityreproducer/com.example.jetpacksecurityreproducer.MainActivity}: javax.crypto.AEADBadTagException
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3646)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3783)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7966)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:942)
Caused by: javax.crypto.AEADBadTagException
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:611)
at javax.crypto.Cipher.doFinal(Cipher.java:2114)
at com.google.crypto.tink.integration.android.AndroidKeystoreAesGcm.decryptInternal(AndroidKeystoreAesGcm.java:118)
at com.google.crypto.tink.integration.android.AndroidKeystoreAesGcm.decrypt(AndroidKeystoreAesGcm.java:101)
at com.google.crypto.tink.KeysetHandle.decrypt(KeysetHandle.java:919)
at com.google.crypto.tink.KeysetHandle.readWithAssociatedData(KeysetHandle.java:804)
at com.google.crypto.tink.KeysetHandle.read(KeysetHandle.java:785)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.readMasterkeyDecryptAndParseKeyset(AndroidKeysetManager.java:381)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.build(AndroidKeysetManager.java:297)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:169)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:130)
at com.example.jetpacksecurityreproducer.MainActivity$sharedPreferences$2.invoke(MainActivity.kt:19)
at com.example.jetpacksecurityreproducer.MainActivity$sharedPreferences$2.invoke(MainActivity.kt:18)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at com.example.jetpacksecurityreproducer.MainActivity.getSharedPreferences(MainActivity.kt:18)
at com.example.jetpacksecurityreproducer.MainActivity.onCreate(MainActivity.kt:31)
at android.app.Activity.performCreate(Activity.java:8348)
at android.app.Activity.performCreate(Activity.java:8327)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1427)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3627)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3783)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7966)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:942)
Caused by: android.security.KeyStoreException: Signature/MAC verification failed (internal Keystore code: -30 message: In KeystoreOperation::finish
Caused by:
0: In finish: KeyMint::finish failed.
1: Error::Km(ErrorCode(-30))) (public error code: 10 internal Keystore code: -30)
at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:369)
at android.security.KeyStoreOperation.handleExceptions(KeyStoreOperation.java:78)
at android.security.KeyStoreOperation.finish(KeyStoreOperation.java:128)
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer$MainDataStream.finish(KeyStoreCryptoOperationChunkedStreamer.java:228)
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:181)
at android.security.keystore2.AndroidKeyStoreAuthenticatedAESCipherSpi$BufferAllOutputUntilDoFinalStreamer.doFinal(AndroidKeyStoreAuthenticatedAESCipherSpi.java:396)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:603)
... 31 more
Thanks for the detailed report. Others have had the same issue before, that's why a warning was added to not back up these files: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java;l=54;bpv=1 Instead of disabling backup, you can also just exclude the encrypted files, see https://developer.android.com/guide/topics/data/autobackup#IncludingFiles
There is nothing else we can do, unfortunately. The keys stored in keystore are not backed up. So if you restore the backup, you don't have the key anymore, and therefore you can't decrypt anymore.
Also, note that Android will encrypt the data by default, see https://source.android.com/docs/security/features/encryption/file-based.
Thanks @juergw for the response.
Does that mean a solution would be, to handle this without declaring backup rules, during EncryptedSharedPreferences.create, would be to catch GeneralSecurityException and delete the existing keys from the KeyStore, and delete the shared prefs files themselves and recreate them? (I know this would make existing data to be deleted, but I don't believe it will matter that much when an app is being reinstalled)
Also, note that Android will encrypt the data by default,
If this was the case, Do you think storing the keysets into plain text format rather than on Android's KeyStore would be more reliable during backup/restore? (Not sure about the Security implications of this though)
I'd also like to add that it turned out that even with allowBackup="false" the same crash still happens on some devices (I couldn't figure out how to reproduce this though), so it looks like it can happen even outside the realm of Auto backup.
It would be enough to delete the shared preferences, there is no need to delete the (newly created) key in keystore.
I'm experiencing this issue (repeatable) on a single device but not others: Pixel 7 running Android 14 (up to date with latest official updates). I'm using the same versions of jetpack as mentioned. I have tried: disabling backups via the manifest and I have tried excluding the encrypted files. Neither work, it does not appear to be the cause of the problem. Is it possible a past backup would cause the issue even after the manifest and/or backup rules were updated?
I have also factory reset the device and the error still occurs.
in my case, a clean install of the app on that device, launches and works. if I kill the app and restart (all future restarts) fail with the error from this report. In my case, deleting the prefs is not a working solution.
I have also discovered that once EncryptedSharedPreferences.create shows the error so does EncryptedFile.Builder.
As a follow-up, I was finally able to get the app to no longer have this failure. I used Device Explorer in Android Studio to manually delete the offending shared prefs file and let the app recreate it.
This solution that targets the scenario when the encrypted file is backed up and system restored it, but lost the key used for encryption (e.g. app was uninstalled and installed again).
Here is the gist if someone still needed it: https://gist.github.com/rynkowsg/86ebd680a67669dfcece4cc9ec9974df The approach there relies on recreating the shared preferences when they can not be read.
Tested with "androidx.security:security-crypto:1.1.0-alpha06".
In addition, it is good to explicitly set which files should be backed up, with:
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
tools:targetApi="upside_down_cake"
android:dataExtractionRules for API >= 31
android:fullBackupContent for API < 31
tools:targetApi to fix warning, set to your compile sdk version
I think the main problem here is that both jetpack and Tink tried to give a too simple API to the user. That API works fine in most cases, but when something goes wrong, it is much more difficult for the user of the API to figure out what is happening. So we have decided to provide a more low-level API that forces the user to write more code themselves. But it makes it easier to understand what is going on, so I think it will be easier to maintain.
Here is the new API: https://github.com/tink-crypto/tink-java/blob/main/src/main/java/com/google/crypto/tink/integration/android/AndroidKeystore.java
and here is how a user might use this API in their API: https://github.com/tink-crypto/tink-java/blob/main/examples/android/helloworld/app/src/main/java/com/helloworld/TinkApplication.java