背景
在我工作的一个应用程序上,我将重要的东西(令牌(存储到EncryptedSharedPreferences中(取自此处和这里(:
/** a hardware-encrypted based shared preference (for the values).
* Note that it is a bit slow, so it's better to always use it in a background thread.
* Also, avoid having it being backed-up in the manifest, as it's hardware based and will become useless: https://stackoverflow.com/a/63795282/878126*/
object SecuredSharedPreferences {
private var cachedDefaultSharedPreferences: SharedPreferences? = null
/**warning: using this function can take some time (249 ms on Pixel 4, for example). Very recommended to avoid calling it on UI thread */
@WorkerThread
fun getDefaultSecuredSharedPreferences(context: Context): SharedPreferences {
if (cachedDefaultSharedPreferences != null)
return cachedDefaultSharedPreferences!!
synchronized(this) {
if (cachedDefaultSharedPreferences != null)
return cachedDefaultSharedPreferences!!
cachedDefaultSharedPreferences = getSecuredSharedPreferences(context, context.packageName + "_secured_preferences")
}
return cachedDefaultSharedPreferences!!
}
@WorkerThread
private fun getSecuredSharedPreferences(context: Context, fileName: String): SharedPreferences {
val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create(context, fileName, masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
等级:
implementation 'androidx.security:security-crypto:1.1.0-alpha03'
问题
我注意到在使用此代码时通过Crashlytics报告了2个错误(此处报告了(:
- 第一个是
GeneralSecurityException
的MasterKey.Builder
线:
Fatal Exception: java.security.GeneralSecurityException: Keystore operation failed
at androidx.security.crypto.MasterKeys.generateKey(MasterKeys.java:146)
at androidx.security.crypto.MasterKeys.getOrCreate(MasterKeys.java:97)
at androidx.security.crypto.MasterKey$Builder.buildOnM(MasterKey.java:357)
at androidx.security.crypto.MasterKey$Builder.build(MasterKey.java:314)
...
Caused by java.security.ProviderException: Keystore operation failed
at android.security.keystore.AndroidKeyStoreKeyGeneratorSpi.engineGenerateKey(AndroidKeyStoreKeyGeneratorSpi.java:372)
at javax.crypto.KeyGenerator.generateKey(KeyGenerator.java:612)
at androidx.security.crypto.MasterKeys.generateKey(MasterKeys.java:142)
at androidx.security.crypto.MasterKeys.getOrCreate(MasterKeys.java:97)
at androidx.security.crypto.MasterKey$Builder.buildOnM(MasterKey.java:357)
at androidx.security.crypto.MasterKey$Builder.build(MasterKey.java:314)
- 第二个在
KeyStoreException
的EncryptedSharedPreferences.create
线上,发生频率更高,适用于更多用户:
Fatal Exception: java.security.KeyStoreException: the master key android-keystore://_androidx_security_master_key_ exists but is unusable
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.readOrGenerateNewMasterKey(AndroidKeysetManager.java:275)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.build(AndroidKeysetManager.java:236)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:155)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:120)
...
Caused by java.security.UnrecoverableKeyException: Failed to obtain information about key
at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreSecretKeyFromKeystore(AndroidKeyStoreProvider.java:282)
at android.security.keystore.AndroidKeyStoreSpi.engineGetKey(AndroidKeyStoreSpi.java:98)
at java.security.KeyStore.getKey(KeyStore.java:825)
at com.google.crypto.tink.integration.android.AndroidKeystoreAesGcm.<init>(AndroidKeystoreAesGcm.java:58)
at com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient.getAead(AndroidKeystoreKmsClient.java:164)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.readOrGenerateNewMasterKey(AndroidKeysetManager.java:267)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.build(AndroidKeysetManager.java:236)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:155)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:120)
编辑:似乎还有更多类型的例外:
- InvalidProtocolBufferException:
Fatal Exception: com.google.crypto.tink.shaded.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero).
at com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:1566)
at com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:1663)
at com.google.crypto.tink.proto.Keyset.parseFrom(Keyset.java:957)
at com.google.crypto.tink.integration.android.SharedPrefKeysetReader.read(SharedPrefKeysetReader.java:84)
at com.google.crypto.tink.CleartextKeysetHandle.read(CleartextKeysetHandle.java:58)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.read(AndroidKeysetManager.java:328)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.readOrGenerateNewKeyset(AndroidKeysetManager.java:287)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.build(AndroidKeysetManager.java:238)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:160)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:120)
- NullPointerException:
Fatal Exception: java.lang.NullPointerException: Attempt to invoke interface method 'android.security.keymaster.OperationResult android.security.IKeystoreService.begin(android.os.IBinder, java.lang.String, int, boolean, android.security.keymaster.KeymasterArguments, byte[], int)' on a null object reference
at android.security.KeyStore.begin(KeyStore.java:501)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:248)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:109)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2977)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2884)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2789)
at javax.crypto.Cipher.chooseProvider(Cipher.java:956)
at javax.crypto.Cipher.init(Cipher.java:1199)
at javax.crypto.Cipher.init(Cipher.java:1143)
at com.google.crypto.tink.integration.android.AndroidKeystoreAesGcm.encryptInternal(AndroidKeystoreAesGcm.java:84)
at com.google.crypto.tink.integration.android.AndroidKeystoreAesGcm.encrypt(AndroidKeystoreAesGcm.java:72)
at com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient.validateAead(AndroidKeystoreKmsClient.java:248)
at com.google.crypto.tink.integration.android.AndroidKeystoreKmsClient.getAead(AndroidKeystoreKmsClient.java:165)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.readOrGenerateNewMasterKey(AndroidKeysetManager.java:267)
at com.google.crypto.tink.integration.android.AndroidKeysetManager$Builder.build(AndroidKeysetManager.java:236)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:155)
at androidx.security.crypto.EncryptedSharedPreferences.create(EncryptedSharedPreferences.java:120)
我尝试过的
在互联网上搜索,我只发现了第一个异常(GeneralSecurityException(的线索,它可能发生在自定义ROM中,因为它们可能无法很好地实现加密的硬件密钥。
事实上,看看Crashlytics上的设备,看看每种设备的安卓版本,我发现它们领先于我所看到的支持它们的最新版本。
遗憾的是,对于第二个例外,我找不到任何解释,也找不到解决方案。我认为这可能与应用程序的恢复有关,但这很奇怪,因为它经常发生。在reddit(这里(上,有人写道,如果出现这样的例外,他选择了wrap the initialization of EncryptedSharedPreferences with "clear all data if fails" and bite the bullet
。还提出,这可能与禁用android:allowBackup
有关(事实确实如此(。
不知道其他的。
问题
为什么会出现这些异常情况?我能对他们做些什么?
清晰的数据是唯一可以做的事情吗?我甚至不确定它是否真的有帮助,因为如果我选择拥有它,这意味着每次事故报告都会消失。。。
是否与android:allowBackup
被禁用有关?
在使用Jetpack Security EncryptedSharedPreferences(即使是稳定的(之前,请注意这个活跃的故障,它严重影响了我们的无故障会话,主要来自外来设备-https://issuetracker.google.com/issues/176215143?pli=1
唯一肮脏的解决方法是-https://github.com/google/tink/issues/535#issuecomment-912661574
一旦我找到一个友好的解决方案,我会更新答案。以下是变通解决方案-
/**
* A builder for creating an encrypted shared preference class.
*/
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
private const val SHARED_PREFS_FILENAME = "TVPrefs"
@KoinApiExtension
class EncryptedSharedPreferenceBuilder(var context: Context) : KoinComponent {
private val reporter: Reporter by inject()
private val masterKeyAlias =
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
fun build(): SharedPreferences {
return try {
createSharedPreferences()
} catch (gsException: GeneralSecurityException) {
reporter.logException(gsException)
Timber.d("EncryptedSharedPref: Error occurred while create shared pref=$gsException")
// There's not much point in keeping data you can't decrypt anymore,
// delete & re-create; user has to start from scratch
deleteSharedPreferences()
createSharedPreferences()
}
}
private fun createSharedPreferences() = EncryptedSharedPreferences.create(
context,
SHARED_PREFS_FILENAME,
masterKeyAlias,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// Clearing getSharedPreferences using default Preference wrapper.
// This is to work around any key-mismatches that may happen.
fun clearSharedPreference() {
context.getSharedPreferences(SHARED_PREFS_FILENAME, Context.MODE_PRIVATE).edit().clear()
.apply()
}
// Workaround [https://github.com/google/tink/issues/535#issuecomment-912170221]
// Issue Tracker - https://issuetracker.google.com/issues/176215143?pli=1
private fun deleteSharedPreferences() {
try {
val sharedPrefsFile =
File("${context.filesDir.parent}/shared_prefs/$SHARED_PREFS_FILENAME.xml")
// Clear the encrypted prefs
clearSharedPreference()
// Delete the encrypted prefs file
if (sharedPrefsFile.exists()) {
val deleted = sharedPrefsFile.delete()
Timber.d("EncryptedSharedPref: Shared pref file deleted=$deleted; path=${sharedPrefsFile.absolutePath}")
} else {
Timber.d("EncryptedSharedPref: Shared pref file non-existent; path=${sharedPrefsFile.absolutePath}")
}
// Delete the master key
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER)
keyStore.load(null)
keyStore.deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
} catch (e: Exception) {
Timber.d("EncryptedSharedPref: Error occurred while trying to reset shared pref=$e")
}
}
}
我认为你认为android:allowBackup=false
是罪魁祸首的本能是正确的。我遇到了类似的问题(当allowBackup设置为false时,加密的共享首选项在应用程序更新时引发错误(。在我的情况下,我通过安装、登录(设置加密的首选项(,然后更新到应用程序的最新版本并尝试读取首选项,成功地进行了复制。我最终通过删除android:allowBackup=false
来解决这个问题