feat: Implement message encryption in database to enhance security
This commit is contained in:
@@ -978,18 +978,23 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
/**
|
||||
* Сохранить диалог в базу данных
|
||||
* 🔒 lastMessage шифруется для безопасного хранения
|
||||
*/
|
||||
private suspend fun saveDialog(lastMessage: String, timestamp: Long) {
|
||||
val account = myPublicKey ?: return
|
||||
val opponent = opponentKey ?: return
|
||||
val privateKey = myPrivateKey ?: return
|
||||
|
||||
try {
|
||||
ProtocolManager.addLog("💾 Saving dialog: ${lastMessage.take(20)}...")
|
||||
// 🔒 Шифруем lastMessage перед сохранением
|
||||
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
|
||||
|
||||
ProtocolManager.addLog("💾 Saving dialog: ${lastMessage.take(20)}... (encrypted)")
|
||||
val existingDialog = dialogDao.getDialog(account, opponent)
|
||||
|
||||
if (existingDialog != null) {
|
||||
// Обновляем последнее сообщение
|
||||
dialogDao.updateLastMessage(account, opponent, lastMessage, timestamp)
|
||||
dialogDao.updateLastMessage(account, opponent, encryptedLastMessage, timestamp)
|
||||
ProtocolManager.addLog("✅ Dialog updated (existing)")
|
||||
} else {
|
||||
// Создаём новый диалог
|
||||
@@ -998,7 +1003,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
opponentKey = opponent,
|
||||
opponentTitle = opponentTitle,
|
||||
opponentUsername = opponentUsername,
|
||||
lastMessage = lastMessage,
|
||||
lastMessage = encryptedLastMessage,
|
||||
lastMessageTimestamp = timestamp
|
||||
))
|
||||
ProtocolManager.addLog("✅ Dialog created (new)")
|
||||
@@ -1014,13 +1019,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
*/
|
||||
private suspend fun updateDialog(opponentKey: String, lastMessage: String, timestamp: Long, incrementUnread: Boolean) {
|
||||
val account = myPublicKey ?: return
|
||||
val privateKey = myPrivateKey ?: return
|
||||
|
||||
try {
|
||||
// 🔒 Шифруем lastMessage для диалога
|
||||
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
|
||||
|
||||
val existingDialog = dialogDao.getDialog(account, opponentKey)
|
||||
|
||||
if (existingDialog != null) {
|
||||
// Обновляем последнее сообщение
|
||||
dialogDao.updateLastMessage(account, opponentKey, lastMessage, timestamp)
|
||||
dialogDao.updateLastMessage(account, opponentKey, encryptedLastMessage, timestamp)
|
||||
|
||||
// Инкрементируем непрочитанные если нужно
|
||||
if (incrementUnread) {
|
||||
@@ -1035,7 +1044,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
opponentKey = opponentKey,
|
||||
opponentTitle = opponentTitle,
|
||||
opponentUsername = opponentUsername,
|
||||
lastMessage = lastMessage,
|
||||
lastMessage = encryptedLastMessage, // 🔒 Зашифрованный
|
||||
lastMessageTimestamp = timestamp,
|
||||
unreadCount = if (incrementUnread) 1 else 0
|
||||
))
|
||||
@@ -1066,7 +1075,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
) {
|
||||
val account = myPublicKey ?: return
|
||||
val opponent = opponentPublicKey ?: opponentKey ?: return
|
||||
val privateKey = this.privateKey ?: return
|
||||
val privateKey = myPrivateKey ?: return
|
||||
|
||||
try {
|
||||
val dialogKey = getDialogKey(account, opponent)
|
||||
|
||||
@@ -30,7 +30,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.airbnb.lottie.compose.*
|
||||
import com.rosetta.messenger.R
|
||||
import com.rosetta.messenger.data.RecentSearchesManager
|
||||
import com.rosetta.messenger.database.DialogEntity
|
||||
import com.rosetta.messenger.database.RosettaDatabase
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
import com.rosetta.messenger.network.ProtocolState
|
||||
import com.rosetta.messenger.ui.components.AppleEmojiText
|
||||
@@ -126,6 +126,7 @@ fun ChatsListScreen(
|
||||
accountName: String,
|
||||
accountPhone: String,
|
||||
accountPublicKey: String,
|
||||
accountPrivateKey: String = "",
|
||||
privateKeyHash: String = "",
|
||||
onToggleTheme: () -> Unit,
|
||||
onProfileClick: () -> Unit,
|
||||
@@ -182,9 +183,9 @@ fun ChatsListScreen(
|
||||
val dialogsList by chatsViewModel.dialogs.collectAsState()
|
||||
|
||||
// Load dialogs when account is available
|
||||
LaunchedEffect(accountPublicKey) {
|
||||
if (accountPublicKey.isNotEmpty()) {
|
||||
chatsViewModel.setAccount(accountPublicKey)
|
||||
LaunchedEffect(accountPublicKey, accountPrivateKey) {
|
||||
if (accountPublicKey.isNotEmpty() && accountPrivateKey.isNotEmpty()) {
|
||||
chatsViewModel.setAccount(accountPublicKey, accountPrivateKey)
|
||||
// Устанавливаем аккаунт для RecentSearchesManager
|
||||
RecentSearchesManager.setAccount(accountPublicKey)
|
||||
}
|
||||
@@ -783,7 +784,7 @@ fun DrawerMenuItem(
|
||||
|
||||
/** Элемент диалога из базы данных - ОПТИМИЗИРОВАННЫЙ */
|
||||
@Composable
|
||||
fun DialogItem(dialog: DialogEntity, isDarkTheme: Boolean, onClick: () -> Unit) {
|
||||
fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit) {
|
||||
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
|
||||
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||
val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) }
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.rosetta.messenger.ui.chats
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.rosetta.messenger.crypto.CryptoManager
|
||||
import com.rosetta.messenger.database.DialogEntity
|
||||
import com.rosetta.messenger.database.RosettaDatabase
|
||||
import com.rosetta.messenger.network.ProtocolManager
|
||||
@@ -10,9 +11,26 @@ import com.rosetta.messenger.network.SearchUser
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* UI модель диалога с расшифрованным lastMessage
|
||||
*/
|
||||
data class DialogUiModel(
|
||||
val id: Long,
|
||||
val account: String,
|
||||
val opponentKey: String,
|
||||
val opponentTitle: String,
|
||||
val opponentUsername: String,
|
||||
val lastMessage: String, // 🔓 Расшифрованный текст
|
||||
val lastMessageTimestamp: Long,
|
||||
val unreadCount: Int,
|
||||
val isOnline: Int,
|
||||
val lastSeen: Long,
|
||||
val verified: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* ViewModel для списка чатов
|
||||
* Загружает диалоги из базы данных
|
||||
* Загружает диалоги из базы данных и расшифровывает lastMessage
|
||||
*/
|
||||
class ChatsListViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@@ -20,10 +38,11 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
private val dialogDao = database.dialogDao()
|
||||
|
||||
private var currentAccount: String = ""
|
||||
private var currentPrivateKey: String? = null
|
||||
|
||||
// Список диалогов из базы
|
||||
private val _dialogs = MutableStateFlow<List<DialogEntity>>(emptyList())
|
||||
val dialogs: StateFlow<List<DialogEntity>> = _dialogs.asStateFlow()
|
||||
// Список диалогов с расшифрованными сообщениями
|
||||
private val _dialogs = MutableStateFlow<List<DialogUiModel>>(emptyList())
|
||||
val dialogs: StateFlow<List<DialogUiModel>> = _dialogs.asStateFlow()
|
||||
|
||||
// Загрузка
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
@@ -32,15 +51,44 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
/**
|
||||
* Установить текущий аккаунт и загрузить диалоги
|
||||
*/
|
||||
fun setAccount(publicKey: String) {
|
||||
fun setAccount(publicKey: String, privateKey: String) {
|
||||
if (currentAccount == publicKey) return
|
||||
currentAccount = publicKey
|
||||
currentPrivateKey = privateKey
|
||||
|
||||
viewModelScope.launch {
|
||||
dialogDao.getDialogsFlow(publicKey)
|
||||
.collect { dialogsList ->
|
||||
_dialogs.value = dialogsList
|
||||
ProtocolManager.addLog("📋 Dialogs loaded: ${dialogsList.size}")
|
||||
// 🔓 Расшифровываем lastMessage для каждого диалога
|
||||
val decryptedDialogs = dialogsList.map { dialog ->
|
||||
val decryptedLastMessage = try {
|
||||
if (privateKey.isNotEmpty() && dialog.lastMessage.isNotEmpty()) {
|
||||
CryptoManager.decryptWithPassword(dialog.lastMessage, privateKey)
|
||||
?: dialog.lastMessage
|
||||
} else {
|
||||
dialog.lastMessage
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
dialog.lastMessage // Fallback на зашифрованный текст
|
||||
}
|
||||
|
||||
DialogUiModel(
|
||||
id = dialog.id,
|
||||
account = dialog.account,
|
||||
opponentKey = dialog.opponentKey,
|
||||
opponentTitle = dialog.opponentTitle,
|
||||
opponentUsername = dialog.opponentUsername,
|
||||
lastMessage = decryptedLastMessage,
|
||||
lastMessageTimestamp = dialog.lastMessageTimestamp,
|
||||
unreadCount = dialog.unreadCount,
|
||||
isOnline = dialog.isOnline,
|
||||
lastSeen = dialog.lastSeen,
|
||||
verified = dialog.verified
|
||||
)
|
||||
}
|
||||
|
||||
_dialogs.value = decryptedDialogs
|
||||
ProtocolManager.addLog("📋 Dialogs loaded: ${decryptedDialogs.size} (lastMessages decrypted)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,12 +106,16 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
isOnline: Int = 0
|
||||
) {
|
||||
if (currentAccount.isEmpty()) return
|
||||
val privateKey = currentPrivateKey ?: return
|
||||
|
||||
// 🔒 Шифруем lastMessage перед сохранением
|
||||
val encryptedLastMessage = CryptoManager.encryptWithPassword(lastMessage, privateKey)
|
||||
|
||||
val existingDialog = dialogDao.getDialog(currentAccount, opponentKey)
|
||||
|
||||
if (existingDialog != null) {
|
||||
// Обновляем
|
||||
dialogDao.updateLastMessage(currentAccount, opponentKey, lastMessage, timestamp)
|
||||
dialogDao.updateLastMessage(currentAccount, opponentKey, encryptedLastMessage, timestamp)
|
||||
if (opponentTitle.isNotEmpty()) {
|
||||
dialogDao.updateOpponentInfo(currentAccount, opponentKey, opponentTitle, opponentUsername, verified)
|
||||
}
|
||||
@@ -74,7 +126,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
opponentKey = opponentKey,
|
||||
opponentTitle = opponentTitle,
|
||||
opponentUsername = opponentUsername,
|
||||
lastMessage = lastMessage,
|
||||
lastMessage = encryptedLastMessage, // 🔒 Зашифрованный
|
||||
lastMessageTimestamp = timestamp,
|
||||
verified = verified,
|
||||
isOnline = isOnline
|
||||
@@ -83,9 +135,9 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертировать DialogEntity в SearchUser для навигации
|
||||
* Конвертировать DialogUiModel в SearchUser для навигации
|
||||
*/
|
||||
fun dialogToSearchUser(dialog: DialogEntity): SearchUser {
|
||||
fun dialogToSearchUser(dialog: DialogUiModel): SearchUser {
|
||||
return SearchUser(
|
||||
title = dialog.opponentTitle,
|
||||
username = dialog.opponentUsername,
|
||||
|
||||
Reference in New Issue
Block a user