android: secure token storage with keystore-backed encrypted prefs
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
@@ -318,3 +318,8 @@
|
||||
- Added dedicated `Settings` and `Profile` routes/screens with mobile-safe insets and placeholder content.
|
||||
- Removed direct logout action from chat list and moved logout action to `Settings`.
|
||||
- Wired bottom navigation pills in chats to open `Settings` and `Profile`.
|
||||
|
||||
### Step 53 - Secure token storage (Keystore-backed)
|
||||
- Added `EncryptedPrefsTokenRepository` backed by `EncryptedSharedPreferences` and Android `MasterKey` (Keystore).
|
||||
- Switched DI token binding from DataStore token repository to encrypted shared preferences repository.
|
||||
- Kept DataStore for non-token app settings and renamed preferences file to `messenger_preferences.preferences_pb`.
|
||||
|
||||
@@ -85,6 +85,7 @@ dependencies {
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
|
||||
implementation("androidx.datastore:datastore-preferences:1.1.1")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package ru.daemonlord.messenger.core.token
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import ru.daemonlord.messenger.di.TokenPrefs
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class EncryptedPrefsTokenRepository @Inject constructor(
|
||||
@TokenPrefs private val sharedPreferences: SharedPreferences,
|
||||
) : TokenRepository {
|
||||
|
||||
private val tokensFlow = MutableStateFlow(readTokens())
|
||||
|
||||
override fun observeTokens(): Flow<TokenBundle?> = tokensFlow.asStateFlow()
|
||||
|
||||
override suspend fun getTokens(): TokenBundle? = tokensFlow.value
|
||||
|
||||
override suspend fun saveTokens(tokens: TokenBundle) {
|
||||
sharedPreferences.edit()
|
||||
.putString(ACCESS_TOKEN_KEY, tokens.accessToken)
|
||||
.putString(REFRESH_TOKEN_KEY, tokens.refreshToken)
|
||||
.putLong(SAVED_AT_KEY, tokens.savedAtMillis)
|
||||
.apply()
|
||||
tokensFlow.value = tokens
|
||||
}
|
||||
|
||||
override suspend fun clearTokens() {
|
||||
sharedPreferences.edit()
|
||||
.remove(ACCESS_TOKEN_KEY)
|
||||
.remove(REFRESH_TOKEN_KEY)
|
||||
.remove(SAVED_AT_KEY)
|
||||
.apply()
|
||||
tokensFlow.value = null
|
||||
}
|
||||
|
||||
private fun readTokens(): TokenBundle? {
|
||||
val access = sharedPreferences.getString(ACCESS_TOKEN_KEY, null)
|
||||
val refresh = sharedPreferences.getString(REFRESH_TOKEN_KEY, null)
|
||||
val savedAt = sharedPreferences.getLong(SAVED_AT_KEY, -1L)
|
||||
if (access.isNullOrBlank() || refresh.isNullOrBlank() || savedAt <= 0L) {
|
||||
return null
|
||||
}
|
||||
return TokenBundle(
|
||||
accessToken = access,
|
||||
refreshToken = refresh,
|
||||
savedAtMillis = savedAt,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCESS_TOKEN_KEY = "access_token"
|
||||
const val REFRESH_TOKEN_KEY = "refresh_token"
|
||||
const val SAVED_AT_KEY = "tokens_saved_at"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +13,7 @@ annotation class RefreshAuthApi
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class IoDispatcher
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class TokenPrefs
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package ru.daemonlord.messenger.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import ru.daemonlord.messenger.core.token.DataStoreTokenRepository
|
||||
import ru.daemonlord.messenger.core.token.EncryptedPrefsTokenRepository
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -24,13 +27,31 @@ object StorageModule {
|
||||
@ApplicationContext context: Context,
|
||||
): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
produceFile = { context.preferencesDataStoreFile("messenger_tokens.preferences_pb") }
|
||||
produceFile = { context.preferencesDataStoreFile("messenger_preferences.preferences_pb") }
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@TokenPrefs
|
||||
fun provideTokenSharedPreferences(
|
||||
@ApplicationContext context: Context,
|
||||
): SharedPreferences {
|
||||
val masterKey = MasterKey.Builder(context)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
return EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"messenger_secure_tokens",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenRepository(
|
||||
repository: DataStoreTokenRepository,
|
||||
repository: EncryptedPrefsTokenRepository,
|
||||
): TokenRepository = repository
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
- [ ] Accessibility (TalkBack, dynamic type)
|
||||
|
||||
## 14. Безопасность
|
||||
- [ ] Secure token storage (EncryptedSharedPrefs/Keystore)
|
||||
- [x] Secure token storage (EncryptedSharedPrefs/Keystore)
|
||||
- [ ] Certificate pinning (опционально)
|
||||
- [ ] Root/emulator policy (опционально)
|
||||
- [ ] Privacy-safe logging (без токенов/PII)
|
||||
|
||||
Reference in New Issue
Block a user