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
719fb0018d
commit
16d8425071
|
|
@ -2,8 +2,7 @@ package app.closer.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import app.closer.data.local.SecurePreferencesFactory
|
||||||
import androidx.security.crypto.MasterKeys
|
|
||||||
import com.google.crypto.tink.Aead
|
import com.google.crypto.tink.Aead
|
||||||
import com.google.crypto.tink.CleartextKeysetHandle
|
import com.google.crypto.tink.CleartextKeysetHandle
|
||||||
import com.google.crypto.tink.JsonKeysetReader
|
import com.google.crypto.tink.JsonKeysetReader
|
||||||
|
|
@ -24,16 +23,8 @@ import javax.inject.Singleton
|
||||||
class CoupleKeyStore @Inject constructor(
|
class CoupleKeyStore @Inject constructor(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context
|
||||||
) {
|
) {
|
||||||
private val prefs: SharedPreferences = run {
|
private val prefs: SharedPreferences =
|
||||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
SecurePreferencesFactory.encryptedSharedPreferences(context, PREFS_NAME)
|
||||||
EncryptedSharedPreferences.create(
|
|
||||||
masterKeyAlias,
|
|
||||||
"couple_crypto_secure",
|
|
||||||
context,
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val aeadCache = ConcurrentHashMap<String, Aead>()
|
private val aeadCache = ConcurrentHashMap<String, Aead>()
|
||||||
|
|
||||||
|
|
@ -110,4 +101,8 @@ class CoupleKeyStore @Inject constructor(
|
||||||
val json = prefs.getString(key, null) ?: return null
|
val json = prefs.getString(key, null) ?: return null
|
||||||
CleartextKeysetHandle.read(JsonKeysetReader.withString(json))
|
CleartextKeysetHandle.read(JsonKeysetReader.withString(json))
|
||||||
}.getOrNull()
|
}.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.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import app.closer.data.local.SecurePreferencesFactory
|
||||||
import androidx.security.crypto.MasterKeys
|
|
||||||
import app.closer.domain.model.LocalAnswer
|
import app.closer.domain.model.LocalAnswer
|
||||||
import app.closer.domain.repository.LocalAnswerRepository
|
import app.closer.domain.repository.LocalAnswerRepository
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
@ -24,15 +23,7 @@ class SharedPreferencesLocalAnswerRepository @Inject constructor(
|
||||||
// Remove legacy plaintext file on first migration
|
// Remove legacy plaintext file on first migration
|
||||||
context.deleteSharedPreferences("local_answers")
|
context.deleteSharedPreferences("local_answers")
|
||||||
|
|
||||||
// In security-crypto 1.0.0, EncryptedSharedPreferences.create takes (alias, name, context, ...)
|
SecurePreferencesFactory.encryptedSharedPreferences(context, PREFS_NAME)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
private val answers = MutableStateFlow(readAnswers())
|
private val answers = MutableStateFlow(readAnswers())
|
||||||
|
|
||||||
|
|
@ -132,5 +123,6 @@ class SharedPreferencesLocalAnswerRepository @Inject constructor(
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val KEY_ANSWERS = "answers"
|
const val KEY_ANSWERS = "answers"
|
||||||
|
const val PREFS_NAME = "local_answers_secure"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue