feat: enhance profile fetching and update mechanisms in ProtocolManager and MainScreen
This commit is contained in:
@@ -514,6 +514,18 @@ fun MainScreen(
|
|||||||
|
|
||||||
// Состояние протокола для передачи в SearchScreen
|
// Состояние протокола для передачи в SearchScreen
|
||||||
val protocolState by ProtocolManager.state.collectAsState()
|
val protocolState by ProtocolManager.state.collectAsState()
|
||||||
|
|
||||||
|
// Перечитать username/name после получения own profile с сервера
|
||||||
|
// Аналог Desktop: useUserInformation автоматически обновляет UI при PacketSearch ответе
|
||||||
|
LaunchedEffect(protocolState) {
|
||||||
|
if (protocolState == ProtocolState.AUTHENTICATED && accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") {
|
||||||
|
delay(2000) // Ждём fetchOwnProfile() → PacketSearch → AccountManager update
|
||||||
|
val accountManager = AccountManager(context)
|
||||||
|
val encryptedAccount = accountManager.getAccount(accountPublicKey)
|
||||||
|
accountUsername = encryptedAccount?.username ?: ""
|
||||||
|
accountName = encryptedAccount?.name ?: accountName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Навигация между экранами
|
// Навигация между экранами
|
||||||
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
var selectedUser by remember { mutableStateOf<SearchUser?>(null) }
|
||||||
|
|||||||
@@ -108,36 +108,47 @@ object CryptoManager {
|
|||||||
/**
|
/**
|
||||||
* Convert seed phrase to private key (32 bytes hex string)
|
* Convert seed phrase to private key (32 bytes hex string)
|
||||||
*
|
*
|
||||||
* ⚠️ НОВЫЙ МЕТОД (crypto_new): Использует SHA256(seedPhrase) вместо BIP39
|
* Алгоритм (совместим с Desktop desktop-rosetta):
|
||||||
* Совместимо с JavaScript реализацией crypto_new/crypto.ts:
|
* 1. BIP39 mnemonicToSeed(phrase, "") → 64 байта (PBKDF2-SHA512, 2048 итераций)
|
||||||
|
* 2. Конвертация seed в hex-строку (128 символов)
|
||||||
|
* 3. SHA256(hexSeed) → 32 байта privateKey
|
||||||
|
*
|
||||||
|
* Desktop эквивалент (SetPassword.tsx + crypto.ts):
|
||||||
* ```js
|
* ```js
|
||||||
* const privateKey = sha256.create().update(seed).digest().toHex().toString();
|
* let seed = await mnemonicToSeed(phrase); // BIP39 → 64 bytes
|
||||||
|
* let hex = Buffer.from(seed).toString('hex'); // → 128-char hex string
|
||||||
|
* let keypair = await generateKeyPairFromSeed(hex); // SHA256(hex) → privateKey
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
fun seedPhraseToPrivateKey(seedPhrase: List<String>): String {
|
fun seedPhraseToPrivateKey(seedPhrase: List<String>): String {
|
||||||
// Новый метод: SHA256(seedPhrase joined by space)
|
// Step 1: BIP39 mnemonicToSeed — PBKDF2-SHA512 with 2048 iterations
|
||||||
val seedString = seedPhrase.joinToString(" ")
|
// passphrase = "" (empty), salt = "mnemonic" + passphrase
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
val bip39Seed = MnemonicCode.toSeed(seedPhrase, "")
|
||||||
val hash = digest.digest(seedString.toByteArray(Charsets.UTF_8))
|
|
||||||
|
// Step 2: Convert to hex string (128 chars for 64 bytes)
|
||||||
|
val hexSeed = bip39Seed.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
// Step 3: SHA256(hexSeed) — matches Desktop's sha256.create().update(hex).digest()
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hash = digest.digest(hexSeed.toByteArray(Charsets.UTF_8))
|
||||||
|
|
||||||
// Convert to hex string (64 characters for 32 bytes)
|
|
||||||
return hash.joinToString("") { "%02x".format(it) }
|
return hash.joinToString("") { "%02x".format(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate key pair from seed phrase using secp256k1 curve
|
* Generate key pair from seed phrase using secp256k1 curve
|
||||||
*
|
*
|
||||||
* ⚠️ НОВЫЙ МЕТОД (crypto_new):
|
* Алгоритм (совместим с Desktop desktop-rosetta):
|
||||||
* - privateKey = SHA256(seedPhrase) - 32 байта
|
* - privateKey = SHA256(hex(BIP39_seed(mnemonic))) - 32 байта
|
||||||
* - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта
|
* - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта
|
||||||
*
|
*
|
||||||
* Совместимо с JavaScript реализацией crypto_new/crypto.ts:
|
* Desktop эквивалент (crypto.ts):
|
||||||
* ```js
|
* ```js
|
||||||
* const privateKey = sha256.create().update(seed).digest().toHex().toString();
|
* const privateKey = sha256.create().update(seed).digest().toHex().toString();
|
||||||
* const publicKey = secp256k1.getPublicKey(Buffer.from(privateKey, "hex"), true);
|
* const publicKey = secp256k1.getPublicKey(Buffer.from(privateKey, "hex"), true);
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* 🚀 ОПТИМИЗАЦИЯ: Кэшируем результаты для избежания повторных вычислений
|
* Кэшируем результаты для избежания повторных вычислений
|
||||||
*/
|
*/
|
||||||
fun generateKeyPairFromSeed(seedPhrase: List<String>): KeyPairData {
|
fun generateKeyPairFromSeed(seedPhrase: List<String>): KeyPairData {
|
||||||
val cacheKey = seedPhrase.joinToString(" ")
|
val cacheKey = seedPhrase.joinToString(" ")
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ class Protocol(
|
|||||||
private var lastPublicKey: String? = null
|
private var lastPublicKey: String? = null
|
||||||
private var lastPrivateHash: String? = null
|
private var lastPrivateHash: String? = null
|
||||||
|
|
||||||
|
// Getters for ProtocolManager to fetch own profile
|
||||||
|
fun getPublicKey(): String? = lastPublicKey
|
||||||
|
fun getPrivateHash(): String? = lastPrivateHash
|
||||||
|
|
||||||
// Heartbeat
|
// Heartbeat
|
||||||
private var heartbeatJob: Job? = null
|
private var heartbeatJob: Job? = null
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.rosetta.messenger.network
|
package com.rosetta.messenger.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -21,6 +22,7 @@ object ProtocolManager {
|
|||||||
|
|
||||||
private var protocol: Protocol? = null
|
private var protocol: Protocol? = null
|
||||||
private var messageRepository: MessageRepository? = null
|
private var messageRepository: MessageRepository? = null
|
||||||
|
private var appContext: Context? = null
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
// Debug logs for dev console - 🚀 ОТКЛЮЧЕНО для производительности
|
// Debug logs for dev console - 🚀 ОТКЛЮЧЕНО для производительности
|
||||||
@@ -59,6 +61,7 @@ object ProtocolManager {
|
|||||||
* Инициализация с контекстом для доступа к MessageRepository
|
* Инициализация с контекстом для доступа к MessageRepository
|
||||||
*/
|
*/
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
|
appContext = context.applicationContext
|
||||||
messageRepository = MessageRepository.getInstance(context)
|
messageRepository = MessageRepository.getInstance(context)
|
||||||
setupPacketHandlers()
|
setupPacketHandlers()
|
||||||
setupStateMonitoring()
|
setupStateMonitoring()
|
||||||
@@ -156,19 +159,32 @@ object ProtocolManager {
|
|||||||
|
|
||||||
// 🔥 Обработчик поиска/user info (0x03)
|
// 🔥 Обработчик поиска/user info (0x03)
|
||||||
// Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера
|
// Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера
|
||||||
|
// + обновляет own profile (username/name) аналогично Desktop useUserInformation()
|
||||||
waitPacket(0x03) { packet ->
|
waitPacket(0x03) { packet ->
|
||||||
val searchPacket = packet as PacketSearch
|
val searchPacket = packet as PacketSearch
|
||||||
|
|
||||||
// Обновляем информацию о пользователях в диалогах
|
|
||||||
if (searchPacket.users.isNotEmpty()) {
|
if (searchPacket.users.isNotEmpty()) {
|
||||||
scope.launch(Dispatchers.IO) { // 🔥 Запускаем на IO потоке для работы с БД
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val ownPublicKey = getProtocol().getPublicKey()
|
||||||
searchPacket.users.forEach { user ->
|
searchPacket.users.forEach { user ->
|
||||||
|
// Обновляем инфо в диалогах (для всех пользователей)
|
||||||
messageRepository?.updateDialogUserInfo(
|
messageRepository?.updateDialogUserInfo(
|
||||||
user.publicKey,
|
user.publicKey,
|
||||||
user.title,
|
user.title,
|
||||||
user.username,
|
user.username,
|
||||||
user.verified
|
user.verified
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Если это наш own profile — сохраняем username/name в AccountManager
|
||||||
|
if (user.publicKey == ownPublicKey && appContext != null) {
|
||||||
|
val accountManager = AccountManager(appContext!!)
|
||||||
|
if (user.title.isNotBlank()) {
|
||||||
|
accountManager.updateAccountName(user.publicKey, user.title)
|
||||||
|
}
|
||||||
|
if (user.username.isNotBlank()) {
|
||||||
|
accountManager.updateAccountUsername(user.publicKey, user.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,13 +232,29 @@ object ProtocolManager {
|
|||||||
fun authenticate(publicKey: String, privateHash: String) {
|
fun authenticate(publicKey: String, privateHash: String) {
|
||||||
getProtocol().startHandshake(publicKey, privateHash)
|
getProtocol().startHandshake(publicKey, privateHash)
|
||||||
|
|
||||||
// 🚀 Запрашиваем транспортный сервер после авторизации
|
// 🚀 Запрашиваем транспортный сервер и own profile после авторизации
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(500) // Даём время на завершение handshake
|
delay(500) // Даём время на завершение handshake
|
||||||
TransportManager.requestTransportServer()
|
TransportManager.requestTransportServer()
|
||||||
|
fetchOwnProfile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запрашивает собственный профиль с сервера (username, name/title).
|
||||||
|
* Аналог Desktop: useUserInformation(ownPublicKey) → PacketSearch(0x03)
|
||||||
|
*/
|
||||||
|
private fun fetchOwnProfile() {
|
||||||
|
val publicKey = getProtocol().getPublicKey() ?: return
|
||||||
|
val privateHash = getProtocol().getPrivateHash() ?: return
|
||||||
|
|
||||||
|
val packet = PacketSearch().apply {
|
||||||
|
this.privateKey = privateHash
|
||||||
|
this.search = publicKey
|
||||||
|
}
|
||||||
|
send(packet)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send packet (simplified)
|
* Send packet (simplified)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -171,16 +171,6 @@ class AuthStateManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Import existing account from seed phrase
|
|
||||||
*/
|
|
||||||
suspend fun importAccount(
|
|
||||||
seedPhrase: List<String>,
|
|
||||||
password: String
|
|
||||||
): Result<DecryptedAccountData> {
|
|
||||||
return createAccount(seedPhrase, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlock account with password
|
* Unlock account with password
|
||||||
* Matches loginWithPassword from React Native
|
* Matches loginWithPassword from React Native
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ class CryptoManagerTest {
|
|||||||
val seedPhrase = CryptoManager.generateSeedPhrase()
|
val seedPhrase = CryptoManager.generateSeedPhrase()
|
||||||
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
||||||
|
|
||||||
assertTrue("Public key should start with 04", keyPair.publicKey.startsWith("04"))
|
// Compressed public key: starts with "02" or "03", 66 hex chars (33 bytes)
|
||||||
assertTrue("Public key should be 130 chars (65 bytes hex)", keyPair.publicKey.length == 130)
|
assertTrue("Public key should start with 02 or 03 (compressed)",
|
||||||
|
keyPair.publicKey.startsWith("02") || keyPair.publicKey.startsWith("03"))
|
||||||
|
assertEquals("Public key should be 66 chars (33 bytes hex, compressed)", 66, keyPair.publicKey.length)
|
||||||
assertTrue("Private key should be 64 chars (32 bytes hex)", keyPair.privateKey.length == 64)
|
assertTrue("Private key should be 64 chars (32 bytes hex)", keyPair.privateKey.length == 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,5 +192,77 @@ class CryptoManagerTest {
|
|||||||
assertEquals("Same seed should produce same private key", privateKey1, privateKey2)
|
assertEquals("Same seed should produce same private key", privateKey1, privateKey2)
|
||||||
assertTrue("Private key should be hex", privateKey1.all { it in '0'..'9' || it in 'a'..'f' })
|
assertTrue("Private key should be hex", privateKey1.all { it in '0'..'9' || it in 'a'..'f' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform compatibility test.
|
||||||
|
* These test vectors were generated using the Desktop (Electron) app's derivation:
|
||||||
|
* 1. mnemonicToSeed(phrase) → BIP39 PBKDF2-SHA512 (2048 iterations) → 64 bytes
|
||||||
|
* 2. hex(seed) → 128-char hex string
|
||||||
|
* 3. SHA256(hexSeed) → 32-byte privateKey
|
||||||
|
* 4. SHA256(privateKey + "rosetta") → privateKeyHash
|
||||||
|
*
|
||||||
|
* Mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
* Desktop equivalent (SetPassword.tsx):
|
||||||
|
* let seed = await mnemonicToSeed(phrase);
|
||||||
|
* let hex = Buffer.from(seed).toString('hex');
|
||||||
|
* let keypair = await generateKeyPairFromSeed(hex);
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `seedPhraseToPrivateKey should match Desktop derivation`() {
|
||||||
|
val seedPhrase = listOf("abandon", "abandon", "abandon", "abandon", "abandon",
|
||||||
|
"abandon", "abandon", "abandon", "abandon", "abandon",
|
||||||
|
"abandon", "about")
|
||||||
|
|
||||||
|
val expectedPrivateKey = "0f57707d9198b6dc74195e972fa4fa214a2986a8bdfea7abf952e86f57715a31"
|
||||||
|
val privateKey = CryptoManager.seedPhraseToPrivateKey(seedPhrase)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Private key must match Desktop's BIP39 → hex → SHA256 derivation",
|
||||||
|
expectedPrivateKey,
|
||||||
|
privateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `generatePrivateKeyHash should match Desktop derivation`() {
|
||||||
|
// privateKey derived from "abandon...about" seed via BIP39 → SHA256
|
||||||
|
val privateKey = "0f57707d9198b6dc74195e972fa4fa214a2986a8bdfea7abf952e86f57715a31"
|
||||||
|
val expectedHash = "4e218a657b3a993f9f1dfcb18ecf39e2f322e89ddd9f98648b64142b376d910b"
|
||||||
|
|
||||||
|
val hash = CryptoManager.generatePrivateKeyHash(privateKey)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"Private key hash must match Desktop's SHA256(privateKey + 'rosetta')",
|
||||||
|
expectedHash,
|
||||||
|
hash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `full key derivation flow should produce Desktop-compatible keys`() {
|
||||||
|
val seedPhrase = listOf("abandon", "abandon", "abandon", "abandon", "abandon",
|
||||||
|
"abandon", "abandon", "abandon", "abandon", "abandon",
|
||||||
|
"abandon", "about")
|
||||||
|
|
||||||
|
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
|
||||||
|
|
||||||
|
// Verify privateKey matches Desktop
|
||||||
|
assertEquals(
|
||||||
|
"0f57707d9198b6dc74195e972fa4fa214a2986a8bdfea7abf952e86f57715a31",
|
||||||
|
keyPair.privateKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify compressed public key format (02 or 03 prefix, 66 hex chars)
|
||||||
|
assertTrue("Public key should be compressed (66 hex chars)", keyPair.publicKey.length == 66)
|
||||||
|
assertTrue("Public key should start with 02 or 03",
|
||||||
|
keyPair.publicKey.startsWith("02") || keyPair.publicKey.startsWith("03"))
|
||||||
|
|
||||||
|
// Verify privateKeyHash matches Desktop
|
||||||
|
val hash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
|
||||||
|
assertEquals(
|
||||||
|
"4e218a657b3a993f9f1dfcb18ecf39e2f322e89ddd9f98648b64142b376d910b",
|
||||||
|
hash
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user