refactor: extract EncryptedSharedPreferences into SecurePreferencesFactory with auto-recovery (batch v0.2.21)
- New SecurePreferencesFactory handles EncryptedSharedPreferences creation - Auto-resets and recreates on corruption (unreadable prefs) - CoupleKeyStore and SharedPreferencesLocalAnswerRepository use the factory - Removes duplicated EncryptedSharedPreferences boilerplate
This commit is contained in:
parent
8be7b7da0e
commit
700201bbd6
|
|
@ -2,8 +2,7 @@ package app.closer.crypto
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import app.closer.data.local.SecurePreferencesFactory
|
||||
import com.google.crypto.tink.Aead
|
||||
import com.google.crypto.tink.CleartextKeysetHandle
|
||||
import com.google.crypto.tink.JsonKeysetReader
|
||||
|
|
@ -24,16 +23,8 @@ import javax.inject.Singleton
|
|||
class CoupleKeyStore @Inject constructor(
|
||||
@ApplicationContext context: Context
|
||||
) {
|
||||
private val prefs: SharedPreferences = run {
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
EncryptedSharedPreferences.create(
|
||||
masterKeyAlias,
|
||||
"couple_crypto_secure",
|
||||
context,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
private val prefs: SharedPreferences =
|
||||
SecurePreferencesFactory.encryptedSharedPreferences(context, PREFS_NAME)
|
||||
|
||||
private val aeadCache = ConcurrentHashMap<String, Aead>()
|
||||
|
||||
|
|
@ -110,4 +101,8 @@ class CoupleKeyStore @Inject constructor(
|
|||
val json = prefs.getString(key, null) ?: return null
|
||||
CleartextKeysetHandle.read(JsonKeysetReader.withString(json))
|
||||
}.getOrNull()
|
||||
|
||||
private companion object {
|
||||
const val PREFS_NAME = "couple_crypto_secure"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package app.closer.data.local
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import java.security.KeyStore
|
||||
|
||||
object SecurePreferencesFactory {
|
||||
private const val TAG = "SecurePreferences"
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
private const val MASTER_KEY_ALIAS = "_androidx_security_master_key_"
|
||||
|
||||
fun encryptedSharedPreferences(
|
||||
context: Context,
|
||||
prefsName: String
|
||||
): SharedPreferences {
|
||||
val appContext = context.applicationContext
|
||||
return runCatching {
|
||||
create(appContext, prefsName)
|
||||
}.getOrElse { firstError ->
|
||||
Log.w(TAG, "Resetting unreadable encrypted preferences: $prefsName", firstError)
|
||||
reset(appContext, prefsName)
|
||||
create(appContext, prefsName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun create(context: Context, prefsName: String): SharedPreferences {
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
return EncryptedSharedPreferences.create(
|
||||
prefsName,
|
||||
masterKeyAlias,
|
||||
context,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
|
||||
private fun reset(context: Context, prefsName: String) {
|
||||
context.deleteSharedPreferences(prefsName)
|
||||
context.deleteSharedPreferences(MASTER_KEY_ALIAS)
|
||||
runCatching {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
if (keyStore.containsAlias(prefsName)) {
|
||||
keyStore.deleteEntry(prefsName)
|
||||
}
|
||||
if (keyStore.containsAlias(MASTER_KEY_ALIAS)) {
|
||||
keyStore.deleteEntry(MASTER_KEY_ALIAS)
|
||||
}
|
||||
}.onFailure { error ->
|
||||
Log.w(TAG, "Could not delete Android Keystore master key.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,7 @@ package app.closer.data.repository
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import app.closer.data.local.SecurePreferencesFactory
|
||||
import app.closer.domain.model.LocalAnswer
|
||||
import app.closer.domain.repository.LocalAnswerRepository
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -24,15 +23,7 @@ class SharedPreferencesLocalAnswerRepository @Inject constructor(
|
|||
// Remove legacy plaintext file on first migration
|
||||
context.deleteSharedPreferences("local_answers")
|
||||
|
||||
// In security-crypto 1.0.0, EncryptedSharedPreferences.create takes (alias, name, context, ...)
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
EncryptedSharedPreferences.create(
|
||||
masterKeyAlias, // alias (first param)
|
||||
"local_answers_secure", // name (second param)
|
||||
context, // context (third param)
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
SecurePreferencesFactory.encryptedSharedPreferences(context, PREFS_NAME)
|
||||
}
|
||||
private val answers = MutableStateFlow(readAnswers())
|
||||
|
||||
|
|
@ -132,5 +123,6 @@ class SharedPreferencesLocalAnswerRepository @Inject constructor(
|
|||
|
||||
private companion object {
|
||||
const val KEY_ANSWERS = "answers"
|
||||
const val PREFS_NAME = "local_answers_secure"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue