diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt index f3de4527..ff78aa50 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt @@ -6,6 +6,7 @@ import io.rebble.cobble.MainActivity import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.datastore.SecureStorage import io.rebble.cobble.shared.domain.state.CurrentToken import io.rebble.cobble.shared.ui.nav.Routes import kotlinx.coroutines.flow.MutableStateFlow @@ -14,6 +15,7 @@ import javax.inject.Inject class KMPApiBridge @Inject constructor( private val tokenState: MutableStateFlow, + private val secureStorage: SecureStorage, bridgeLifecycleController: BridgeLifecycleController, private val activity: FlutterMainActivity? = null ): FlutterBridge, Pigeons.KMPApi { @@ -24,6 +26,7 @@ class KMPApiBridge @Inject constructor( override fun updateToken(token: Pigeons.StringWrapper) { tokenState.value = token.value?.let { CurrentToken.LoggedIn(it) } ?: CurrentToken.LoggedOut + secureStorage.token = token.value } override fun openLockerView() { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt index 9d624827..6f6448b1 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt @@ -13,6 +13,7 @@ import io.rebble.cobble.shared.database.dao.NotificationChannelDao import io.rebble.cobble.shared.database.dao.PersistedNotificationDao import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.shared.datastore.KMPPrefs +import io.rebble.cobble.shared.datastore.SecureStorage import io.rebble.cobble.shared.domain.calendar.CalendarSync import io.rebble.cobble.shared.domain.state.CurrentToken import io.rebble.cobble.shared.errors.GlobalExceptionHandler @@ -61,6 +62,10 @@ abstract class AppModule { return KoinPlatformTools.defaultContext().get().get(named("currentToken")) } @Provides + fun provideSecureStorage(): SecureStorage { + return KoinPlatformTools.defaultContext().get().get() + } + @Provides fun providePersistedNotificationDao(context: Context): PersistedNotificationDao { return AppDatabase.instance().persistedNotificationDao() } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index d02efd12..45d9ac10 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -16,6 +16,7 @@ errorproneVersion = "2.26.1" rruleVersion = "1.0.3" spotbugsVersion = "4.8.6" atomicfu = "0.25.0" +securityCrypto = "1.1.0-alpha06" protoliteWellKnownTypes = "18.0.0" room = "2.7.0-alpha11" @@ -57,6 +58,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTest" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTest" } androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidxTest" } +androidx-security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCrypto" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workManagerVersion" } dagger = { module = "com.google.dagger:dagger", version.ref = "daggerVersion" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "daggerVersion" } diff --git a/android/shared/build.gradle.kts b/android/shared/build.gradle.kts index f0455b83..9bd868e8 100644 --- a/android/shared/build.gradle.kts +++ b/android/shared/build.gradle.kts @@ -79,6 +79,7 @@ kotlin { implementation(libs.androidx.core.ktx) implementation(libs.timber) implementation(libs.rrule) + implementation(libs.androidx.security.crypto.ktx) implementation(project(":pebblekit_android")) implementation(project(":speex_codec")) } @@ -106,6 +107,7 @@ android { } dependencies { implementation(libs.protolite.wellknowntypes) + implementation(libs.androidx.security.crypto.ktx) add("kspCommonMainMetadata", libs.androidx.room.compiler) add("kspAndroid", libs.androidx.room.compiler) } diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/AndroidSecureStorage.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/AndroidSecureStorage.kt new file mode 100644 index 00000000..d7e9017e --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/AndroidSecureStorage.kt @@ -0,0 +1,28 @@ +package io.rebble.cobble.shared.datastore + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey + + +class AndroidSecureStorage(context: Context): SecureStorage() { + private val masterKey: MasterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + private val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create( + context, + "secret_shared_prefs", + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + override fun putString(key: String, value: String?) { + sharedPreferences.edit().putString(key, value).apply() + } + + override fun getString(key: String): String? { + return sharedPreferences.getString(key, null) + } +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt index 53632b2f..4a199df6 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt @@ -6,7 +6,9 @@ import android.service.notification.StatusBarNotification import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.AndroidPlatformContext import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.datastore.AndroidSecureStorage import io.rebble.cobble.shared.datastore.FlutterPreferences +import io.rebble.cobble.shared.datastore.SecureStorage import io.rebble.cobble.shared.datastore.createDataStore import io.rebble.cobble.shared.domain.calendar.AndroidCalendarActionExecutor import io.rebble.cobble.shared.domain.calendar.PlatformCalendarActionExecutor @@ -80,4 +82,5 @@ val androidModule = module { } else { factoryOf(::NullDictationService) bind DictationService::class } + singleOf(::AndroidSecureStorage) bind SecureStorage::class } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/datastore/SecureStorage.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/datastore/SecureStorage.kt new file mode 100644 index 00000000..d7ee52d4 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/datastore/SecureStorage.kt @@ -0,0 +1,16 @@ +package io.rebble.cobble.shared.datastore + +abstract class SecureStorage { + protected abstract fun putString(key: String, value: String?) + protected abstract fun getString(key: String): String? + + var token: String? + get() = getString("token") + set(value) { + if (value == null) { + putString("token", null) + } else { + putString("token", value) + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt index 923475e9..24890803 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt @@ -1,7 +1,9 @@ package io.rebble.cobble.shared.di +import io.rebble.cobble.shared.datastore.SecureStorage import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.CurrentToken +import io.rebble.cobble.shared.domain.state.CurrentToken.LoggedOut.tokenOrNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* @@ -20,6 +22,7 @@ val stateModule = module { } single(named("currentToken")) { - MutableStateFlow(CurrentToken.LoggedOut) + val token = get().token + MutableStateFlow(token?.let { CurrentToken.LoggedIn(token) } ?: CurrentToken.LoggedOut) } bind StateFlow::class } \ No newline at end of file