diff --git a/app/shared/build.gradle.kts b/app/shared/build.gradle.kts index 7b06cf1..0efe010 100644 --- a/app/shared/build.gradle.kts +++ b/app/shared/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { iosTarget.binaries.framework { baseName = "ComposeApp" isStatic = true + linkerOpts("-framework", "UIKit") } } @@ -77,12 +78,15 @@ kotlin { implementation(project(":utils:shared")) implementation(libs.krypto) + implementation(libs.cryptography.core) + implementation(libs.cryptography.provider.optimal) } iosMain.dependencies { implementation(libs.jetbrains.kotlinx.io.bytestring) implementation(libs.jetbrains.kotlinx.coroutines.core) implementation(libs.ktor.client.darwin) + implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.5") } } } diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/api/ApiClient.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/api/ApiClient.ios.kt index 6b49b61..9a0e25e 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/api/ApiClient.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/api/ApiClient.ios.kt @@ -3,34 +3,29 @@ package ru.fromchat.api import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.engine.darwin.Darwin -import io.ktor.client.engine.darwin.DarwinClientEngineConfig import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.defaultRequest import io.ktor.http.ContentType import io.ktor.http.contentType -import io.ktor.http.headers actual fun createPlatformHttpClient( block: HttpClientConfig<*>.() -> Unit ): HttpClient { - @Suppress("UNCHECKED_CAST") return HttpClient(Darwin) { - // Configure default request headers to ensure UTF-8 encoding defaultRequest { contentType(ContentType.Application.Json) + headers { append("Accept-Charset", "utf-8") append("Content-Type", "application/json; charset=utf-8") } } - // Add timeout configuration install(HttpTimeout) { requestTimeoutMillis = 30000 connectTimeoutMillis = 30000 } - // Apply the passed configuration block block(this) } } diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/backup/BackupCrypto.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/backup/BackupCrypto.ios.kt index 128d72f..7b0c6bb 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/backup/BackupCrypto.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/backup/BackupCrypto.ios.kt @@ -1,29 +1,81 @@ package ru.fromchat.crypto.backup +import dev.whyoleg.cryptography.BinarySize.Companion.bits +import dev.whyoleg.cryptography.BinarySize.Companion.bytes +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.algorithms.AES +import dev.whyoleg.cryptography.algorithms.PBKDF2 +import dev.whyoleg.cryptography.algorithms.SHA256 +import dev.whyoleg.cryptography.random.CryptographyRandom import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlin.random.Random +private const val PBKDF2_ITERATIONS = 210_000 +private const val IV_SIZE = 12 +private const val KEY_SIZE_BYTES = 32 + +@OptIn(dev.whyoleg.cryptography.DelicateCryptographyApi::class) actual object BackupCrypto { + private val provider get() = CryptographyProvider.Default + private val aesGcm get() = provider.get(AES.GCM) + private val pbkdf2 get() = provider.get(PBKDF2) + actual suspend fun encryptBackupWithPassword( password: String, bundle: PrivateKeyBundle ): EncryptedBackupBlob = withContext(Dispatchers.Default) { - // iOS backup encryption is not implemented yet; fail fast if called. - error("Backup encryption is not yet implemented on iOS.") + val salt = CryptographyRandom.nextBytes(16) + val nonce = CryptographyRandom.nextBytes(IV_SIZE) + + EncryptedBackupBlob( + salt, + nonce, + aesGcm + .keyDecoder() + .decodeFromByteArray( + AES.Key.Format.RAW, + pbkdf2 + .secretDerivation( + digest = SHA256, + iterations = PBKDF2_ITERATIONS, + outputSize = KEY_SIZE_BYTES.bytes, + salt = salt + ) + .deriveSecretBlocking( + password.encodeToByteArray() + ) + .toByteArray() + ) + .cipher(tagSize = 128.bits) + .encryptWithIv(nonce, serializeBundle(bundle)) + ) } actual suspend fun decryptBackupWithPassword( password: String, blob: EncryptedBackupBlob ): PrivateKeyBundle = withContext(Dispatchers.Default) { - // iOS backup decryption is not implemented yet; fail fast if called. - error("Backup decryption is not yet implemented on iOS.") + deserializeBundle( + aesGcm + .keyDecoder() + .decodeFromByteArray( + AES.Key.Format.RAW, + pbkdf2 + .secretDerivation( + digest = SHA256, + iterations = PBKDF2_ITERATIONS, + outputSize = KEY_SIZE_BYTES.bytes, + salt = blob.salt + ) + .deriveSecretBlocking( + password.encodeToByteArray() + ) + .toByteArray() + ) + .cipher(tagSize = 128.bits) + .decryptWithIv(blob.iv, blob.ciphertext) + ) } - actual fun randomBytes(length: Int): ByteArray { - val bytes = ByteArray(length) - Random.Default.nextBytes(bytes) - return bytes - } + actual fun randomBytes(length: Int): ByteArray = CryptographyRandom.nextBytes(length) } diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/dm/DmCrypto.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/dm/DmCrypto.ios.kt index a8193b9..f0aa704 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/dm/DmCrypto.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/dm/DmCrypto.ios.kt @@ -1,79 +1,71 @@ package ru.fromchat.crypto.dm import com.pr0gramm3r101.utils.crypto.Base64 -import com.pr0gramm3r101.utils.crypto.Hmac -import com.pr0gramm3r101.utils.crypto.PasswordHash -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.allocArray -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value +import com.pr0gramm3r101.utils.require +import dev.whyoleg.cryptography.BinarySize.Companion.bits +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.algorithms.AES import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import platform.CoreCrypto.CCCryptorCreateWithMode -import platform.CoreCrypto.CCCryptorRelease -import platform.CoreCrypto.CCCryptorUpdate -import platform.CoreCrypto.CCCryptorFinal -import platform.CoreCrypto.kCCAlgorithmAES -import platform.CoreCrypto.kCCKeySizeAES256 -import platform.CoreCrypto.kCCDecrypt -import platform.CoreCrypto.kCCSuccess -// GCM mode constant (value 11) - CommonCrypto GCM support via cinterop is limited -private const val kCCModeGCM = 11u +private const val AES_KEY_SIZE = 32 +private const val GCM_IV_SIZE = 12 +private const val GCM_TAG_SIZE = 16 +@OptIn(dev.whyoleg.cryptography.DelicateCryptographyApi::class) actual object DmCrypto { - private const val AES_KEY_SIZE = 32 - private const val GCM_IV_SIZE = 12 - private const val GCM_TAG_SIZE = 16 + private val provider get() = CryptographyProvider.Default + private val aesGcm get() = provider.get(AES.GCM) - @OptIn(ExperimentalForeignApi::class) - actual suspend fun unwrapMek(wrappedMekB64: String, wrappingKey: ByteArray): ByteArray = - withContext(Dispatchers.Default) { - require(wrappingKey.size == AES_KEY_SIZE) { "Wrapping key must be 32 bytes" } - - val wrapped = Base64.decode(wrappedMekB64) - require(wrapped.size >= GCM_IV_SIZE + GCM_TAG_SIZE) { "Wrapped MEK too short" } - - // Extract IV and ciphertext+tag - val iv = wrapped.sliceArray(0 until GCM_IV_SIZE) - val ciphertext = wrapped.sliceArray(GCM_IV_SIZE until wrapped.size) - - // Decrypt using AES-GCM - aesGcmDecrypt(wrappingKey, iv, ciphertext) - } + actual suspend fun unwrapMek( + wrappedMekB64: String, + wrappingKey: ByteArray + ) = withContext(Dispatchers.Default) { + require(wrappingKey.size == AES_KEY_SIZE) { "Wrapping key must be 32 bytes" } + val wrapped = Base64.decode(wrappedMekB64) + require(wrapped.size >= GCM_IV_SIZE + GCM_TAG_SIZE) { "Wrapped MEK too short" } + + aesGcmDecrypt( + wrappingKey, + wrapped.sliceArray(0 until GCM_IV_SIZE), + wrapped.sliceArray(GCM_IV_SIZE until wrapped.size) + ) + } - @OptIn(ExperimentalForeignApi::class) actual suspend fun decryptEnvelope( ivB64: String, ciphertextB64: String, mek: ByteArray - ): ByteArray = withContext(Dispatchers.Default) { - require(mek.size == AES_KEY_SIZE) { "MEK must be 32 bytes" } - - val iv = Base64.decode(ivB64) - val ciphertext = Base64.decode(ciphertextB64) - - require(iv.size == GCM_IV_SIZE) { "IV must be 12 bytes" } - require(ciphertext.size >= GCM_TAG_SIZE) { "Ciphertext too short" } - - // Decrypt using AES-GCM - aesGcmDecrypt(mek, iv, ciphertext) + ) = withContext(Dispatchers.Default) { + aesGcmDecrypt( + mek.require("MEK must be 32 bytes") { + it.size == AES_KEY_SIZE + }, + Base64 + .decode(ivB64) + .require("IV must be 12 bytes") { + it.size == GCM_IV_SIZE + }, + Base64 + .decode(ciphertextB64) + .require("Ciphertext too short") { + it.size >= GCM_TAG_SIZE + } + ) } - @OptIn(ExperimentalForeignApi::class) - private fun aesGcmDecrypt(key: ByteArray, iv: ByteArray, ciphertext: ByteArray): ByteArray { + private suspend fun aesGcmDecrypt(key: ByteArray, iv: ByteArray, ciphertext: ByteArray): ByteArray { require(iv.size == GCM_IV_SIZE) { "IV must be 12 bytes for GCM" } - require(key.size == AES_KEY_SIZE) { "Key must be 32 bytes (256 bits)" } + require(key.size == AES_KEY_SIZE) { "Key must be 32 bytes" } require(ciphertext.size >= GCM_TAG_SIZE) { "Ciphertext too short" } - - // TODO: Implement proper AES-GCM using CommonCrypto or CryptoKit - // See BackupCrypto.ios.kt for implementation notes. - // For now, delegate to BackupCrypto's implementation when available. - - error("AES-GCM decryption not yet implemented for iOS. Use CryptoKit via Swift interop or krypto library.") + + return aesGcm + .keyDecoder() + .decodeFromByteArray( + AES.Key.Format.RAW, + key + ) + .cipher(tagSize = 128.bits) + .decryptWithIv(iv, ciphertext) } } diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/transport/TransportCrypto.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/transport/TransportCrypto.ios.kt index 4974747..a327346 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/transport/TransportCrypto.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/crypto/transport/TransportCrypto.ios.kt @@ -1,5 +1,10 @@ package ru.fromchat.crypto.transport +import com.ionspin.kotlin.crypto.LibsodiumInitializer +import com.ionspin.kotlin.crypto.box.Box +import com.ionspin.kotlin.crypto.box.crypto_box_NONCEBYTES +import com.ionspin.kotlin.crypto.util.LibsodiumRandom +import com.pr0gramm3r101.utils.crypto.Base64 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -7,8 +12,24 @@ actual object TransportCrypto { actual suspend fun encryptWithTransportKey( plaintext: String, transportPublicKeyB64: String - ): TransportCiphertext = withContext(Dispatchers.Default) { - error("TransportCrypto.encryptWithTransportKey is not yet implemented on iOS.") + ) = withContext(Dispatchers.Default) { + if (!LibsodiumInitializer.isInitialized()) { + LibsodiumInitializer.initialize() + } + + val keyPair = Box.keypair() + val nonce = LibsodiumRandom.buf(crypto_box_NONCEBYTES) + val ciphertext = Box.easy( + plaintext.encodeToByteArray().toUByteArray(), + nonce, + Base64.decode(transportPublicKeyB64).toUByteArray(), + keyPair.secretKey + ) + + TransportCiphertext( + clientPublicKeyB64 = Base64.encode(keyPair.publicKey.toByteArray()), + nonceB64 = Base64.encode(nonce.toByteArray()), + ciphertextB64 = Base64.encode(ciphertext.toByteArray()) + ) } } - diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/fcm/FcmRegistration.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/fcm/FcmRegistration.ios.kt index 8d24871..f089cc2 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/fcm/FcmRegistration.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/fcm/FcmRegistration.ios.kt @@ -1,3 +1,5 @@ package ru.fromchat.fcm -actual suspend fun uploadPendingFcmTokenIfAvailable() {} \ No newline at end of file +actual suspend fun uploadPendingFcmTokenIfAvailable() { + // This should remain a placeholder because iOS is stupid +} diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt index 3a52341..ed36ad7 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/BackHandler.ios.kt @@ -1,11 +1,27 @@ package ru.fromchat.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import kotlinx.cinterop.ExperimentalForeignApi +import platform.UIKit.UIImpactFeedbackGenerator +import platform.UIKit.UIImpactFeedbackStyle @Composable actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { // iOS has no system back button; back is handled by navigation. } +@OptIn(ExperimentalForeignApi::class) @Composable -actual fun rememberHapticFeedbackInternal(): (Int) -> Unit = { } +actual fun rememberHapticFeedbackInternal(): (Int) -> Unit { + return remember { + { ordinal -> + val style: UIImpactFeedbackStyle = when (ordinal) { + HapticFeedbackEvent.MessageSent.ordinal -> UIImpactFeedbackStyle.UIImpactFeedbackStyleMedium + else -> UIImpactFeedbackStyle.UIImpactFeedbackStyleLight + } + val generator = UIImpactFeedbackGenerator(style) + generator.impactOccurred() + } + } +} diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/NotificationUtils.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/NotificationUtils.ios.kt deleted file mode 100644 index 8b13789..0000000 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/NotificationUtils.ios.kt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt index a9e0885..4a59b98 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/Theme.ios.kt @@ -6,5 +6,4 @@ import androidx.compose.runtime.Composable @Composable actual fun getColorScheme(darkTheme: Boolean, dynamicColor: Boolean) = - if (darkTheme) darkColorScheme() - else lightColorScheme() \ No newline at end of file + if (darkTheme) darkColorScheme() else lightColorScheme() \ No newline at end of file diff --git a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/debug/DebugApiScreen.ios.kt b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/debug/DebugApiScreen.ios.kt index 122fd2c..42330b6 100644 --- a/app/shared/src/iosMain/kotlin/ru/fromchat/ui/debug/DebugApiScreen.ios.kt +++ b/app/shared/src/iosMain/kotlin/ru/fromchat/ui/debug/DebugApiScreen.ios.kt @@ -1,9 +1,291 @@ package ru.fromchat.ui.debug +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material3.Button +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.pr0gramm3r101.utils.settings.settings +import kotlinx.coroutines.launch +import ru.fromchat.api.ApiClient +import ru.fromchat.crypto.IdentityKeyManager +import ru.fromchat.crypto.decryptEnvelope +import ru.fromchat.ui.LocalNavController +@OptIn(ExperimentalMaterial3Api::class) @Composable actual fun DebugApiScreen() { - // iOS debug UI not implemented; Android-only for now. -} + val navController = LocalNavController.current + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + var statusMessage by rememberSaveable { mutableStateOf("") } + var profileResult by rememberSaveable { mutableStateOf(null) } + var conversationsResult by rememberSaveable { mutableStateOf(null) } + var historyResult by rememberSaveable { mutableStateOf(null) } + var historyUserId by rememberSaveable { mutableStateOf("0") } + var decryptedMessage by rememberSaveable { mutableStateOf(null) } + var sendRecipientId by rememberSaveable { mutableStateOf("") } + var sendMessageText by rememberSaveable { mutableStateOf("") } + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { navController.navigateUp() }) { + Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + title = { + Text(text = "Debug API") + }, + actions = { + IconButton(onClick = {}) { + Icon(imageVector = Icons.Filled.BugReport, contentDescription = null) + } + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .verticalScroll(scrollState) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = statusMessage.takeIf { it.isNotBlank() } + ?: "Status will appear here", + style = MaterialTheme.typography.bodyMedium + ) + + Button( + onClick = { + scope.launch { + runCatching { ApiClient.getOwnProfile() } + .onSuccess { + profileResult = ApiClient.json.encodeToString(it) + statusMessage = "Profile loaded" + } + .onFailure { + statusMessage = it.message ?: "An unknown error occurred" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Load own profile") + } + + Text( + text = profileResult ?: "No data loaded yet", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { + scope.launch { + runCatching { ApiClient.getDmConversations() } + .onSuccess { + conversationsResult = ApiClient.json.encodeToString(it) + statusMessage = "Conversations loaded" + } + .onFailure { + statusMessage = it.message ?: "An unknown error occurred" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Load DM conversations") + } + + Text( + text = conversationsResult ?: "No data loaded yet", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = historyUserId, + onValueChange = { historyUserId = it }, + label = { Text(text = "History user ID") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { + val userId = historyUserId.toIntOrNull() + if (userId == null) { + statusMessage = "Enter a valid user ID" + return@Button + } + + scope.launch { + runCatching { ApiClient.getDmHistory(userId) } + .onSuccess { + historyResult = ApiClient.json.encodeToString(it) + statusMessage = "DM history loaded" + } + .onFailure { + statusMessage = it.message ?: "An unknown error occurred" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Load DM history") + } + + Text( + text = historyResult ?: "No data loaded yet", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + thickness = DividerDefaults.Thickness, + color = DividerDefaults.color + ) + + Text( + text = "DM Send Test", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = sendRecipientId, + onValueChange = { sendRecipientId = it }, + label = { Text(text = "Recipient user ID") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = sendMessageText, + onValueChange = { sendMessageText = it }, + label = { Text(text = "Message text") }, + singleLine = false, + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { + val userId = sendRecipientId.toIntOrNull() + if (userId == null) { + statusMessage = "Enter a valid recipient ID" + return@Button + } + if (sendMessageText.isBlank()) { + statusMessage = "Enter a message to send" + return@Button + } + + scope.launch { + runCatching { + ApiClient.sendDm( + recipientId = userId, + plaintext = sendMessageText.trim(), + replyToId = null + ) + }.onSuccess { + statusMessage = "DM sent successfully" + }.onFailure { + statusMessage = it.message ?: "Failed to send DM" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Send DM") + } + + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + thickness = DividerDefaults.Thickness, + color = DividerDefaults.color + ) + + Text( + text = "DM Decryption Test", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.fillMaxWidth() + ) + + Button( + onClick = { + val userId = historyUserId.toIntOrNull() + if (userId == null) { + statusMessage = "Enter a valid user ID" + return@Button + } + + scope.launch { + try { + IdentityKeyManager.restoreFromLocal() + + val history = ApiClient.getDmHistory(userId) + if (history.messages.isEmpty()) { + statusMessage = "No messages found" + decryptedMessage = null + return@launch + } + + val currentUserId = settings.getInt("current_user_id", 0) + if (currentUserId == 0) { + statusMessage = "Current user ID not found" + decryptedMessage = null + return@launch + } + + val firstEnvelope = history.messages.first() + val plaintext = decryptEnvelope(firstEnvelope, currentUserId) + decryptedMessage = "Decrypted: $plaintext" + statusMessage = "Decryption successful" + } catch (e: Exception) { + statusMessage = "Decryption failed: ${e.message}" + decryptedMessage = "Error: ${e.message}\n${e.stackTraceToString()}" + } + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Decrypt First DM") + } + + Text( + text = decryptedMessage ?: "No decrypted message yet", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d51ee92..13b6a49 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "9.0.0" -androidx-activityCompose = "1.12.2" +androidx-activityCompose = "1.12.3" androidx-appcompat = "1.7.1" androidx-core-ktx = "1.17.0" coilCompose = "3.3.0" @@ -11,7 +11,7 @@ coreSplashscreen = "1.2.0" firebaseMessaging = "25.0.1" googleServices = "4.4.4" haze = "1.7.1" -kotlin = "2.3.0" +kotlin = "2.3.10" adaptiveAndroid = "1.2.0" biometric = "1.4.0-alpha05" gson = "2.13.2" @@ -19,19 +19,19 @@ kotlinxIoBytestring = "0.8.2" kotlinxCoroutinesCore = "1.10.2" kotlinxIoCore = "0.8.2" multiplatformSettings = "1.3.0" -serialization = "2.3.0" -serialization-json = "1.9.0" +serialization-json = "1.10.0" material = "1.13.0" -activityKtx = "1.12.2" +activityKtx = "1.12.3" navigationCompose = "2.9.1" datastore = "1.2.0" security-crypto = "1.1.0" -ktor = "3.3.3" +ktor = "3.4.0" slf4j = "1.7.36" kotlinxDatetime = "0.7.1" +cryptography-kotlin = "0.5.0" krypto = "4.0.10" lifecycleRuntimeKtx = "2.10.0" -composeBom = "2026.01.00" +composeBom = "2026.01.01" composeMaterialIconsExtended = "1.7.3" composeMaterial3 = "1.10.0-alpha05" composeComponents = "1.10.0" @@ -91,6 +91,8 @@ compose-components-resources = { module = "org.jetbrains.compose.components:comp compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" } compose-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMaterialIconsExtended" } krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "krypto" } +cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography-kotlin" } +cryptography-provider-optimal = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography-kotlin" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -99,5 +101,5 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } \ No newline at end of file diff --git a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/Utils.kt b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/Utils.kt index 053a614..3abb12e 100644 --- a/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/Utils.kt +++ b/utils/shared/src/commonMain/kotlin/com/pr0gramm3r101/utils/Utils.kt @@ -175,4 +175,9 @@ operator fun PaddingValues.plus(other: PaddingValues) = PaddingValues( } ) -expect val materialYouAvailable: Boolean \ No newline at end of file +expect val materialYouAvailable: Boolean + +inline fun T.require( message: String, condition: (T) -> Boolean): T { + require(condition(this)) { message } + return this +} \ No newline at end of file