chore(preferences): migration to multiplatform-settings and encryption support

This commit is contained in:
Diego Beraldin 2023-07-28 12:47:34 +02:00
parent ae99a7c2c2
commit 49706c603d
8 changed files with 78 additions and 130 deletions

View File

@ -28,12 +28,18 @@ kotlin {
baseName = "core-preferences"
}
}
sourceSets {
val androidMain by getting {
dependencies {
implementation(libs.multiplatform.settings)
implementation(libs.androidx.security.crypto)
}
}
val commonMain by getting {
dependencies {
implementation(libs.koin.core)
implementation(libs.androidx.datastore)
implementation(libs.multiplatform.settings)
}
}
val commonTest by getting {

View File

@ -1,9 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core_preferences
import android.content.Context
internal class AndroidKeyStoreFilePathProvider(
private val context: Context,
) {
fun get(): String = context.filesDir.resolve(DefaultTemporaryKeyStore.FILE_NAME).absolutePath
}

View File

@ -1,12 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core_preferences
import org.koin.java.KoinJavaComponent.inject
actual fun getKeyStoreFilePath(): String {
val provider: AndroidKeyStoreFilePathProvider by inject(
AndroidKeyStoreFilePathProvider::class.java
)
return provider.get()
}

View File

@ -1,18 +1,51 @@
package com.github.diegoberaldin.raccoonforlemmy.core_preferences.di
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.AndroidKeyStoreFilePathProvider
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.DefaultTemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import org.koin.java.KoinJavaComponent.inject
actual val corePreferencesModule = module {
singleOf(::AndroidKeyStoreFilePathProvider)
singleOf<TemporaryKeyStore>(::DefaultTemporaryKeyStore)
singleOf(::SharedPreferencesProvider)
single<Settings> {
val sharedPreferencesProvider: SharedPreferencesProvider by inject()
SharedPreferencesSettings(
sharedPreferencesProvider.sharedPreferences,
false,
)
}
single<TemporaryKeyStore> {
DefaultTemporaryKeyStore(settings = get())
}
}
actual fun getTemporaryKeyStore(): TemporaryKeyStore {
val res: TemporaryKeyStore by inject(TemporaryKeyStore::class.java)
val res by inject<TemporaryKeyStore>(TemporaryKeyStore::class.java)
return res
}
private class SharedPreferencesProvider(
context: Context,
) {
companion object {
const val PREFERENCES_NAME = "secret_shared_prefs"
}
private val masterKeyAlias: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
PREFERENCES_NAME,
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

View File

@ -1,103 +1,42 @@
package com.github.diegoberaldin.raccoonforlemmy.core_preferences
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.doublePreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withTimeoutOrNull
import okio.Path.Companion.toPath
import com.russhwolf.settings.Settings
import com.russhwolf.settings.get
import com.russhwolf.settings.set
internal expect fun getKeyStoreFilePath(): String
internal class DefaultTemporaryKeyStore(
private val settings: Settings,
) : TemporaryKeyStore {
internal class DefaultTemporaryKeyStore : TemporaryKeyStore {
companion object {
internal const val FILE_NAME = "main.preferences_pb"
}
private val scope = CoroutineScope(Dispatchers.Default)
private val dataStore: DataStore<Preferences> = createDataStore()
private fun createDataStore(): DataStore<Preferences> =
PreferenceDataStoreFactory.createWithPath(
corruptionHandler = null,
migrations = emptyList(),
scope = scope,
produceFile = { getKeyStoreFilePath().toPath() }
)
override suspend fun containsKey(key: String): Boolean = withTimeoutOrNull(500) {
dataStore.data.map {
it.asMap().keys.any { k -> k.name == key }
}.first()
} ?: false
override suspend fun containsKey(key: String): Boolean = settings.keys.contains(key)
override suspend fun save(key: String, value: Boolean) {
dataStore.edit {
it[booleanPreferencesKey(key)] = value
}
settings[key] = value
}
override suspend fun get(key: String, default: Boolean) = withTimeoutOrNull(500) {
dataStore.data.map {
it[booleanPreferencesKey(key)]
}.first()
} ?: default
override suspend fun get(key: String, default: Boolean): Boolean = settings[key, default]
override suspend fun save(key: String, value: String) {
dataStore.edit {
it[stringPreferencesKey(key)] = value
}
settings[key] = value
}
override suspend fun get(key: String, default: String): String = withTimeoutOrNull(500) {
dataStore.data.map {
it[stringPreferencesKey(key)]
}.first()
} ?: default
override suspend fun get(key: String, default: String): String = settings[key, default]
override suspend fun save(key: String, value: Int) {
dataStore.edit {
it[intPreferencesKey(key)] = value
}
settings[key] = value
}
override suspend fun get(key: String, default: Int): Int = withTimeoutOrNull(500) {
dataStore.data.map {
it[intPreferencesKey(key)]
}.first()
} ?: default
override suspend fun get(key: String, default: Int): Int = settings[key, default]
override suspend fun save(key: String, value: Float) {
dataStore.edit {
it[floatPreferencesKey(key)] = value
}
settings[key] = value
}
override suspend fun get(key: String, default: Float): Float = withTimeoutOrNull(500) {
dataStore.data.map {
it[floatPreferencesKey(key)]
}.first()
} ?: default
override suspend fun get(key: String, default: Float): Float = settings[key, default]
override suspend fun save(key: String, value: Double) {
dataStore.edit {
it[doublePreferencesKey(key)] = value
}
settings[key] = value
}
override suspend fun get(key: String, default: Double): Double = withTimeoutOrNull(500) {
dataStore.data.map {
it[doublePreferencesKey(key)]
}.first()
} ?: default
override suspend fun get(key: String, default: Double): Double = settings[key, default]
}

View File

@ -1,17 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core_preferences
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSURL
import platform.Foundation.NSUserDomainMask
actual fun getKeyStoreFilePath(): String {
val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
return requireNotNull(documentDirectory).path + "/${DefaultTemporaryKeyStore.FILE_NAME}"
}

View File

@ -1,20 +1,26 @@
package com.github.diegoberaldin.raccoonforlemmy.core_preferences.di
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.DefaultTemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import com.russhwolf.settings.KeychainSettings
import com.russhwolf.settings.Settings
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
actual val corePreferencesModule = module {
singleOf<TemporaryKeyStore>(::DefaultTemporaryKeyStore)
single<Settings> {
KeychainSettings(service = "secret_shared_prefs")
}
single<TemporaryKeyStore> {
DefaultTemporaryKeyStore(settings = get())
}
}
actual fun getTemporaryKeyStore(): TemporaryKeyStore {
return TemporaryKeyStoreHelper.keyStore
return TemporaryKeyStoreHelper.temporaryKeyStore
}
object TemporaryKeyStoreHelper : KoinComponent {
val keyStore: TemporaryKeyStore by inject()
val temporaryKeyStore: TemporaryKeyStore by inject()
}

View File

@ -1,6 +1,6 @@
[versions]
androidx_activity_compose = "1.7.2"
androidx_datastore = "1.1.0-dev01"
androidx_crypto = "1.0.0"
android_gradle = "7.4.2"
compose = "1.4.3"
compose_imageloader = "1.6.0"
@ -12,15 +12,17 @@ ktorfit_gradle = "1.0.0"
ktorfit_lib = "1.4.3"
markdown = "0.4.1"
moko_resources = "0.23.0"
multiplatform_settings = "1.0.0"
voyager = "1.0.0-rc05"
[libraries]
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "androidx.activity.compose" }
androidx_datastore = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx.datastore" }
androidx_security_crypto = { module = "androidx.security:security-crypto", version.ref = "androidx.crypto" }
compose_imageloader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "compose.imageloader" }
markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" }
multiplatform_settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform.settings" }
koin_core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin_test = { module = "io.insert-koin:koin-test", version.ref = "koin" }