diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 36faa3a..c0c9b8d 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 { @@ -19,6 +18,8 @@ kotlin { 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/coop/MainActivity.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/coop/MainActivity.kt index b11c533..dab52ca 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/coop/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/coop/MainActivity.kt @@ -17,6 +17,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) + // Get database directory val dbDir = File(filesDir, "nostr") dbDir.mkdirs() diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/coop/Platform.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/coop/Platform.kt deleted file mode 100644 index d8ac07b..0000000 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/coop/Platform.kt +++ /dev/null @@ -1,9 +0,0 @@ -package su.reya.coop.coop - -import android.os.Build - -class AndroidPlatform { - val name: String = "Android ${Build.VERSION.SDK_INT}" -} - -fun getPlatform() = AndroidPlatform() \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/coop/storage/SecretCrypto.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/coop/storage/SecretCrypto.kt new file mode 100644 index 0000000..817e7c4 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/coop/storage/SecretCrypto.kt @@ -0,0 +1,75 @@ +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/coop/storage/SecretStore.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/coop/storage/SecretStore.kt new file mode 100644 index 0000000..e376167 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/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