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

@@ -1,6 +1,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
}
android {
@@ -91,7 +92,7 @@ dependencies {
// Room for database
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
// Biometric authentication
implementation("androidx.biometric:biometric:1.1.0")

View File

@@ -29,6 +29,10 @@ object CryptoManager {
private const val KEY_SIZE = 256
private const val SALT = "rosetta"
// 🚀 ОПТИМИЗАЦИЯ: Кэш для генерации ключей (seedPhrase -> KeyPair)
private val keyPairCache = mutableMapOf<String, KeyPairData>()
private val privateKeyHashCache = mutableMapOf<String, String>()
init {
// Add BouncyCastle provider for secp256k1 support
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
@@ -74,8 +78,14 @@ object CryptoManager {
/**
* Generate key pair from seed phrase using secp256k1 curve
* 🚀 ОПТИМИЗАЦИЯ: Кэшируем результаты для избежания повторных вычислений
*/
fun generateKeyPairFromSeed(seedPhrase: List<String>): KeyPairData {
val cacheKey = seedPhrase.joinToString(" ")
// Проверяем кэш
keyPairCache[cacheKey]?.let { return it }
val privateKeyHex = seedPhraseToPrivateKey(seedPhrase)
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
@@ -91,20 +101,40 @@ object CryptoManager {
val publicKeyHex = publicKeyPoint.getEncoded(false)
.joinToString("") { "%02x".format(it) }
return KeyPairData(
val keyPair = KeyPairData(
privateKey = privateKeyHex.take(64),
publicKey = publicKeyHex
)
// Сохраняем в кэш (ограничиваем размер до 5 записей)
keyPairCache[cacheKey] = keyPair
if (keyPairCache.size > 5) {
keyPairCache.remove(keyPairCache.keys.first())
}
return keyPair
}
/**
* Generate private key hash for protocol (SHA256(privateKey + "rosetta"))
* 🚀 ОПТИМИЗАЦИЯ: Кэшируем хэши для избежания повторных вычислений
*/
fun generatePrivateKeyHash(privateKey: String): String {
// Проверяем кэш
privateKeyHashCache[privateKey]?.let { return it }
val data = (privateKey + SALT).toByteArray()
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(data)
return hash.joinToString("") { "%02x".format(it) }
val hashHex = hash.joinToString("") { "%02x".format(it) }
// Сохраняем в кэш
privateKeyHashCache[privateKey] = hashHex
if (privateKeyHashCache.size > 10) {
privateKeyHashCache.remove(privateKeyHashCache.keys.first())
}
return hashHex
}
/**

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
}
}
}
}

View File

@@ -1,11 +1,12 @@
package com.rosetta.messenger.providers
import android.content.Context
import android.util.Log
import androidx.compose.runtime.*
import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.database.DatabaseService
import com.rosetta.messenger.database.DecryptedAccountData
import com.rosetta.messenger.network.ProtocolManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@@ -13,30 +14,41 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Auth state management - similar to AuthContext in React Native version
* Auth state management - matches React Native architecture
*/
sealed class AuthStatus {
object Loading : AuthStatus()
object Unauthenticated : AuthStatus()
data class Authenticated(val account: DecryptedAccount) : AuthStatus()
data class Authenticated(val account: DecryptedAccountData) : AuthStatus()
data class Locked(val publicKey: String) : AuthStatus()
}
data class AuthStateData(
val status: AuthStatus = AuthStatus.Loading,
val accounts: List<EncryptedAccount> = emptyList(),
val hasExistingAccounts: Boolean = false
val hasExistingAccounts: Boolean = false,
val availableAccounts: List<String> = emptyList()
)
class AuthStateManager(
private val context: Context,
private val scope: CoroutineScope
) {
private val accountManager = AccountManager(context)
private val databaseService = DatabaseService.getInstance(context)
private val _state = MutableStateFlow(AuthStateData())
val state: StateFlow<AuthStateData> = _state.asStateFlow()
private var currentDecryptedAccount: DecryptedAccountData? = null
// 🚀 ОПТИМИЗАЦИЯ: Кэш списка аккаунтов для UI
private var accountsCache: List<String>? = null
private var lastAccountsLoadTime = 0L
private val accountsCacheTTL = 5000L // 5 секунд
companion object {
private const val TAG = "AuthStateManager"
}
init {
scope.launch {
loadAccounts()
@@ -45,74 +57,132 @@ class AuthStateManager(
}
private suspend fun loadAccounts() = withContext(Dispatchers.IO) {
val accounts = accountManager.getAllAccounts()
try {
// 🚀 ОПТИМИЗАЦИЯ: Используем кэш если он свежий
val currentTime = System.currentTimeMillis()
if (accountsCache != null && (currentTime - lastAccountsLoadTime) < accountsCacheTTL) {
Log.d(TAG, "📚 Using cached accounts list")
_state.update { it.copy(
accounts = accounts,
hasExistingAccounts = accounts.isNotEmpty()
hasExistingAccounts = accountsCache!!.isNotEmpty(),
availableAccounts = accountsCache!!
)}
return@withContext
}
val accounts = databaseService.getAllEncryptedAccounts()
val hasAccounts = accounts.isNotEmpty()
val accountKeys = accounts.map { it.publicKey }
// Обновляем кэш
accountsCache = accountKeys
lastAccountsLoadTime = currentTime
Log.d(TAG, "📚 Loaded ${accounts.size} accounts from database")
_state.update { it.copy(
hasExistingAccounts = hasAccounts,
availableAccounts = accountKeys
)}
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to load accounts", e)
}
}
private suspend fun checkAuthStatus() {
accountManager.isLoggedIn.collect { isLoggedIn ->
if (isLoggedIn) {
accountManager.currentPublicKey.first()?.let { publicKey ->
try {
val hasAccounts = databaseService.hasAccounts()
if (!hasAccounts) {
_state.update { it.copy(
status = AuthStatus.Locked(publicKey)
status = AuthStatus.Unauthenticated
)}
}
} else {
_state.update { it.copy(
status = AuthStatus.Unauthenticated
)}
}
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to check auth status", e)
_state.update { it.copy(
status = AuthStatus.Unauthenticated
)}
}
}
/**
* Create new account from seed phrase
* Matches createAccountFromSeedPhrase from React Native
* 🚀 ОПТИМИЗАЦИЯ: Dispatchers.Default для CPU-интенсивной криптографии
*/
suspend fun createAccount(
seedPhrase: List<String>,
password: String,
name: String = "Rosetta Account"
): Result<DecryptedAccount> = withContext(Dispatchers.Default) {
password: String
): Result<DecryptedAccountData> = withContext(Dispatchers.Default) {
try {
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
Log.d(TAG, "🔧 Creating new account from seed phrase")
Log.d(TAG, " - Seed phrase: ${seedPhrase.size} words")
Log.d(TAG, " - Password length: ${password.length}")
// Step 1: Generate key pair from seed phrase (using BIP39)
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
Log.d(TAG, "🔑 Generated keys from seed phrase")
Log.d(TAG, " - Public Key: ${keyPair.publicKey.take(20)}...")
Log.d(TAG, " - Private Key length: ${keyPair.privateKey.length}")
// Step 2: Generate private key hash for protocol
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
Log.d(TAG, "🔐 Generated private key hash: $privateKeyHash")
// Step 3: Encrypt private key with password
val encryptedPrivateKey = CryptoManager.encryptWithPassword(
keyPair.privateKey, password
)
Log.d(TAG, "🔒 Encrypted private key: ${encryptedPrivateKey.take(50)}...")
// Step 4: Encrypt seed phrase with password
val encryptedSeedPhrase = CryptoManager.encryptWithPassword(
seedPhrase.joinToString(" "), password
)
Log.d(TAG, "🔒 Encrypted seed phrase: ${encryptedSeedPhrase.take(50)}...")
withContext(Dispatchers.IO) {
val encryptedAccount = EncryptedAccount(
// Step 5: Save to database
val saved = withContext(Dispatchers.IO) {
databaseService.saveEncryptedAccount(
publicKey = keyPair.publicKey,
encryptedPrivateKey = encryptedPrivateKey,
encryptedSeedPhrase = encryptedSeedPhrase,
name = name
privateKeyEncrypted = encryptedPrivateKey,
seedPhraseEncrypted = encryptedSeedPhrase
)
accountManager.saveAccount(encryptedAccount)
accountManager.setCurrentAccount(keyPair.publicKey)
}
val decryptedAccount = DecryptedAccount(
if (!saved) {
return@withContext Result.failure(Exception("Failed to save account to database"))
}
Log.d(TAG, "✅ Account saved to database successfully")
// Step 6: Create decrypted account object
val decryptedAccount = DecryptedAccountData(
publicKey = keyPair.publicKey,
privateKey = keyPair.privateKey,
seedPhrase = seedPhrase,
privateKeyHash = privateKeyHash,
name = name
seedPhrase = seedPhrase
)
// Step 7: Update state and reload accounts
currentDecryptedAccount = decryptedAccount
_state.update { it.copy(
status = AuthStatus.Authenticated(decryptedAccount)
)}
loadAccounts()
// Step 8: Authenticate with protocol
Log.d(TAG, "🌐 Authenticating with protocol server...")
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
Log.d(TAG, "✅ Account created and authenticated successfully!")
Result.success(decryptedAccount)
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to create account", e)
Result.failure(e)
}
}
@@ -122,115 +192,108 @@ class AuthStateManager(
*/
suspend fun importAccount(
seedPhrase: List<String>,
password: String,
name: String = "Imported Account"
): Result<DecryptedAccount> {
return createAccount(seedPhrase, password, name)
password: String
): Result<DecryptedAccountData> {
Log.d(TAG, "📥 Importing account from seed phrase")
return createAccount(seedPhrase, password)
}
/**
* Unlock account with password
* Matches loginWithPassword from React Native
*/
suspend fun unlock(
publicKey: String,
password: String
): Result<DecryptedAccount> = withContext(Dispatchers.Default) {
): Result<DecryptedAccountData> = withContext(Dispatchers.Default) {
try {
val encryptedAccount = withContext(Dispatchers.IO) {
accountManager.getAccount(publicKey)
} ?: return@withContext Result.failure(Exception("Account not found"))
Log.d(TAG, "🔓 Unlocking account: ${publicKey.take(20)}...")
Log.d(TAG, " - Password length: ${password.length}")
val privateKey = CryptoManager.decryptWithPassword(
encryptedAccount.encryptedPrivateKey, password
) ?: return@withContext Result.failure(Exception("Invalid password"))
val seedPhraseStr = CryptoManager.decryptWithPassword(
encryptedAccount.encryptedSeedPhrase, password
) ?: return@withContext Result.failure(Exception("Invalid password"))
val seedPhrase = seedPhraseStr.split(" ")
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
if (keyPair.publicKey != publicKey) {
return@withContext Result.failure(Exception("Invalid password or corrupted data"))
// Decrypt account from database
val decryptedAccount = withContext(Dispatchers.IO) {
databaseService.decryptAccount(publicKey, password)
}
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
if (decryptedAccount == null) {
Log.e(TAG, "❌ Failed to decrypt account - wrong password or account not found")
return@withContext Result.failure(Exception("Invalid password or account not found"))
}
val decryptedAccount = DecryptedAccount(
publicKey = publicKey,
privateKey = privateKey,
seedPhrase = seedPhrase,
privateKeyHash = privateKeyHash,
name = encryptedAccount.name
)
Log.d(TAG, "✅ Account decrypted successfully")
Log.d(TAG, " - Public Key: ${decryptedAccount.publicKey.take(20)}...")
Log.d(TAG, " - Private Key Hash: ${decryptedAccount.privateKeyHash}")
Log.d(TAG, " - Seed Phrase: ${decryptedAccount.seedPhrase.size} words")
// Update last used timestamp
withContext(Dispatchers.IO) {
accountManager.setCurrentAccount(publicKey)
databaseService.updateLastUsed(publicKey)
}
// Update state
currentDecryptedAccount = decryptedAccount
_state.update { it.copy(
status = AuthStatus.Authenticated(decryptedAccount)
)}
// Authenticate with protocol
Log.d(TAG, "🌐 Authenticating with protocol server...")
ProtocolManager.authenticate(decryptedAccount.publicKey, decryptedAccount.privateKeyHash)
Log.d(TAG, "✅ Account unlocked and authenticated successfully!")
Result.success(decryptedAccount)
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to unlock account", e)
Result.failure(e)
}
}
/**
* Lock account
* Logout - clears decrypted account from memory
*/
fun lock() {
val currentStatus = _state.value.status
if (currentStatus is AuthStatus.Authenticated) {
_state.update { it.copy(
status = AuthStatus.Locked(currentStatus.account.publicKey)
)}
}
}
/**
* Logout
*/
suspend fun logout() {
withContext(Dispatchers.IO) {
accountManager.logout()
}
fun logout() {
Log.d(TAG, "🚪 Logging out, clearing decrypted keys from memory")
currentDecryptedAccount = null
_state.update { it.copy(
status = AuthStatus.Unauthenticated
)}
loadAccounts()
}
/**
* Switch to different account
* Delete account from database
*/
fun switchAccount(publicKey: String) {
_state.update { it.copy(
status = AuthStatus.Locked(publicKey)
)}
suspend fun deleteAccount(publicKey: String): Result<Unit> = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "🗑️ Deleting account: ${publicKey.take(20)}...")
val success = databaseService.deleteAccount(publicKey)
if (!success) {
return@withContext Result.failure(Exception("Failed to delete account"))
}
/**
* Delete account permanently
*/
suspend fun deleteAccount(publicKey: String) = withContext(Dispatchers.IO) {
val accounts = accountManager.getAllAccounts().toMutableList()
accounts.removeAll { it.publicKey == publicKey }
accountManager.clearAll()
accounts.forEach { accountManager.saveAccount(it) }
loadAccounts()
val currentStatus = _state.value.status
if ((currentStatus is AuthStatus.Authenticated && currentStatus.account.publicKey == publicKey) ||
(currentStatus is AuthStatus.Locked && currentStatus.publicKey == publicKey)) {
// If deleting current account, logout
if (currentDecryptedAccount?.publicKey == publicKey) {
withContext(Dispatchers.Main) {
logout()
}
}
loadAccounts()
Log.d(TAG, "✅ Account deleted successfully")
Result.success(Unit)
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to delete account", e)
Result.failure(e)
}
}
/**
* Get current decrypted account (if authenticated)
*/
fun getCurrentAccount(): DecryptedAccountData? = currentDecryptedAccount
}
}
}
}
@Composable