From a5fe4bc29eb636e81c7fbb8a2449ad91920b7bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=91=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=B5=D0=B2?= Date: Sun, 5 Apr 2026 18:02:58 +0800 Subject: [PATCH] Remove hardcoded API key, add encrypted storage with AES-256 --- app/build.gradle | 6 +++ .../java/com/mistral/chat/ui/MainActivity.kt | 48 ++++++++++++++----- app/src/main/res/values/strings.xml | 2 + 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c4df6ef..81982a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,8 +40,14 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' + implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.google.code.gson:gson:2.10.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + + implementation 'io.noties.markwon:core:4.6.2' + implementation 'io.noties.markwon:ext-strikethrough:4.6.2' + implementation 'io.noties.markwon:ext-tables:4.6.2' + implementation 'io.noties.markwon:ext-tasklist:4.6.2' } \ No newline at end of file diff --git a/app/src/main/java/com/mistral/chat/ui/MainActivity.kt b/app/src/main/java/com/mistral/chat/ui/MainActivity.kt index bbcfea4..cd9b12e 100644 --- a/app/src/main/java/com/mistral/chat/ui/MainActivity.kt +++ b/app/src/main/java/com/mistral/chat/ui/MainActivity.kt @@ -12,6 +12,7 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.PopupMenu import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -19,6 +20,8 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey import com.google.android.material.color.DynamicColors import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.textfield.TextInputEditText @@ -48,6 +51,7 @@ class MainActivity : AppCompatActivity() { private var availableModels: List> = emptyList() private var selectedModelName: String = "mistral-small-latest" private lateinit var prefs: SharedPreferences + private lateinit var encryptedPrefs: SharedPreferences companion object { private const val PREFS_NAME = "mistral_chat_prefs" @@ -57,7 +61,6 @@ class MainActivity : AppCompatActivity() { private const val KEY_MESSAGES = "chat_messages" private const val KEY_PROFILE_HASH = "profile_hash" private const val KEY_API_KEY = "api_key" - private const val DEFAULT_API_KEY = "YW0IjDBRLuyEBcgNjVeVUFlMI6fcZYLA" } override fun onCreate(savedInstanceState: Bundle?) { @@ -70,6 +73,22 @@ class MainActivity : AppCompatActivity() { gson = Gson() prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + val masterKey = MasterKey.Builder(this) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + encryptedPrefs = EncryptedSharedPreferences.create( + this, + PREFS_NAME + "_secure", + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + if (!hasApiKey()) { + showApiKeyDialog() + } + client = MistralClient(getApiKey()) loadMessages() @@ -231,22 +250,27 @@ class MainActivity : AppCompatActivity() { } private fun getApiKey(): String { - return prefs.getString(KEY_API_KEY, DEFAULT_API_KEY) ?: DEFAULT_API_KEY + return encryptedPrefs.getString(KEY_API_KEY, null) ?: "" + } + + private fun hasApiKey(): Boolean { + return encryptedPrefs.contains(KEY_API_KEY) } private fun saveApiKey(apiKey: String) { - prefs.edit().putString(KEY_API_KEY, apiKey).apply() + encryptedPrefs.edit().putString(KEY_API_KEY, apiKey).apply() client = MistralClient(apiKey) } private fun deleteApiKey() { - prefs.edit().remove(KEY_API_KEY).apply() - client = MistralClient(DEFAULT_API_KEY) + encryptedPrefs.edit().remove(KEY_API_KEY).apply() + Toast.makeText(this, getString(R.string.api_key_deleted), Toast.LENGTH_SHORT).show() + showApiKeyDialog() } private fun showApiKeyDialog() { val currentKey = getApiKey() - val hasCustomKey = currentKey != DEFAULT_API_KEY && prefs.contains(KEY_API_KEY) + val hasCustomKey = currentKey.isNotEmpty() val displayKey = if (hasCustomKey && currentKey.length > 8) { currentKey.take(4) + "*".repeat(currentKey.length - 8) + currentKey.takeLast(4) } else if (hasCustomKey) { @@ -270,25 +294,23 @@ class MainActivity : AppCompatActivity() { val newKey = inputField.text?.toString()?.trim() if (!newKey.isNullOrEmpty()) { saveApiKey(newKey) - showToast(getString(R.string.api_key_saved)) + Toast.makeText(this, getString(R.string.api_key_saved), Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, getString(R.string.enter_api_key), Toast.LENGTH_SHORT).show() } } - .setNegativeButton(R.string.cancel, null) .apply { if (hasCustomKey) { + setNegativeButton(R.string.cancel, null) setNeutralButton(R.string.delete) { _, _ -> deleteApiKey() - showToast(getString(R.string.api_key_deleted)) } } } + .setCancelable(false) .show() } - private fun showToast(message: String) { - android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show() - } - private fun sendMessage(userInput: String) { val selectedModel = selectedModelName diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2095aec..703f8ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,4 +32,6 @@ API ключ удалён API ключ не установлен Текущий ключ: %s + Введите API ключ + Требуется API ключ Mistral \ No newline at end of file