How to write thread-safe code for Android Keystore
What is Android Keystore
The Android Keystore provides access to special secure hardware for storing cryptographic keys. From Android 6.0, the security of Android Keystore was significantly enhanced, because Android started to support hardware-backed secure storage, then key material may be bound to the secure hardware inside the device, e.g., Trusted Execution Environment (TEE), Secure Element (SE).
Why Android Keystore is not thread-safe
Android provides a set of common APIs and algorithms for applications to use Android Keystore. But these APIs are just a wrapper layer, the actual calculation and storage are done by the secure hardware. There is only one single hardware chip doing all these things, once multiple applications or multiple threads are trying to access the hardware at same time, conflicts will happen, and below exceptions may be thrown from Android Keystore at different stages of operations.
During key generate stage, it will throw:
java.security.ProviderException: Keystore operation failed
at android.security.keystore.AndroidKeyStoreKeyGeneratorSpi.engineGenerateKey(AndroidKeyStoreKeyGeneratorSpi.java:324)
at javax.crypto.KeyGenerator.generateKey(KeyGenerator.java:612)
During crypto calculation stage, it can throw:
javax.crypto.IllegalBlockSizeException
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:486)
at javax.crypto.Cipher.doFinal(Cipher.java:1502)
Solution
The solution for these exceptions are simple, we just need to make sure all Android Keystore operations are thread-safe. The simplest solution is, all the methods in your code to call generateKey()
, generateKeyPair()
,getKey()
, and all crypto operations to Android Keystore should be tagged as synchronized
method.
Is the latest Jetpack Security library thread-safe
Google has released the first version of security library on top of Android keystore. The latest version is:
androidx.security:security-crypto:1.0.0-rc03
It provides 2 simple but yet powerful classes for developers to store encrypted data to file system: EncryptedFile
and EncryptedSharedPreferences
.
Unfortunately, Google has clearly told us in this document,
Note: The methods in both the
EncryptedFile
class and theEncryptedSharedPreferences
class aren't thread-safe.
If we don’t follow this, we will likely get the same crash as we use the Android Keystore directly, and someone has reported it here.
No matter when we create EncryptedFile
or EncryptedSharedPreferences
, or when we read and write data from them, we need to make sure they are thread-safe. Especially EncryptedFile
is used to read and write big data with files, it required more time for Android keystore to do the operations. In another words, it has higher chance to have those exceptions.
This is an example how to avoid the issue when we create a encrypted shared preference:
@synchronized
fun createSharedPreferences() {
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
prefs = EncryptedSharedPreferences.create(
"file-name",
masterKeyAlias,
context,
PrefKeyEncryptionScheme.AES256_SIV,
PrefValueEncryptionScheme.AES256_GCM)
}
Conclusion
Android Keystore operation is not thread-safe. No matter we use it directly or indirectly, please make sure all the operations are synchronized.
One more thing to mention is if you use Google’s Tink library with Android Keystore, it is also not thread-safe. Because Jetpack’s security library is just another wrapper of Tink library.
Update 26-11-2020
Android Keystore is implemented by the vendors of the phone. Some devices have design flaw in the Android Keystore, and can throw below exceptions out of expectation.
During key query stage, it may throw:
java.security.UnrecoverableKeyException: Failed to obtain information about key
at android.security.keystore.AndroidKeyStoreProvider.getKeyCharacteristics(AndroidKeyStoreProvider.java:234)
at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(AndroidKeyStoreProvider.java:356)
at android.security.keystore.AndroidKeyStoreSpi.engineGetKey(AndroidKeyStoreSpi.java:101)
at java.security.KeyStore.getKey(KeyStore.java:1062)
During crypto calculation stage, it can throw:
javax.crypto.AEADBadTagException
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:517)
at javax.crypto.Cipher.doFinal(Cipher.java:2055)
Once the error happens, it could be recovered soon, it could be recovered in a few hours or even a few days.
To be able to recover quickly by the application, it is suggested to delete the key and re-generate it again.
To reduce the errors, some of the data can be cached in memory if the data is not very sensitive.