android: add secret storage

This commit is contained in:
2026-04-23 13:50:51 +07:00
parent 6295378b78
commit fc0d6b6057
4 changed files with 125 additions and 4 deletions

View File

@@ -1,4 +1,3 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
@@ -19,6 +18,8 @@ kotlin {
androidMain.dependencies { androidMain.dependencies {
implementation(libs.compose.uiToolingPreview) implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation("androidx.datastore:datastore-preferences:1.2.1")
implementation("androidx.datastore:datastore-preferences-core:1.2.1")
} }
commonMain.dependencies { commonMain.dependencies {
implementation(libs.compose.runtime) implementation(libs.compose.runtime)

View File

@@ -0,0 +1,78 @@
package su.reya.coop.coop.storage
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
data class SecretEntry(
val encrypted: String,
val iv: String
)
class SecretCrypto {
private val keyAlias = "coop"
private val keyStoreType = "AndroidKeyStore"
private val transformation = "AES/GCM/NoPadding"
fun encrypt(content: String): SecretEntry {
// Initialize cipher
val cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
// Encrypt content
val encrypted = cipher.doFinal(content.toByteArray())
val iv = cipher.iv
return SecretEntry(
encrypted = Base64.encodeToString(encrypted, Base64.NO_WRAP),
iv = Base64.encodeToString(iv, Base64.NO_WRAP)
)
}
fun decrypt(entry: SecretEntry): String {
val encrypted = Base64.decode(entry.encrypted, Base64.NO_WRAP)
val iv = Base64.decode(entry.iv, Base64.NO_WRAP)
// Initialize cipher
val cipher = Cipher.getInstance(transformation)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
// Decrypt content
val plaintext = cipher.doFinal(encrypted)
return String(plaintext, StandardCharsets.UTF_8)
}
private fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance(keyStoreType).apply { load(null) }
val existingKey = keyStore.getKey(keyAlias, null)
// Return existing key if available
if (existingKey is SecretKey) return existingKey
// Construct a new key generator
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStoreType)
// Initialize key generation parameters
val spec = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
// Generate a new key
keyGenerator.init(spec)
return keyGenerator.generateKey()
}
}

View File

@@ -0,0 +1,42 @@
package su.reya.coop.coop.storage
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
private val Context.dataStore by preferencesDataStore("secret_store")
class SecretStore(private val context: Context) {
private val crypto = SecretCrypto()
suspend fun set(key: String, value: String) {
val entry = crypto.encrypt(value)
context.dataStore.edit { prefs ->
prefs[stringPreferencesKey("${key}_encrypted")] = entry.encrypted
prefs[stringPreferencesKey("${key}_iv")] = entry.iv
}
}
suspend fun get(key: String): String? {
val prefs = context.dataStore.data.first()
val encrypted = prefs[stringPreferencesKey("${key}_encrypted")] ?: return null
val iv = prefs[stringPreferencesKey("${key}_iv")] ?: return null
return crypto.decrypt(SecretEntry(encrypted, iv))
}
suspend fun clear(name: String) {
context.dataStore.edit { prefs ->
prefs.remove(stringPreferencesKey("${name}_encrypted"))
prefs.remove(stringPreferencesKey("${name}_iv"))
}
}
suspend fun has(name: String): Boolean {
val prefs = context.dataStore.data.first()
return prefs[stringPreferencesKey("${name}_encrypted")] != null
}
}