From fc0d6b60570d44740b300d4926f2546f681e9122 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 23 Apr 2026 13:50:51 +0700 Subject: [PATCH] android: add secret storage --- composeApp/build.gradle.kts | 5 +- .../su/reya/coop/storage/SecretCrypto.kt | 78 +++++++++++++++++++ .../su/reya/coop/storage/SecretStore.kt | 42 ++++++++++ shared/build.gradle.kts | 4 +- 4 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretCrypto.kt create mode 100644 composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretStore.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index b1ac2b7..a6af89a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -14,11 +13,13 @@ kotlin { jvmTarget.set(JvmTarget.JVM_11) } } - + sourceSets { androidMain.dependencies { implementation(libs.compose.uiToolingPreview) implementation(libs.androidx.activity.compose) + implementation("androidx.datastore:datastore-preferences:1.2.1") + implementation("androidx.datastore:datastore-preferences-core:1.2.1") } commonMain.dependencies { implementation(libs.compose.runtime) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretCrypto.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretCrypto.kt new file mode 100644 index 0000000..dac0de6 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretCrypto.kt @@ -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() + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretStore.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretStore.kt new file mode 100644 index 0000000..e376167 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/storage/SecretStore.kt @@ -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 + } +} \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index eb41203..84dfc7b 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -11,7 +11,7 @@ kotlin { jvmTarget.set(JvmTarget.JVM_11) } } - + listOf( iosArm64(), iosSimulatorArm64() @@ -21,7 +21,7 @@ kotlin { isStatic = true } } - + sourceSets { commonMain.dependencies { implementation("org.rust-nostr:nostr-sdk-kmp:0.44.3")