feat: Implement secure database operations and caching for encrypted accounts
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
|
kotlin("kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -91,7 +92,7 @@ dependencies {
|
|||||||
// Room for database
|
// Room for database
|
||||||
implementation("androidx.room:room-runtime:2.6.1")
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
implementation("androidx.room:room-ktx: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
|
// Biometric authentication
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ object CryptoManager {
|
|||||||
private const val KEY_SIZE = 256
|
private const val KEY_SIZE = 256
|
||||||
private const val SALT = "rosetta"
|
private const val SALT = "rosetta"
|
||||||
|
|
||||||
|
// 🚀 ОПТИМИЗАЦИЯ: Кэш для генерации ключей (seedPhrase -> KeyPair)
|
||||||
|
private val keyPairCache = mutableMapOf<String, KeyPairData>()
|
||||||
|
private val privateKeyHashCache = mutableMapOf<String, String>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Add BouncyCastle provider for secp256k1 support
|
// Add BouncyCastle provider for secp256k1 support
|
||||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||||
@@ -74,8 +78,14 @@ object CryptoManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate key pair from seed phrase using secp256k1 curve
|
* Generate key pair from seed phrase using secp256k1 curve
|
||||||
|
* 🚀 ОПТИМИЗАЦИЯ: Кэшируем результаты для избежания повторных вычислений
|
||||||
*/
|
*/
|
||||||
fun generateKeyPairFromSeed(seedPhrase: List<String>): KeyPairData {
|
fun generateKeyPairFromSeed(seedPhrase: List<String>): KeyPairData {
|
||||||
|
val cacheKey = seedPhrase.joinToString(" ")
|
||||||
|
|
||||||
|
// Проверяем кэш
|
||||||
|
keyPairCache[cacheKey]?.let { return it }
|
||||||
|
|
||||||
val privateKeyHex = seedPhraseToPrivateKey(seedPhrase)
|
val privateKeyHex = seedPhraseToPrivateKey(seedPhrase)
|
||||||
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||||
|
|
||||||
@@ -91,20 +101,40 @@ object CryptoManager {
|
|||||||
val publicKeyHex = publicKeyPoint.getEncoded(false)
|
val publicKeyHex = publicKeyPoint.getEncoded(false)
|
||||||
.joinToString("") { "%02x".format(it) }
|
.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
return KeyPairData(
|
val keyPair = KeyPairData(
|
||||||
privateKey = privateKeyHex.take(64),
|
privateKey = privateKeyHex.take(64),
|
||||||
publicKey = publicKeyHex
|
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"))
|
* Generate private key hash for protocol (SHA256(privateKey + "rosetta"))
|
||||||
|
* 🚀 ОПТИМИЗАЦИЯ: Кэшируем хэши для избежания повторных вычислений
|
||||||
*/
|
*/
|
||||||
fun generatePrivateKeyHash(privateKey: String): String {
|
fun generatePrivateKeyHash(privateKey: String): String {
|
||||||
|
// Проверяем кэш
|
||||||
|
privateKeyHashCache[privateKey]?.let { return it }
|
||||||
|
|
||||||
val data = (privateKey + SALT).toByteArray()
|
val data = (privateKey + SALT).toByteArray()
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
val hash = digest.digest(data)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package com.rosetta.messenger.providers
|
package com.rosetta.messenger.providers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.*
|
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.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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@@ -13,30 +14,41 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth state management - similar to AuthContext in React Native version
|
* Auth state management - matches React Native architecture
|
||||||
*/
|
*/
|
||||||
sealed class AuthStatus {
|
sealed class AuthStatus {
|
||||||
object Loading : AuthStatus()
|
object Loading : AuthStatus()
|
||||||
object Unauthenticated : 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 Locked(val publicKey: String) : AuthStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AuthStateData(
|
data class AuthStateData(
|
||||||
val status: AuthStatus = AuthStatus.Loading,
|
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(
|
class AuthStateManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val scope: CoroutineScope
|
private val scope: CoroutineScope
|
||||||
) {
|
) {
|
||||||
private val accountManager = AccountManager(context)
|
private val databaseService = DatabaseService.getInstance(context)
|
||||||
|
|
||||||
private val _state = MutableStateFlow(AuthStateData())
|
private val _state = MutableStateFlow(AuthStateData())
|
||||||
val state: StateFlow<AuthStateData> = _state.asStateFlow()
|
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 {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
loadAccounts()
|
loadAccounts()
|
||||||
@@ -45,74 +57,132 @@ class AuthStateManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadAccounts() = withContext(Dispatchers.IO) {
|
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(
|
_state.update { it.copy(
|
||||||
accounts = accounts,
|
hasExistingAccounts = accountsCache!!.isNotEmpty(),
|
||||||
hasExistingAccounts = accounts.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() {
|
private suspend fun checkAuthStatus() {
|
||||||
accountManager.isLoggedIn.collect { isLoggedIn ->
|
try {
|
||||||
if (isLoggedIn) {
|
val hasAccounts = databaseService.hasAccounts()
|
||||||
accountManager.currentPublicKey.first()?.let { publicKey ->
|
if (!hasAccounts) {
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
status = AuthStatus.Locked(publicKey)
|
status = AuthStatus.Unauthenticated
|
||||||
)}
|
)}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
status = AuthStatus.Unauthenticated
|
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
|
* Create new account from seed phrase
|
||||||
|
* Matches createAccountFromSeedPhrase from React Native
|
||||||
|
* 🚀 ОПТИМИЗАЦИЯ: Dispatchers.Default для CPU-интенсивной криптографии
|
||||||
*/
|
*/
|
||||||
suspend fun createAccount(
|
suspend fun createAccount(
|
||||||
seedPhrase: List<String>,
|
seedPhrase: List<String>,
|
||||||
password: String,
|
password: String
|
||||||
name: String = "Rosetta Account"
|
): Result<DecryptedAccountData> = withContext(Dispatchers.Default) {
|
||||||
): Result<DecryptedAccount> = withContext(Dispatchers.Default) {
|
|
||||||
try {
|
try {
|
||||||
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
Log.d(TAG, "🔧 Creating new account from seed phrase")
|
||||||
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
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(
|
val encryptedPrivateKey = CryptoManager.encryptWithPassword(
|
||||||
keyPair.privateKey, password
|
keyPair.privateKey, password
|
||||||
)
|
)
|
||||||
|
Log.d(TAG, "🔒 Encrypted private key: ${encryptedPrivateKey.take(50)}...")
|
||||||
|
|
||||||
|
// Step 4: Encrypt seed phrase with password
|
||||||
val encryptedSeedPhrase = CryptoManager.encryptWithPassword(
|
val encryptedSeedPhrase = CryptoManager.encryptWithPassword(
|
||||||
seedPhrase.joinToString(" "), password
|
seedPhrase.joinToString(" "), password
|
||||||
)
|
)
|
||||||
|
Log.d(TAG, "🔒 Encrypted seed phrase: ${encryptedSeedPhrase.take(50)}...")
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
// Step 5: Save to database
|
||||||
val encryptedAccount = EncryptedAccount(
|
val saved = withContext(Dispatchers.IO) {
|
||||||
|
databaseService.saveEncryptedAccount(
|
||||||
publicKey = keyPair.publicKey,
|
publicKey = keyPair.publicKey,
|
||||||
encryptedPrivateKey = encryptedPrivateKey,
|
privateKeyEncrypted = encryptedPrivateKey,
|
||||||
encryptedSeedPhrase = encryptedSeedPhrase,
|
seedPhraseEncrypted = encryptedSeedPhrase
|
||||||
name = name
|
|
||||||
)
|
)
|
||||||
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,
|
publicKey = keyPair.publicKey,
|
||||||
privateKey = keyPair.privateKey,
|
privateKey = keyPair.privateKey,
|
||||||
seedPhrase = seedPhrase,
|
|
||||||
privateKeyHash = privateKeyHash,
|
privateKeyHash = privateKeyHash,
|
||||||
name = name
|
seedPhrase = seedPhrase
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Step 7: Update state and reload accounts
|
||||||
|
currentDecryptedAccount = decryptedAccount
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
status = AuthStatus.Authenticated(decryptedAccount)
|
status = AuthStatus.Authenticated(decryptedAccount)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
loadAccounts()
|
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)
|
Result.success(decryptedAccount)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "❌ Failed to create account", e)
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,115 +192,108 @@ class AuthStateManager(
|
|||||||
*/
|
*/
|
||||||
suspend fun importAccount(
|
suspend fun importAccount(
|
||||||
seedPhrase: List<String>,
|
seedPhrase: List<String>,
|
||||||
password: String,
|
password: String
|
||||||
name: String = "Imported Account"
|
): Result<DecryptedAccountData> {
|
||||||
): Result<DecryptedAccount> {
|
Log.d(TAG, "📥 Importing account from seed phrase")
|
||||||
return createAccount(seedPhrase, password, name)
|
return createAccount(seedPhrase, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlock account with password
|
* Unlock account with password
|
||||||
|
* Matches loginWithPassword from React Native
|
||||||
*/
|
*/
|
||||||
suspend fun unlock(
|
suspend fun unlock(
|
||||||
publicKey: String,
|
publicKey: String,
|
||||||
password: String
|
password: String
|
||||||
): Result<DecryptedAccount> = withContext(Dispatchers.Default) {
|
): Result<DecryptedAccountData> = withContext(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
val encryptedAccount = withContext(Dispatchers.IO) {
|
Log.d(TAG, "🔓 Unlocking account: ${publicKey.take(20)}...")
|
||||||
accountManager.getAccount(publicKey)
|
Log.d(TAG, " - Password length: ${password.length}")
|
||||||
} ?: return@withContext Result.failure(Exception("Account not found"))
|
|
||||||
|
|
||||||
val privateKey = CryptoManager.decryptWithPassword(
|
// Decrypt account from database
|
||||||
encryptedAccount.encryptedPrivateKey, password
|
val decryptedAccount = withContext(Dispatchers.IO) {
|
||||||
) ?: return@withContext Result.failure(Exception("Invalid password"))
|
databaseService.decryptAccount(publicKey, 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"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
Log.d(TAG, "✅ Account decrypted successfully")
|
||||||
publicKey = publicKey,
|
Log.d(TAG, " - Public Key: ${decryptedAccount.publicKey.take(20)}...")
|
||||||
privateKey = privateKey,
|
Log.d(TAG, " - Private Key Hash: ${decryptedAccount.privateKeyHash}")
|
||||||
seedPhrase = seedPhrase,
|
Log.d(TAG, " - Seed Phrase: ${decryptedAccount.seedPhrase.size} words")
|
||||||
privateKeyHash = privateKeyHash,
|
|
||||||
name = encryptedAccount.name
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// Update last used timestamp
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
accountManager.setCurrentAccount(publicKey)
|
databaseService.updateLastUsed(publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
currentDecryptedAccount = decryptedAccount
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
status = AuthStatus.Authenticated(decryptedAccount)
|
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)
|
Result.success(decryptedAccount)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "❌ Failed to unlock account", e)
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lock account
|
* Logout - clears decrypted account from memory
|
||||||
*/
|
*/
|
||||||
fun lock() {
|
fun logout() {
|
||||||
val currentStatus = _state.value.status
|
Log.d(TAG, "🚪 Logging out, clearing decrypted keys from memory")
|
||||||
if (currentStatus is AuthStatus.Authenticated) {
|
currentDecryptedAccount = null
|
||||||
_state.update { it.copy(
|
|
||||||
status = AuthStatus.Locked(currentStatus.account.publicKey)
|
|
||||||
)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout
|
|
||||||
*/
|
|
||||||
suspend fun logout() {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
accountManager.logout()
|
|
||||||
}
|
|
||||||
_state.update { it.copy(
|
_state.update { it.copy(
|
||||||
status = AuthStatus.Unauthenticated
|
status = AuthStatus.Unauthenticated
|
||||||
)}
|
)}
|
||||||
loadAccounts()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to different account
|
* Delete account from database
|
||||||
*/
|
*/
|
||||||
fun switchAccount(publicKey: String) {
|
suspend fun deleteAccount(publicKey: String): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
_state.update { it.copy(
|
try {
|
||||||
status = AuthStatus.Locked(publicKey)
|
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
|
||||||
* Delete account permanently
|
if (currentDecryptedAccount?.publicKey == publicKey) {
|
||||||
*/
|
withContext(Dispatchers.Main) {
|
||||||
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()
|
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
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user