feat: Implement secure database operations and caching for encrypted accounts
This commit is contained in:
@@ -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()
|
||||
_state.update { it.copy(
|
||||
accounts = accounts,
|
||||
hasExistingAccounts = accounts.isNotEmpty()
|
||||
)}
|
||||
try {
|
||||
// 🚀 ОПТИМИЗАЦИЯ: Используем кэш если он свежий
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (accountsCache != null && (currentTime - lastAccountsLoadTime) < accountsCacheTTL) {
|
||||
Log.d(TAG, "📚 Using cached accounts list")
|
||||
_state.update { it.copy(
|
||||
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 ->
|
||||
_state.update { it.copy(
|
||||
status = AuthStatus.Locked(publicKey)
|
||||
)}
|
||||
}
|
||||
try {
|
||||
val hasAccounts = databaseService.hasAccounts()
|
||||
if (!hasAccounts) {
|
||||
_state.update { it.copy(
|
||||
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,113 +192,106 @@ 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"))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete account permanently
|
||||
* Get current decrypted account (if authenticated)
|
||||
*/
|
||||
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)) {
|
||||
logout()
|
||||
fun getCurrentAccount(): DecryptedAccountData? = currentDecryptedAccount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user