diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 099d9ec..7e2a8b1 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -514,6 +514,18 @@ fun MainScreen( // Состояние протокола для передачи в SearchScreen 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(null) } diff --git a/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt b/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt index 3b50ed4..5651508 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt @@ -108,36 +108,47 @@ object CryptoManager { /** * Convert seed phrase to private key (32 bytes hex string) * - * ⚠️ НОВЫЙ МЕТОД (crypto_new): Использует SHA256(seedPhrase) вместо BIP39 - * Совместимо с JavaScript реализацией crypto_new/crypto.ts: + * Алгоритм (совместим с Desktop desktop-rosetta): + * 1. BIP39 mnemonicToSeed(phrase, "") → 64 байта (PBKDF2-SHA512, 2048 итераций) + * 2. Конвертация seed в hex-строку (128 символов) + * 3. SHA256(hexSeed) → 32 байта privateKey + * + * Desktop эквивалент (SetPassword.tsx + crypto.ts): * ```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 { - // Новый метод: SHA256(seedPhrase joined by space) - val seedString = seedPhrase.joinToString(" ") - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(seedString.toByteArray(Charsets.UTF_8)) + // Step 1: BIP39 mnemonicToSeed — PBKDF2-SHA512 with 2048 iterations + // passphrase = "" (empty), salt = "mnemonic" + passphrase + val bip39Seed = MnemonicCode.toSeed(seedPhrase, "") + + // 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) } } /** * Generate key pair from seed phrase using secp256k1 curve * - * ⚠️ НОВЫЙ МЕТОД (crypto_new): - * - privateKey = SHA256(seedPhrase) - 32 байта + * Алгоритм (совместим с Desktop desktop-rosetta): + * - privateKey = SHA256(hex(BIP39_seed(mnemonic))) - 32 байта * - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта * - * Совместимо с JavaScript реализацией crypto_new/crypto.ts: + * Desktop эквивалент (crypto.ts): * ```js * const privateKey = sha256.create().update(seed).digest().toHex().toString(); * const publicKey = secp256k1.getPublicKey(Buffer.from(privateKey, "hex"), true); * ``` * - * 🚀 ОПТИМИЗАЦИЯ: Кэшируем результаты для избежания повторных вычислений + * Кэшируем результаты для избежания повторных вычислений */ fun generateKeyPairFromSeed(seedPhrase: List): KeyPairData { val cacheKey = seedPhrase.joinToString(" ") diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index a90e3fd..1ae050d 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -103,6 +103,10 @@ class Protocol( private var lastPublicKey: String? = null private var lastPrivateHash: String? = null + // Getters for ProtocolManager to fetch own profile + fun getPublicKey(): String? = lastPublicKey + fun getPrivateHash(): String? = lastPrivateHash + // Heartbeat private var heartbeatJob: Job? = null diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 101ad14..6892899 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -1,6 +1,7 @@ package com.rosetta.messenger.network import android.content.Context +import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.MessageRepository import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow @@ -21,6 +22,7 @@ object ProtocolManager { private var protocol: Protocol? = null private var messageRepository: MessageRepository? = null + private var appContext: Context? = null private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // Debug logs for dev console - 🚀 ОТКЛЮЧЕНО для производительности @@ -59,6 +61,7 @@ object ProtocolManager { * Инициализация с контекстом для доступа к MessageRepository */ fun initialize(context: Context) { + appContext = context.applicationContext messageRepository = MessageRepository.getInstance(context) setupPacketHandlers() setupStateMonitoring() @@ -156,19 +159,32 @@ object ProtocolManager { // 🔥 Обработчик поиска/user info (0x03) // Обновляет информацию о пользователе в диалогах когда приходит ответ от сервера + // + обновляет own profile (username/name) аналогично Desktop useUserInformation() waitPacket(0x03) { packet -> val searchPacket = packet as PacketSearch - // Обновляем информацию о пользователях в диалогах if (searchPacket.users.isNotEmpty()) { - scope.launch(Dispatchers.IO) { // 🔥 Запускаем на IO потоке для работы с БД + scope.launch(Dispatchers.IO) { + val ownPublicKey = getProtocol().getPublicKey() searchPacket.users.forEach { user -> + // Обновляем инфо в диалогах (для всех пользователей) messageRepository?.updateDialogUserInfo( user.publicKey, user.title, user.username, 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) { getProtocol().startHandshake(publicKey, privateHash) - // 🚀 Запрашиваем транспортный сервер после авторизации + // 🚀 Запрашиваем транспортный сервер и own profile после авторизации scope.launch { delay(500) // Даём время на завершение handshake 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) */ diff --git a/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt b/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt index 3b5c306..a78ac2d 100644 --- a/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt +++ b/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt @@ -171,16 +171,6 @@ class AuthStateManager( } } - /** - * Import existing account from seed phrase - */ - suspend fun importAccount( - seedPhrase: List, - password: String - ): Result { - return createAccount(seedPhrase, password) - } - /** * Unlock account with password * Matches loginWithPassword from React Native diff --git a/app/src/test/java/com/rosetta/messenger/crypto/CryptoManagerTest.kt b/app/src/test/java/com/rosetta/messenger/crypto/CryptoManagerTest.kt index 6294700..c66f801 100644 --- a/app/src/test/java/com/rosetta/messenger/crypto/CryptoManagerTest.kt +++ b/app/src/test/java/com/rosetta/messenger/crypto/CryptoManagerTest.kt @@ -37,8 +37,10 @@ class CryptoManagerTest { val seedPhrase = CryptoManager.generateSeedPhrase() val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase) - assertTrue("Public key should start with 04", keyPair.publicKey.startsWith("04")) - assertTrue("Public key should be 130 chars (65 bytes hex)", keyPair.publicKey.length == 130) + // Compressed public key: starts with "02" or "03", 66 hex chars (33 bytes) + 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) } @@ -190,5 +192,77 @@ class CryptoManagerTest { 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' }) } + + /** + * 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 + ) + } }