[Background]
프로젝트를 하면서 중요한 정보를 로컬에 저장할 필요가 있을 때 Encypted Shared Preference을 사용하고자 했는데
이상하게 일부 디바이스에서 오류가 발생하는 에러 로그를 볼 수 있었습니다
그래서 이번 글에는 프로젝트에 적용할 수 있는 방법을 알아보고
문제 해결 방법을 알아보고자 합니다.
프로젝트는 Clean Architecture을 적용한 상태입니다.
[적용하기]
먼저 필요한 dependencies을 적용합니다.
dependencies {
implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06")
}
그리고 저는 프로젝트에서는 Hilt을 이용해서 초기화를 해주었습니다.
모든 데이터에 암호화를 걸고 싶지는 않아서 일부 일반 SharedPreference와 EncryptedSharedPreference 두 개를 선언해 줍니다.
둘 다 같은 SharedPreference을 반환하기 때문에 직접 만든 QualifierAnnotation까지 달아서 Hilt가 구분할 수 있게 선언해 줍니다.
@Provides
@Singleton
@EncryptedPreference
fun provideSharedEncrypted(@ApplicationContext context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create(
context,
context.packageName + "_secured_preferences",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, //key 암호화
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, // value 암호화
)
}
@Provides
@Singleton
@DefaultPreference
fun provideShared(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
}
이제 Data Module에서 이 SharedPreference에 저장하고 가져올 수 있는 DataSource을 한 개 만들어줍니다.
class LocalPreferenceDataSourceImpl @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val encryptedSharedPreferences: SharedPreferences
) : LocalPreferenceDataSource {
private val gson = Gson()
override fun <T> save(key: String, value: T, isEncrypted: Boolean) {
val preference = if (isEncrypted) encryptedSharedPreferences else sharedPreferences
when (value) {
is String -> preference.edit {
putString(key, value)
}
is Boolean -> preference.edit {
putBoolean(key, value)
}
is Int -> preference.edit {
putInt(key, value)
}
is List<*> -> {
val gson = GsonBuilder().create()
val jsonArray = gson.toJsonTree(value).asJsonArray
preference.edit {
putString(key, jsonArray.toString())
}
}
else ->
throw IllegalArgumentException("Type not supported ${this.javaClass.simpleName}")
}
}
override fun <T> getValue(key: String, defaultValue: T, isEncrypted: Boolean): T {
val preference = if (isEncrypted) encryptedSharedPreferences else sharedPreferences
return when (defaultValue) {
is String -> preference.getString(key, "") as T
is Boolean -> preference.getBoolean(key, false) as T
is Int -> preference.getLong(key, 0) as T
is List<*> -> preference.getString(key, "") as T
else -> {
throw IllegalArgumentException("Type not supported")
}
}
}
}
일일이 원하는 타입을 overload 하는 방식을 사용할 수 있지만 저는 Generic을 이용해서 원하는 타입을 저장하고 가져오는 방식을 사용했습니다.
이제 이 DataSource을 원하는 곳에서 사용하면 됩니다. 저는 Fragment에서 사용하겠습니다.
viewModel.save("test1", "test", false)
viewModel.save("test2", true, true)
viewModel.save("test3", 12313, true)
viewModel.save("test4", listOf(1, 2, 3, 4), false)
val t1 = viewModel.getValue("test1", "", false)
val t2 = viewModel.getValue("test2", false, true)
val t3 = viewModel.getValue("test3", 123, true)
val t4 = viewModel.getValue("test4", "", false)
Log.d("TESTING LOCAL", "$t1 $t2 $t3 $t4")
잘 뜨는 걸 볼 수 있습니다.
사실 이거만 보고 Encryption이 적용된 건지 판단할 수가 없어서 직접 저장하는 위치에서 확인해 보겠습니다.
view -> Tool Windows -> Device Explorer으로 Device Explorer을 켜줍니다.
그럼 창이 뜰 건데 여기서 data -> data -> 본인 프로젝트 패키지 이름 (com.xxx.xxx) -> shared_prefs
그럼 안에 직접 Hilt에서 선언한 이름대로 만들어진 SharedPreference 파일 두 개를 볼 수 있습니다.
들어가면 넣어준 데이터를 직접 확인할 수 있습니다.
Encryption을 안 한 데이터 2개는 평문
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="test4">[1,2,3,4]</string>
<string name="test1">test</string>
</map>
Encryption을 한 데이터 2개는 암호화가 되어 있습니다.
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="AW9TW7dHan8msV2J15QGrJZawaHy4ymiw9I=">AQSjbzM7jn3peDAmMxcZYT+8Ms0xEKHgrNzRvJOF4mOj7KKYTKgTAnk=</string>
<string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a9012ab9566e5de917f3ff02a632f6aeb775910f94a39f849a0f488e83eaaa11551e55a102628d2c02bb3d75f3521e365198b5edbf5dd3c403593958ec342882ad30ce20e10bf746557b030ea3aa9bc1da73b6ab266b5d5df5828109578a967a2c0076a94f6d6cea6bf0eb5fb166500b9578a00bc60b25dd583443e169fd54b13cc5461e9361f4e8706c7e6410abd4a97900d8dcf0c90d99bf019b796aae09444c2966a689d6295f7528d11a4408b7b7cdfa06123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118b7b7cdfa062001</string>
<string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">1286013031b6153b6cd81b4b0ceccd245f504aab24acc08dd8e12ad99595412af52eb076fcedfc15f92bbbac609dc21634b64757713ace32b0c44911d6a126bfd0697e9fe0adbec8b2913ac2eba7ee317f45967ad8bd918eafd9abe51c04ccb62a6b22e461860fd5dd37677ddccacc65f431eb701e6153051607f04156376601fe15fe13bb4cff2a0d1a4208b3de8d25123b0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118b3de8d252001</string>
<string name="AW9TW7db0RkAAfsva1GPsWUXwq7P0/TRsCo=">AQSjbzPhluP08bqsBsXb186fadQNxoBShZoRl6Xd35JZNDoisTk=</string>
</map>
이걸로 중요한 데이터는 암호화를 한 상태에서 저장을 할 수 있습니다.
[오류 해결]
사실 여기까지 진행하시면서 오류가 안 날 수도 있습니다
그런데 일부 실제 디바이스에서는 이상하게도 오류가 발생합니다.
이 문제가 발생하면 앱이 아예 안 켜지는 문제가 발생합니다.
Caused by javax.crypto.AEADBadTagException:
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:617)
at javax.crypto.Cipher.doFinal(Cipher.java:2114)
Caused by android.security.KeyStoreException: Signature/MAC verification failed (internal Keystore code: -30 message: In KeystoreOperation::finish
재설치하면서 Shared Preference이 제대로 초기화 안되거나, 설정이 잘못되거나 다양한 이유가 있는 것 같지만 다 일일이 대응하기에는 힘들기 아예 코드상으로 에러를 잡는 방식을 택했습니다.
@Provides
@Singleton
@EncryptedPreference
fun provideSharedEncrypted(@ApplicationContext context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return try {
createSharedPreference(context, masterKey)
} catch (gsException: GeneralSecurityException) {
deleteSharedPreference(context)
createSharedPreference(context, masterKey)
}
}
private fun createSharedPreference(context: Context, masterKey: MasterKey): SharedPreferences {
return EncryptedSharedPreferences.create(
context,
context.packageName + "_secured_preferences",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
}
private fun deleteSharedPreference(context: Context) {
try {
val check =
context.deleteSharedPreferences(context.packageName + "_secured_preferences")
clearSharedPreference(context)
if (check) {
Log.d("EncrytedSharedPref", "sharedPref deleted")
} else {
Log.d("EncrytedSharedPref", "sharedPref not exists")
}
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore.deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
} catch (e: Exception) {
Log.d("EncrytedSharedPref", "Error occured while deleting sharedPref")
}
}
private fun clearSharedPreference(context: Context) {
context.getSharedPreferences(
context.packageName + "_secured_preferences",
Context.MODE_PRIVATE
).edit().clear().apply()
}
초기화가 잘 안 되는 게 문제라면 직접 삭제를 하고 다시 초기화하는 방식으로 일부 디바이스에서 발생하는 오류를 잡아주었습니다.
이 방식으로 지금까지 같은 오류가 발생하는 것을 본 적이 없습니다.
[마무리]
Encrypted SharedPreference을 적용하고 발생할 수 있는 문제를 잡아보았습니다.
전체 코드는 여기서 확인이 가능합니다.
https://github.com/flash159483/multi-module-navigation
'android' 카테고리의 다른 글
(android/kotlin) 멀티모듈에서 proguard 설정하는 방법 (0) | 2023.10.26 |
---|---|
(android/kotlin) RecyclerView + Admob 적용하기 (0) | 2023.10.13 |
(android/kotlin) 멀티 모듈에서 Google OAuth만으로 로그인 구현해보기 (0) | 2023.09.16 |