feat: Implement secure database operations and caching for encrypted accounts

This commit is contained in:
k1ngsterr1
2026-01-09 01:09:40 +05:00
parent 4adbbd095d
commit 2f77c16484
7 changed files with 523 additions and 105 deletions

View File

@@ -0,0 +1,28 @@
package com.rosetta.messenger.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface AccountDao {
@Query("SELECT * FROM encrypted_accounts WHERE is_active = 1 ORDER BY last_used DESC")
fun getAllAccountsFlow(): Flow<List<EncryptedAccountEntity>>
@Query("SELECT * FROM encrypted_accounts WHERE is_active = 1 ORDER BY last_used DESC")
suspend fun getAllAccounts(): List<EncryptedAccountEntity>
@Query("SELECT * FROM encrypted_accounts WHERE public_key = :publicKey LIMIT 1")
suspend fun getAccount(publicKey: String): EncryptedAccountEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAccount(account: EncryptedAccountEntity): Long
@Query("UPDATE encrypted_accounts SET last_used = :lastUsed WHERE public_key = :publicKey")
suspend fun updateLastUsed(publicKey: String, lastUsed: String)
@Query("DELETE FROM encrypted_accounts WHERE public_key = :publicKey")
suspend fun deleteAccount(publicKey: String)
@Query("SELECT COUNT(*) FROM encrypted_accounts WHERE is_active = 1")
suspend fun getAccountCount(): Int
}

View File

