Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bdcd0256c | |||
| f7d4dbcdb1 |
@@ -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)
|
||||||
@@ -29,6 +30,7 @@ kotlin {
|
|||||||
implementation(libs.compose.uiToolingPreview)
|
implementation(libs.compose.uiToolingPreview)
|
||||||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||||
|
implementation("org.rust-nostr:nostr-sdk-kmp:0.44.3")
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package su.reya.coop.coop
|
package su.reya.coop.coop
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.safeContentPadding
|
import androidx.compose.foundation.layout.safeContentPadding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -14,10 +11,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import org.jetbrains.compose.resources.painterResource
|
|
||||||
|
|
||||||
import coop.composeapp.generated.resources.Res
|
|
||||||
import coop.composeapp.generated.resources.compose_multiplatform
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
@@ -34,16 +27,6 @@ fun App() {
|
|||||||
Button(onClick = { showContent = !showContent }) {
|
Button(onClick = { showContent = !showContent }) {
|
||||||
Text("Click me!")
|
Text("Click me!")
|
||||||
}
|
}
|
||||||
AnimatedVisibility(showContent) {
|
|
||||||
val greeting = remember { Greeting().greet() }
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Image(painterResource(Res.drawable.compose_multiplatform), null)
|
|
||||||
Text("Compose: $greeting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package su.reya.coop.coop
|
|
||||||
|
|
||||||
class Greeting {
|
|
||||||
private val platform = getPlatform()
|
|
||||||
|
|
||||||
fun greet(): String {
|
|
||||||
return "Hello, ${platform.name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,29 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private val nostr = Nostr()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Get database directory
|
||||||
|
val dbDir = File(filesDir, "nostr")
|
||||||
|
dbDir.mkdirs()
|
||||||
|
|
||||||
|
// Initialize nostr client
|
||||||
|
nostr.init(dbDir.absolutePath)
|
||||||
|
|
||||||
|
// Connect to bootstrap relays
|
||||||
|
lifecycleScope.launch {
|
||||||
|
nostr.connect()
|
||||||
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
|
|||||||
23
composeApp/src/androidMain/kotlin/su/reya/coop/coop/Nostr.kt
Normal file
23
composeApp/src/androidMain/kotlin/su/reya/coop/coop/Nostr.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package su.reya.coop.coop
|
||||||
|
|
||||||
|
import rust.nostr.sdk.*
|
||||||
|
|
||||||
|
class Nostr {
|
||||||
|
var client: Client? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun init(dbPath: String) {
|
||||||
|
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||||
|
val gossip = NostrGossip.inMemory()
|
||||||
|
val opts = ClientOptions().automaticAuthentication(false)
|
||||||
|
|
||||||
|
client = ClientBuilder().database(lmdb).gossip(gossip).opts(opts).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun connect() {
|
||||||
|
this.client?.addRelay(RelayUrl.parse("wss://relay.damus.io"))
|
||||||
|
this.client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
||||||
|
this.client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||||
|
this.client?.connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user