@@ -0,0 +1,224 @@
package com.rosetta.messenger.database
import android.content.Context
import android.util.Log
import com.rosetta.messenger.crypto.CryptoManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import java.time.Instant
/**
* Service for secure database operations
* Matches the architecture from React Native app
*/
class DatabaseService(context: Context) {
private val database = RosettaDatabase.getDatabase(context)
private val accountDao = database.accountDao()
// 🚀 ОПТИМИЗАЦИЯ: LRU кэш для зашифрованных аккаунтов (избегаем повторных запросов к БД)
private val accountCache = mutableMapOf<String, EncryptedAccountEntity>()
private val cacheMaxSize = 10
companion object {
private const val TAG = "DatabaseService"
@Volatile
private var INSTANCE: DatabaseService? = null
fun getInstance(context: Context): DatabaseService {
return INSTANCE ?: synchronized(this) {
val instance = DatabaseService(context.applicationContext)
INSTANCE = instance
instance
}
}
}
/**
* Saves encrypted account to database
*/
suspend fun saveEncryptedAccount(
publicKey: String,
privateKeyEncrypted: String,
seedPhraseEncrypted: String
): Boolean = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "💾 Saving encrypted account to database: ${publicKey.take(20)}...")
val account = EncryptedAccountEntity(
publicKey = publicKey,
privateKeyEncrypted = privateKeyEncrypted,
seedPhraseEncrypted = seedPhraseEncrypted,
createdAt = Instant.now().toString(),
lastUsed = Instant.now().toString(),
isActive = true
)
accountDao.insertAccount(account)
// 🚀 ОПТИМИЗАЦИЯ: Обновляем кэш после сохранения
accountCache[publicKey] = account
Log.d(TAG, "✅ Account saved successfully")
true
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to save account", e)
false
}
}
/**
* Loads encrypted account from database
* 🚀 ОПТИМИЗАЦИЯ: Использует кэш для избежания повторных запросов
*/
suspend fun getEncryptedAccount(publicKey: String): EncryptedAccountEntity? =
withContext(Dispatchers.IO) {
try {
// Проверяем кэш сначала
accountCache[publicKey]?.let { return@withContext it }
// Загружаем из БД и кэшируем
val account = accountDao.getAccount(publicKey)
account?.let {
accountCache[publicKey] = it
// Ограничиваем размер кэша
if (accountCache.size > cacheMaxSize) {
accountCache.remove(accountCache.keys.first())
}
}
account
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to load account", e)
null
}
}
/**
* Gets all encrypted accounts
*/
suspend fun getAllEncryptedAccounts(): List<EncryptedAccountEntity> =
withContext(Dispatchers.IO) {
try {
accountDao.getAllAccounts()
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to load accounts", e)
emptyList()
}
}
/**
* Gets all accounts as Flow for reactive updates
*/
fun getAllAccountsFlow(): Flow<List<EncryptedAccountEntity>> {
return accountDao.getAllAccountsFlow()
}
/**
* Updates last used timestamp for account
*/
suspend fun updateLastUsed(publicKey: String) = withContext(Dispatchers.IO) {
try {
accountDao.updateLastUsed(publicKey, Instant.now().toString())
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to update last used", e)
}
}
/**
* Deletes account from database
*/
suspend fun deleteAccount(publicKey: String): Boolean = withContext(Dispatchers.IO) {
try {
accountDao.deleteAccount(publicKey)
Log.d(TAG, "🗑️ Account deleted: ${publicKey.take(20)}...")
true
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to delete account", e)
false
}
}
/**
* Checks if any accounts exist
*/
suspend fun hasAccounts(): Boolean = withContext(Dispatchers.IO) {
try {
accountDao.getAccountCount() > 0
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to check accounts", e)
false
}
}
/**
* Decrypts account with password
* Returns decrypted private key and seed phrase
*/
suspend fun decryptAccount(
publicKey: String,
password: String
): DecryptedAccountData? = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "🔓 Decrypting account: ${publicKey.take(20)}...")
val encryptedAccount = getEncryptedAccount(publicKey) ?: run {
Log.e(TAG, "❌ Account not found")
return@withContext null
}
// Decrypt private key
val privateKey = try {
CryptoManager.decryptWithPassword(
encryptedAccount.privateKeyEncrypted,
password
)
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to decrypt private key - wrong password?", e)
return@withContext null
}
// Decrypt seed phrase
val seedPhraseString = try {
CryptoManager.decryptWithPassword(
encryptedAccount.seedPhraseEncrypted,
password
)
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to decrypt seed phrase - wrong password?", e)
return@withContext null
}
val seedPhrase = seedPhraseString.split(" ")
// Generate private key hash for protocol
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
Log.d(TAG, "✅ Account decrypted successfully")
Log.d(TAG, " - Public Key: ${publicKey.take(20)}...")
Log.d(TAG, " - Private Key: [DECRYPTED]")
Log.d(TAG, " - Private Key Hash: $privateKeyHash")
Log.d(TAG, " - Seed Phrase: ${seedPhrase.size} words")
DecryptedAccountData(
publicKey = publicKey,
privateKey = privateKey,
privateKeyHash = privateKeyHash,
seedPhrase = seedPhrase
)
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to decrypt account", e)
null
}
}
}
/**
* Decrypted account data
*/
data class DecryptedAccountData(
val publicKey: String,
val privateKey: String,
val privateKeyHash: String,
val seedPhrase: List<String>
)

View File

@@ -0,0 +1,38 @@
package com.rosetta.messenger.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Зашифрованный аккаунт в базе данных
* Соответствует структуре из React Native приложения
*/
@Entity(
tableName = "encrypted_accounts",
indices = [Index(value = ["public_key"], unique = true)]
)
data class EncryptedAccountEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Long = 0,
@ColumnInfo(name = "public_key")
val publicKey: String,
@ColumnInfo(name = "private_key_encrypted")
val privateKeyEncrypted: String,
@ColumnInfo(name = "seed_phrase_encrypted")
val seedPhraseEncrypted: String,
@ColumnInfo(name = "created_at")
val createdAt: String,
@ColumnInfo(name = "last_used")
val lastUsed: String? = null,
@ColumnInfo(name = "is_active")
val isActive: Boolean = true
)

View File

@@ -0,0 +1,34 @@
package com.rosetta.messenger.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(
entities = [EncryptedAccountEntity::class],
version = 1,
exportSchema = false
)
abstract class RosettaDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao
companion object {
@Volatile
private var INSTANCE: RosettaDatabase? = null
fun getDatabase(context: Context): RosettaDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
RosettaDatabase::class.java,
"rosetta_secure.db"
)
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) // WAL mode for performance
.build()
INSTANCE = instance
instance
}
}
}
}