feat: Implement baseline profile generation and startup benchmarking

- Added baseline profile generator for Rosetta Messenger to optimize startup performance.
- Created startup benchmark tests to measure cold start times under different compilation modes.
- Introduced a new Gradle module for baseline profile and benchmark tests.
- Updated ChatsListViewModel to show loading skeleton while data is being fetched.
- Improved keyboard handling in MessageInputBar by using SHOW_IMPLICIT instead of SHOW_FORCED.
- Minor code cleanups and optimizations across various components.
This commit is contained in:
2026-02-08 09:21:05 +05:00
parent 8b8c883a63
commit abe1a1a710
13 changed files with 1111 additions and 564 deletions

View File

@@ -1,47 +1,108 @@
# Baseline Profile для Rosetta Messenger # Baseline Profile for Rosetta Messenger
# Предкомпилирует критические функции при установке APK # AOT-compiles critical classes and methods at APK install time
# ============ Keyboard & Animation (приоритет #1) ============ # 1. APP STARTUP
# MessageInputBar - основной источник JIT лага HSPLcom/rosetta/messenger/RosettaApplication;->onCreate()V
HSPLcom/rosetta/messenger/ui/chats/ChatDetailScreenKt;->MessageInputBar-c4CPeSU(Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;ZJJJJLjava/util/List;ZLkotlin/jvm/functions/Function0;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/focus/FocusRequester;Lapp/rosette/android/ui/keyboard/KeyboardTransitionCoordinator;Landroidx/compose/runtime/Composer;III)V
# AnimatedKeyboardTransition - fade анимация
HSPLapp/rosette/android/ui/keyboard/AnimatedKeyboardTransitionKt;->AnimatedKeyboardTransition(Lapp/rosette/android/ui/keyboard/KeyboardTransitionCoordinator;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
# KeyboardTransitionCoordinator - управление состоянием
HSPLapp/rosette/android/ui/keyboard/KeyboardTransitionCoordinator;->updateKeyboardHeight(F)V
HSPLapp/rosette/android/ui/keyboard/KeyboardTransitionCoordinator;->requestShowEmoji(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V
HSPLapp/rosette/android/ui/keyboard/KeyboardTransitionCoordinator;->requestShowKeyboard(Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V
# ============ EmojiPicker (приоритет #2) ============
# OptimizedEmojiPicker - основной UI эмодзи
HSPLcom/rosetta/messenger/ui/components/emoji/OptimizedEmojiPickerKt;->OptimizedEmojiPicker(Lkotlin/jvm/functions/Function1;FLandroidx/compose/runtime/Composer;I)V
# EmojiPickerContent - рендеринг эмодзи
HSPLcom/rosetta/messenger/ui/components/emoji/OptimizedEmojiPickerKt;->EmojiPickerContent(Lkotlin/jvm/functions/Function1;FLandroidx/compose/runtime/Composer;I)V
# ============ ChatsListScreen (приоритет #3) ============
# DialogItem - вызывает JIT компиляцию (6101KB)
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->DialogItem(Lcom/rosetta/messenger/ui/chats/DialogUiModel;ZZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V
# Главный экран списка чатов
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->ChatsListScreen(ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;III)V
# ============ ChatDetailScreen (приоритет #4) ============
# Главный экран чата
HSPLcom/rosetta/messenger/ui/chats/ChatDetailScreenKt;->ChatDetailScreen(Lcom/rosetta/messenger/network/SearchUser;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V
# MessageBubble - рендеринг сообщений
HSPLcom/rosetta/messenger/ui/chats/ChatDetailScreenKt;->MessageBubble(Lcom/rosetta/messenger/data/local/entity/Message;ZZZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
# ============ Navigation (приоритет #5) ============
# MainActivity и навигация
HSPLcom/rosetta/messenger/MainActivity;->onCreate(Landroid/os/Bundle;)V HSPLcom/rosetta/messenger/MainActivity;->onCreate(Landroid/os/Bundle;)V
HSPLcom/rosetta/messenger/MainActivityKt;->MainScreen(Lcom/rosetta/messenger/data/DecryptedAccount;ZLkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V
# ============ Common Compose (приоритет #6) ============ # 2. CRYPTO
# Часто используемые Compose компоненты HSPLcom/rosetta/messenger/crypto/CryptoManager;->getPbkdf2Key(Ljava/lang/String;)Ljavax/crypto/spec/SecretKeySpec;
HSPLandroidx/compose/foundation/lazy/LazyListState;->scrollToItem(IILkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLcom/rosetta/messenger/crypto/CryptoManager;->decryptWithPassword(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
HSPLandroidx/compose/ui/text/input/TextFieldValue;-><init>(Ljava/lang/String;JLandroidx/compose/ui/text/TextRange;)V HSPLcom/rosetta/messenger/crypto/CryptoManager;->decryptWithPasswordInternal(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
HSPLandroidx/compose/animation/core/Animatable;->animateTo(Ljava/lang/Object;Landroidx/compose/animation/core/AnimationSpec;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLcom/rosetta/messenger/crypto/CryptoManager;->isOldFormat(Ljava/lang/String;)Z
HSPLandroidx/compose/animation/AnimatedContentKt;->AnimatedContent(Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Alignment;Ljava/lang/String;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V HSPLcom/rosetta/messenger/crypto/CryptoManager;->decompress([B)[B
HSPLcom/rosetta/messenger/crypto/CryptoManager;->encryptWithPassword(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
HSPLcom/rosetta/messenger/crypto/CryptoManager;->compress([B)[B
HSPLcom/rosetta/messenger/crypto/CryptoManager;->clearCaches()V
HSPLcom/rosetta/messenger/crypto/MessageCrypto;->**(**)**
HSPLjavax/crypto/Cipher;->getInstance(Ljava/lang/String;)**
HSPLjavax/crypto/Cipher;->doFinal([B)**
HSPLjavax/crypto/SecretKeyFactory;->getInstance(Ljava/lang/String;)**
HSPLorg/bouncycastle/jce/**;->**(**)**
HSPLcom/google/crypto/tink/subtle/XChaCha20Poly1305;->**(**)**
# 3. DATABASE
HSPLcom/rosetta/messenger/database/RosettaDatabase;->**(**)**
HSPLcom/rosetta/messenger/database/RosettaDatabase_Impl;->**(**)**
HSPLcom/rosetta/messenger/database/DialogDao_Impl;->**(**)**
HSPLcom/rosetta/messenger/database/MessageDao_Impl;->**(**)**
HSPLandroidx/room/RoomDatabase;->**(**)**
HSPLandroidx/room/Room;->**(**)**
# 4. VIEWMODELS
HSPLcom/rosetta/messenger/ui/chats/ChatsListViewModel;->**(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatViewModel;->**(**)**
# 5. COMPOSE UI
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->ChatsListScreen(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->DialogItem(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->ChatsListSkeleton(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->SkeletonDialogItem(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatsListScreenKt;->SwipeableDialogItem(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatDetailScreenKt;->ChatDetailScreen(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatDetailScreenKt;->MessageBubble(**)**
HSPLcom/rosetta/messenger/ui/chats/ChatDetailScreenKt;->MessageInputBar-c4CPeSU(**)**
HSPLcom/rosetta/messenger/ui/chats/components/ChatDetailComponentsKt;->**(**)**
HSPLcom/rosetta/messenger/ui/chats/input/ChatDetailInputKt;->**(**)**
# 6. KEYBOARD
HSPLapp/rosette/android/ui/keyboard/AnimatedKeyboardTransitionKt;->AnimatedKeyboardTransition(**)**
HSPLapp/rosette/android/ui/keyboard/KeyboardTransitionCoordinator;->**(**)**
# 7. EMOJI
HSPLcom/rosetta/messenger/ui/components/emoji/OptimizedEmojiPickerKt;->**(**)**
HSPLcom/rosetta/messenger/ui/components/OptimizedEmojiCache;->**(**)**
HSPLcom/rosetta/messenger/ui/components/AppleEmojiEditTextKt;->**(**)**
# 8. AVATAR
HSPLcom/rosetta/messenger/ui/components/AvatarImageKt;->**(**)**
HSPLcom/rosetta/messenger/utils/AvatarBitmapCache;->**(**)**
HSPLcom/rosetta/messenger/utils/AvatarFileManager;->**(**)**
HSPLcom/rosetta/messenger/repository/AvatarRepository;->**(**)**
# 9. NAVIGATION
HSPLcom/rosetta/messenger/MainActivityKt;->MainScreen(**)**
HSPLcom/rosetta/messenger/ui/components/SwipeBackContainerKt;->SwipeBackContainer(**)**
# 10. NETWORK
HSPLcom/rosetta/messenger/data/MessageRepository;->**(**)**
HSPLcom/rosetta/messenger/network/ProtocolManager;->**(**)**
HSPLcom/rosetta/messenger/network/WebSocketManager;->**(**)**
# 11. COMPOSE FRAMEWORK
HSPLandroidx/compose/foundation/lazy/**;->**(**)**
HSPLandroidx/compose/runtime/**;->**(**)**
HSPLandroidx/compose/ui/**;->**(**)**
HSPLandroidx/compose/animation/**;->**(**)**
HSPLandroidx/compose/material3/**;->**(**)**
HSPLandroidx/compose/foundation/**;->**(**)**
# 12. COROUTINES
HSPLkotlinx/coroutines/**;->**(**)**
# 13. PROFILEINSTALLER
HSPLandroidx/profileinstaller/**;->**(**)**
# 14. LOTTIE
HSPLcom/airbnb/lottie/**;->**(**)**
# 15. COIL
HSPLcoil/**;->**(**)**
# CLASS PRELOADING
Lcom/rosetta/messenger/MainActivity;
Lcom/rosetta/messenger/RosettaApplication;
Lcom/rosetta/messenger/crypto/CryptoManager;
Lcom/rosetta/messenger/crypto/MessageCrypto;
Lcom/rosetta/messenger/database/RosettaDatabase;
Lcom/rosetta/messenger/database/RosettaDatabase_Impl;
Lcom/rosetta/messenger/database/DialogDao_Impl;
Lcom/rosetta/messenger/database/MessageDao_Impl;
Lcom/rosetta/messenger/ui/chats/ChatsListViewModel;
Lcom/rosetta/messenger/ui/chats/ChatViewModel;
Lcom/rosetta/messenger/data/MessageRepository;
Lcom/rosetta/messenger/network/ProtocolManager;
Lcom/rosetta/messenger/network/WebSocketManager;
Lcom/rosetta/messenger/ui/components/OptimizedEmojiCache;
Lcom/rosetta/messenger/utils/AvatarBitmapCache;
Lcom/rosetta/messenger/repository/AvatarRepository;

View File

@@ -1,94 +1,95 @@
package com.rosetta.messenger.crypto package com.rosetta.messenger.crypto
import android.util.Base64
import com.google.crypto.tink.subtle.XChaCha20Poly1305
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.*
import java.util.concurrent.ConcurrentHashMap
import java.util.zip.Deflater
import java.util.zip.Inflater
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import org.bitcoinj.crypto.MnemonicCode import org.bitcoinj.crypto.MnemonicCode
import org.bitcoinj.crypto.MnemonicException import org.bitcoinj.crypto.MnemonicException
import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.spec.ECPrivateKeySpec import org.bouncycastle.jce.spec.ECPrivateKeySpec
import org.bouncycastle.jce.spec.ECPublicKeySpec import org.bouncycastle.jce.spec.ECPublicKeySpec
import java.math.BigInteger
import java.security.*
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import android.util.Base64
import java.security.spec.PKCS8EncodedKeySpec
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.Inflater
import java.util.concurrent.ConcurrentHashMap
import com.google.crypto.tink.subtle.XChaCha20Poly1305
/** /**
* Cryptography module for Rosetta Messenger * Cryptography module for Rosetta Messenger Implements BIP39 seed phrase generation and secp256k1
* Implements BIP39 seed phrase generation and secp256k1 key derivation * key derivation
*/ */
object CryptoManager { object CryptoManager {
private const val PBKDF2_ITERATIONS = 1000 private const val PBKDF2_ITERATIONS = 1000
private const val KEY_SIZE = 256 private const val KEY_SIZE = 256
private const val SALT = "rosetta" private const val SALT = "rosetta"
// 🚀 ОПТИМИЗАЦИЯ: Кэш для генерации ключей (seedPhrase -> KeyPair) // 🚀 ОПТИМИЗАЦИЯ: Кэш для генерации ключей (seedPhrase -> KeyPair)
private val keyPairCache = mutableMapOf<String, KeyPairData>() private val keyPairCache = mutableMapOf<String, KeyPairData>()
private val privateKeyHashCache = mutableMapOf<String, String>() private val privateKeyHashCache = mutableMapOf<String, String>()
// 🚀 КРИТИЧЕСКАЯ ОПТИМИЗАЦИЯ: Кэш для PBKDF2 производных ключей // 🚀 КРИТИЧЕСКАЯ ОПТИМИЗАЦИЯ: Кэш для PBKDF2 производных ключей
// PBKDF2 с 1000 итерациями - очень тяжелая операция! // PBKDF2 с 1000 итерациями - очень тяжелая операция!
// Кэшируем производный ключ для каждого пароля чтобы не вычислять его каждый раз // Кэшируем производный ключ для каждого пароля чтобы не вычислять его каждый раз
private val pbkdf2KeyCache = mutableMapOf<String, SecretKeySpec>() private val pbkdf2KeyCache = mutableMapOf<String, SecretKeySpec>()
// 🚀 ОПТИМИЗАЦИЯ: Lock-free кэш для расшифрованных сообщений // 🚀 ОПТИМИЗАЦИЯ: Lock-free кэш для расшифрованных сообщений
// ConcurrentHashMap вместо synchronized LinkedHashMap — убирает контention при параллельной расшифровке // ConcurrentHashMap вместо synchronized LinkedHashMap — убирает контention при параллельной
// расшифровке
private const val DECRYPTION_CACHE_SIZE = 2000 private const val DECRYPTION_CACHE_SIZE = 2000
private val decryptionCache = ConcurrentHashMap<String, String>(DECRYPTION_CACHE_SIZE, 0.75f, 4) private val decryptionCache = ConcurrentHashMap<String, String>(DECRYPTION_CACHE_SIZE, 0.75f, 4)
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) {
Security.addProvider(BouncyCastleProvider()) Security.addProvider(BouncyCastleProvider())
} }
} }
/** /**
* 🚀 Получить или вычислить PBKDF2 ключ для пароля (кэшируется) * 🚀 Получить или вычислить PBKDF2 ключ для пароля (кэшируется) Public для pre-warm при логине
* (чтобы кэш был горячий к моменту дешифровки)
*/ */
private fun getPbkdf2Key(password: String): SecretKeySpec { fun getPbkdf2Key(password: String): SecretKeySpec {
return pbkdf2KeyCache.getOrPut(password) { return pbkdf2KeyCache.getOrPut(password) {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(Charsets.UTF_8), PBKDF2_ITERATIONS, KEY_SIZE) val spec =
PBEKeySpec(
password.toCharArray(),
SALT.toByteArray(Charsets.UTF_8),
PBKDF2_ITERATIONS,
KEY_SIZE
)
val secretKey = factory.generateSecret(spec) val secretKey = factory.generateSecret(spec)
SecretKeySpec(secretKey.encoded, "AES") SecretKeySpec(secretKey.encoded, "AES")
} }
} }
/** /** 🧹 Очистить кэши при logout */
* 🧹 Очистить кэши при logout
*/
fun clearCaches() { fun clearCaches() {
pbkdf2KeyCache.clear() pbkdf2KeyCache.clear()
decryptionCache.clear() decryptionCache.clear()
keyPairCache.clear() keyPairCache.clear()
privateKeyHashCache.clear() privateKeyHashCache.clear()
} }
/** /** Generate a new 12-word BIP39 seed phrase */
* Generate a new 12-word BIP39 seed phrase
*/
fun generateSeedPhrase(): List<String> { fun generateSeedPhrase(): List<String> {
val secureRandom = SecureRandom() val secureRandom = SecureRandom()
val entropy = ByteArray(16) // 128 bits = 12 words val entropy = ByteArray(16) // 128 bits = 12 words
secureRandom.nextBytes(entropy) secureRandom.nextBytes(entropy)
val mnemonicCode = MnemonicCode.INSTANCE val mnemonicCode = MnemonicCode.INSTANCE
return mnemonicCode.toMnemonic(entropy) return mnemonicCode.toMnemonic(entropy)
} }
/** /** Validate a seed phrase */
* Validate a seed phrase
*/
fun validateSeedPhrase(words: List<String>): Boolean { fun validateSeedPhrase(words: List<String>): Boolean {
return try { return try {
val mnemonicCode = MnemonicCode.INSTANCE val mnemonicCode = MnemonicCode.INSTANCE
@@ -98,15 +99,15 @@ object CryptoManager {
false false
} }
} }
/** /**
* Convert seed phrase to private key (32 bytes hex string) * Convert seed phrase to private key (32 bytes hex string)
* *
* Алгоритм (совместим с Desktop desktop-rosetta): * Алгоритм (совместим с Desktop desktop-rosetta):
* 1. BIP39 mnemonicToSeed(phrase, "") → 64 байта (PBKDF2-SHA512, 2048 итераций) * 1. BIP39 mnemonicToSeed(phrase, "") → 64 байта (PBKDF2-SHA512, 2048 итераций)
* 2. Конвертация seed в hex-строку (128 символов) * 2. Конвертация seed в hex-строку (128 символов)
* 3. SHA256(hexSeed) → 32 байта privateKey * 3. SHA256(hexSeed) → 32 байта privateKey
* *
* Desktop эквивалент (SetPassword.tsx + crypto.ts): * Desktop эквивалент (SetPassword.tsx + crypto.ts):
* ```js * ```js
* let seed = await mnemonicToSeed(phrase); // BIP39 → 64 bytes * let seed = await mnemonicToSeed(phrase); // BIP39 → 64 bytes
@@ -118,96 +119,94 @@ object CryptoManager {
// Step 1: BIP39 mnemonicToSeed — PBKDF2-SHA512 with 2048 iterations // Step 1: BIP39 mnemonicToSeed — PBKDF2-SHA512 with 2048 iterations
// passphrase = "" (empty), salt = "mnemonic" + passphrase // passphrase = "" (empty), salt = "mnemonic" + passphrase
val bip39Seed = MnemonicCode.toSeed(seedPhrase, "") val bip39Seed = MnemonicCode.toSeed(seedPhrase, "")
// Step 2: Convert to hex string (128 chars for 64 bytes) // Step 2: Convert to hex string (128 chars for 64 bytes)
val hexSeed = bip39Seed.joinToString("") { "%02x".format(it) } val hexSeed = bip39Seed.joinToString("") { "%02x".format(it) }
// Step 3: SHA256(hexSeed) — matches Desktop's sha256.create().update(hex).digest() // Step 3: SHA256(hexSeed) — matches Desktop's sha256.create().update(hex).digest()
val digest = MessageDigest.getInstance("SHA-256") val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(hexSeed.toByteArray(Charsets.UTF_8)) val hash = digest.digest(hexSeed.toByteArray(Charsets.UTF_8))
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
* *
* Алгоритм (совместим с Desktop desktop-rosetta): * Алгоритм (совместим с Desktop desktop-rosetta):
* - privateKey = SHA256(hex(BIP39_seed(mnemonic))) - 32 байта * - privateKey = SHA256(hex(BIP39_seed(mnemonic))) - 32 байта
* - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта * - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта
* *
* Desktop эквивалент (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(" ")
// Проверяем кэш // Проверяем кэш
keyPairCache[cacheKey]?.let { return it } keyPairCache[cacheKey]?.let {
return it
}
// Генерируем приватный ключ через SHA256 // Генерируем приватный ключ через SHA256
val privateKeyHex = seedPhraseToPrivateKey(seedPhrase) val privateKeyHex = seedPhraseToPrivateKey(seedPhrase)
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1") val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
// Преобразуем hex в bytes (32 байта) // Преобразуем hex в bytes (32 байта)
val privateKeyBytes = privateKeyHex.chunked(2) val privateKeyBytes = privateKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
.map { it.toInt(16).toByte() }
.toByteArray()
val privateKeyBigInt = BigInteger(1, privateKeyBytes) val privateKeyBigInt = BigInteger(1, privateKeyBytes)
// Генерируем публичный ключ из приватного // Генерируем публичный ключ из приватного
val publicKeyPoint = ecSpec.g.multiply(privateKeyBigInt) val publicKeyPoint = ecSpec.g.multiply(privateKeyBigInt)
// ⚡ ВАЖНО: Используем СЖАТЫЙ формат (compressed=true) - 33 байта вместо 65 // ⚡ ВАЖНО: Используем СЖАТЫЙ формат (compressed=true) - 33 байта вместо 65
// Это совместимо с crypto_new где используется: secp256k1.getPublicKey(..., true) // Это совместимо с crypto_new где используется: secp256k1.getPublicKey(..., true)
val publicKeyHex = publicKeyPoint.getEncoded(true) val publicKeyHex = publicKeyPoint.getEncoded(true).joinToString("") { "%02x".format(it) }
.joinToString("") { "%02x".format(it) }
val keyPair = KeyPairData(privateKey = privateKeyHex, publicKey = publicKeyHex)
val keyPair = KeyPairData(
privateKey = privateKeyHex,
publicKey = publicKeyHex
)
// Сохраняем в кэш (ограничиваем размер до 5 записей) // Сохраняем в кэш (ограничиваем размер до 5 записей)
keyPairCache[cacheKey] = keyPair keyPairCache[cacheKey] = keyPair
if (keyPairCache.size > 5) { if (keyPairCache.size > 5) {
keyPairCache.remove(keyPairCache.keys.first()) keyPairCache.remove(keyPairCache.keys.first())
} }
return keyPair 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 } 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)
val hashHex = hash.joinToString("") { "%02x".format(it) } val hashHex = hash.joinToString("") { "%02x".format(it) }
// Сохраняем в кэш // Сохраняем в кэш
privateKeyHashCache[privateKey] = hashHex privateKeyHashCache[privateKey] = hashHex
if (privateKeyHashCache.size > 10) { if (privateKeyHashCache.size > 10) {
privateKeyHashCache.remove(privateKeyHashCache.keys.first()) privateKeyHashCache.remove(privateKeyHashCache.keys.first())
} }
return hashHex return hashHex
} }
/** /**
* Encrypt data with password using PBKDF2 + AES * Encrypt data with password using PBKDF2 + AES
* *
* ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts): * ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts):
* - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию * - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию
* - Salt: "rosetta" * - Salt: "rosetta"
@@ -222,38 +221,44 @@ object CryptoManager {
fun encryptWithPassword(data: String, password: String): String { fun encryptWithPassword(data: String, password: String): String {
// Compress data (zlib deflate - совместимо с pako.deflate в JS) // Compress data (zlib deflate - совместимо с pako.deflate в JS)
val compressed = compress(data.toByteArray(Charsets.UTF_8)) val compressed = compress(data.toByteArray(Charsets.UTF_8))
val CHUNK_SIZE = 10 * 1024 * 1024 // 10MB val CHUNK_SIZE = 10 * 1024 * 1024 // 10MB
// Check if we need chunking // Check if we need chunking
if (compressed.size > CHUNK_SIZE) { if (compressed.size > CHUNK_SIZE) {
// Chunk the compressed data // Chunk the compressed data
val chunks = compressed.toList().chunked(CHUNK_SIZE).map { it.toByteArray() } val chunks = compressed.toList().chunked(CHUNK_SIZE).map { it.toByteArray() }
val encryptedChunks = mutableListOf<String>() val encryptedChunks = mutableListOf<String>()
for (chunk in chunks) { for (chunk in chunks) {
// Derive key using PBKDF2-HMAC-SHA1 // Derive key using PBKDF2-HMAC-SHA1
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(Charsets.UTF_8), PBKDF2_ITERATIONS, KEY_SIZE) val spec =
PBEKeySpec(
password.toCharArray(),
SALT.toByteArray(Charsets.UTF_8),
PBKDF2_ITERATIONS,
KEY_SIZE
)
val secretKey = factory.generateSecret(spec) val secretKey = factory.generateSecret(spec)
val key = SecretKeySpec(secretKey.encoded, "AES") val key = SecretKeySpec(secretKey.encoded, "AES")
// Generate random IV // Generate random IV
val iv = ByteArray(16) val iv = ByteArray(16)
SecureRandom().nextBytes(iv) SecureRandom().nextBytes(iv)
val ivSpec = IvParameterSpec(iv) val ivSpec = IvParameterSpec(iv)
// Encrypt with AES // Encrypt with AES
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec) cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
val encrypted = cipher.doFinal(chunk) val encrypted = cipher.doFinal(chunk)
// Store as ivBase64:ctBase64 // Store as ivBase64:ctBase64
val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP) val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
val ctBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP) val ctBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
encryptedChunks.add("$ivBase64:$ctBase64") encryptedChunks.add("$ivBase64:$ctBase64")
} }
// Return chunked format: "CHNK:" + chunks joined by "::" // Return chunked format: "CHNK:" + chunks joined by "::"
return "CHNK:" + encryptedChunks.joinToString("::") return "CHNK:" + encryptedChunks.joinToString("::")
} else { } else {
@@ -261,52 +266,60 @@ object CryptoManager {
// Derive key using PBKDF2-HMAC-SHA1 (⚠️ SHA1, не SHA256!) // Derive key using PBKDF2-HMAC-SHA1 (⚠️ SHA1, не SHA256!)
// crypto-js по умолчанию использует SHA1 для PBKDF2 // crypto-js по умолчанию использует SHA1 для PBKDF2
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val spec = PBEKeySpec(password.toCharArray(), SALT.toByteArray(Charsets.UTF_8), PBKDF2_ITERATIONS, KEY_SIZE) val spec =
PBEKeySpec(
password.toCharArray(),
SALT.toByteArray(Charsets.UTF_8),
PBKDF2_ITERATIONS,
KEY_SIZE
)
val secretKey = factory.generateSecret(spec) val secretKey = factory.generateSecret(spec)
val key = SecretKeySpec(secretKey.encoded, "AES") val key = SecretKeySpec(secretKey.encoded, "AES")
// Generate random IV // Generate random IV
val iv = ByteArray(16) val iv = ByteArray(16)
SecureRandom().nextBytes(iv) SecureRandom().nextBytes(iv)
val ivSpec = IvParameterSpec(iv) val ivSpec = IvParameterSpec(iv)
// Encrypt with AES // Encrypt with AES
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec) cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
val encrypted = cipher.doFinal(compressed) val encrypted = cipher.doFinal(compressed)
// Return iv:ciphertext in Base64 // Return iv:ciphertext in Base64
val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP) val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
val ctBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP) val ctBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
return "$ivBase64:$ctBase64" return "$ivBase64:$ctBase64"
} }
} }
/** /**
* Decrypt data with password * Decrypt data with password
* *
* ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts): * ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts):
* - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию * - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию
* - Salt: "rosetta" * - Salt: "rosetta"
* - Iterations: 1000 * - Iterations: 1000
* - Key size: 256 bit * - Key size: 256 bit
* - AES-256-CBC с PKCS5/PKCS7 padding * - AES-256-CBC с PKCS5/PKCS7 padding
* - Decompression: zlib inflate (pako.inflate в JS) * - Decompression: zlib inflate (pako.inflate в JS)
* - Supports old format (base64-encoded hex "iv:ciphertext") * - Supports old format (base64-encoded hex "iv:ciphertext")
* - Supports new format (base64 "iv:ciphertext") * - Supports new format (base64 "iv:ciphertext")
* - Supports chunked format ("CHNK:" + chunks joined by "::") * - Supports chunked format ("CHNK:" + chunks joined by "::")
* *
* 🚀 ОПТИМИЗАЦИЯ: Кэширование PBKDF2 ключа и расшифрованных сообщений * 🚀 ОПТИМИЗАЦИЯ: Кэширование PBKDF2 ключа и расшифрованных сообщений
*/ */
fun decryptWithPassword(encryptedData: String, password: String): String? { fun decryptWithPassword(encryptedData: String, password: String): String? {
// 🚀 ОПТИМИЗАЦИЯ: Lock-free проверка кэша (ConcurrentHashMap) // 🚀 ОПТИМИЗАЦИЯ: Lock-free проверка кэша (ConcurrentHashMap)
val cacheKey = "$password:$encryptedData" val cacheKey = "$password:$encryptedData"
decryptionCache[cacheKey]?.let { return it } decryptionCache[cacheKey]?.let {
return it
}
return try { return try {
val result = decryptWithPasswordInternal(encryptedData, password) val result = decryptWithPasswordInternal(encryptedData, password)
// 🚀 Сохраняем в кэш (lock-free) // 🚀 Сохраняем в кэш (lock-free)
if (result != null) { if (result != null) {
// Ограничиваем размер кэша // Ограничиваем размер кэша
@@ -317,106 +330,103 @@ object CryptoManager {
} }
decryptionCache[cacheKey] = result decryptionCache[cacheKey] = result
} }
result result
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
/** /** 🔐 Внутренняя функция расшифровки (без кэширования результата) */
* 🔐 Внутренняя функция расшифровки (без кэширования результата)
*/
private fun decryptWithPasswordInternal(encryptedData: String, password: String): String? { private fun decryptWithPasswordInternal(encryptedData: String, password: String): String? {
return try { return try {
// 🚀 Получаем кэшированный PBKDF2 ключ // 🚀 Получаем кэшированный PBKDF2 ключ
val key = getPbkdf2Key(password) val key = getPbkdf2Key(password)
// Check for old format: base64-encoded string containing hex // Check for old format: base64-encoded string containing hex
if (isOldFormat(encryptedData)) { if (isOldFormat(encryptedData)) {
val decoded = String(Base64.decode(encryptedData, Base64.NO_WRAP), Charsets.UTF_8) val decoded = String(Base64.decode(encryptedData, Base64.NO_WRAP), Charsets.UTF_8)
val parts = decoded.split(":") val parts = decoded.split(":")
if (parts.size != 2) return null if (parts.size != 2) return null
val iv = parts[0].chunked(2).map { it.toInt(16).toByte() }.toByteArray() val iv = parts[0].chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val ciphertext = parts[1].chunked(2).map { it.toInt(16).toByte() }.toByteArray() val ciphertext = parts[1].chunked(2).map { it.toInt(16).toByte() }.toByteArray()
// Decrypt with AES-256-CBC (используем кэшированный ключ!) // Decrypt with AES-256-CBC (используем кэшированный ключ!)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
val decrypted = cipher.doFinal(ciphertext) val decrypted = cipher.doFinal(ciphertext)
return String(decrypted, Charsets.UTF_8) return String(decrypted, Charsets.UTF_8)
} }
// Check for chunked format // Check for chunked format
if (encryptedData.startsWith("CHNK:")) { if (encryptedData.startsWith("CHNK:")) {
val chunkStrings = encryptedData.substring(5).split("::") val chunkStrings = encryptedData.substring(5).split("::")
val decompressedParts = mutableListOf<ByteArray>() val decompressedParts = mutableListOf<ByteArray>()
for (chunkString in chunkStrings) { for (chunkString in chunkStrings) {
val parts = chunkString.split(":") val parts = chunkString.split(":")
if (parts.size != 2) return null if (parts.size != 2) return null
val iv = Base64.decode(parts[0], Base64.NO_WRAP) val iv = Base64.decode(parts[0], Base64.NO_WRAP)
val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP) val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP)
// Decrypt with AES-256-CBC (используем кэшированный ключ!) // Decrypt with AES-256-CBC (используем кэшированный ключ!)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
val decrypted = cipher.doFinal(ciphertext) val decrypted = cipher.doFinal(ciphertext)
decompressedParts.add(decrypted) decompressedParts.add(decrypted)
} }
// Concatenate all decrypted chunks // Concatenate all decrypted chunks
val allBytes = decompressedParts.fold(ByteArray(0)) { acc, arr -> acc + arr } val allBytes = decompressedParts.fold(ByteArray(0)) { acc, arr -> acc + arr }
// Decompress the concatenated data // Decompress the concatenated data
return String(decompress(allBytes), Charsets.UTF_8) return String(decompress(allBytes), Charsets.UTF_8)
} }
// New format: base64 "iv:ciphertext" // New format: base64 "iv:ciphertext"
val parts = encryptedData.split(":") val parts = encryptedData.split(":")
if (parts.size != 2) return null if (parts.size != 2) return null
val iv = Base64.decode(parts[0], Base64.NO_WRAP) val iv = Base64.decode(parts[0], Base64.NO_WRAP)
val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP) val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP)
// Decrypt with AES-256-CBC (используем кэшированный ключ!) // Decrypt with AES-256-CBC (используем кэшированный ключ!)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
val decrypted = cipher.doFinal(ciphertext) val decrypted = cipher.doFinal(ciphertext)
// Decompress (zlib inflate - совместимо с pako.inflate в JS) // Decompress (zlib inflate - совместимо с pako.inflate в JS)
String(decompress(decrypted), Charsets.UTF_8) String(decompress(decrypted), Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
/** /** Check if data is in old format (base64-encoded hex with ":") */
* Check if data is in old format (base64-encoded hex with ":")
*/
private fun isOldFormat(data: String): Boolean { private fun isOldFormat(data: String): Boolean {
// 🚀 Fast path: new format always contains ':' at plaintext level (iv:ct) // 🚀 Fast path: new format always contains ':' at plaintext level (iv:ct)
// Old format is a single base64 blob without ':' in the encoded string // Old format is a single base64 blob without ':' in the encoded string
if (data.contains(':')) return false if (data.contains(':')) return false
if (data.startsWith("CHNK:")) return false if (data.startsWith("CHNK:")) return false
return try { return try {
val decoded = String(Base64.decode(data, Base64.NO_WRAP), Charsets.UTF_8) val decoded = String(Base64.decode(data, Base64.NO_WRAP), Charsets.UTF_8)
decoded.contains(":") && decoded.split(":").all { part -> decoded.contains(":") &&
part.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' } decoded.split(":").all { part ->
} part.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
}
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
} }
/** /**
* RAW Deflate сжатие (без zlib header) * RAW Deflate сжатие (без zlib header)
* *
* ⚠️ ВАЖНО: nowrap=true для совместимости с pako.deflate() в JS! * ⚠️ ВАЖНО: nowrap=true для совместимости с pako.deflate() в JS!
* - pako.deflate() создаёт RAW deflate поток (без 2-byte zlib header) * - pako.deflate() создаёт RAW deflate поток (без 2-byte zlib header)
* - Java Deflater() по умолчанию создаёт zlib поток (с header 78 9C) * - Java Deflater() по умолчанию создаёт zlib поток (с header 78 9C)
@@ -427,7 +437,7 @@ object CryptoManager {
val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, true) val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, true)
deflater.setInput(data) deflater.setInput(data)
deflater.finish() deflater.finish()
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
val buffer = ByteArray(1024) val buffer = ByteArray(1024)
while (!deflater.finished()) { while (!deflater.finished()) {
@@ -438,10 +448,10 @@ object CryptoManager {
outputStream.close() outputStream.close()
return outputStream.toByteArray() return outputStream.toByteArray()
} }
/** /**
* RAW Inflate декомпрессия (без zlib header) * RAW Inflate декомпрессия (без zlib header)
* *
* ⚠️ ВАЖНО: nowrap=true для совместимости с pako.inflate() в JS! * ⚠️ ВАЖНО: nowrap=true для совместимости с pako.inflate() в JS!
* - pako.inflate() ожидает RAW deflate поток * - pako.inflate() ожидает RAW deflate поток
* - Java Inflater() по умолчанию ожидает zlib поток (с header) * - Java Inflater() по умолчанию ожидает zlib поток (с header)
@@ -451,7 +461,7 @@ object CryptoManager {
// nowrap=true = RAW inflate (совместимо с pako.inflate) // nowrap=true = RAW inflate (совместимо с pako.inflate)
val inflater = Inflater(true) val inflater = Inflater(true)
inflater.setInput(data) inflater.setInput(data)
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
val buffer = ByteArray(1024) val buffer = ByteArray(1024)
while (!inflater.finished()) { while (!inflater.finished()) {
@@ -462,10 +472,10 @@ object CryptoManager {
outputStream.close() outputStream.close()
return outputStream.toByteArray() return outputStream.toByteArray()
} }
/** /**
* Encrypt data using ECDH + AES * Encrypt data using ECDH + AES
* *
* Algorithm: * Algorithm:
* 1. Generate ephemeral key pair * 1. Generate ephemeral key pair
* 2. Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey) * 2. Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey)
@@ -475,61 +485,67 @@ object CryptoManager {
*/ */
fun encrypt(data: String, publicKeyHex: String): String { fun encrypt(data: String, publicKeyHex: String): String {
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1") val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
// Generate ephemeral private key (random 32 bytes) // Generate ephemeral private key (random 32 bytes)
val ephemeralPrivateKeyBytes = ByteArray(32) val ephemeralPrivateKeyBytes = ByteArray(32)
SecureRandom().nextBytes(ephemeralPrivateKeyBytes) SecureRandom().nextBytes(ephemeralPrivateKeyBytes)
val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes) val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes)
// Generate ephemeral public key from private key // Generate ephemeral public key from private key
val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt) val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt)
// Parse recipient's public key // Parse recipient's public key
val recipientPublicKeyBytes = publicKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() val recipientPublicKeyBytes =
publicKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val recipientPublicKeyPoint = ecSpec.curve.decodePoint(recipientPublicKeyBytes) val recipientPublicKeyPoint = ecSpec.curve.decodePoint(recipientPublicKeyBytes)
// Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey) // Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey)
val sharedPoint = recipientPublicKeyPoint.multiply(ephemeralPrivateKeyBigInt).normalize() val sharedPoint = recipientPublicKeyPoint.multiply(ephemeralPrivateKeyBigInt).normalize()
// Use x-coordinate of shared point as AES key (32 bytes) // Use x-coordinate of shared point as AES key (32 bytes)
val sharedKeyBytes = sharedPoint.affineXCoord.encoded val sharedKeyBytes = sharedPoint.affineXCoord.encoded
val sharedKey = if (sharedKeyBytes.size >= 32) { val sharedKey =
sharedKeyBytes.copyOfRange(sharedKeyBytes.size - 32, sharedKeyBytes.size) if (sharedKeyBytes.size >= 32) {
} else { sharedKeyBytes.copyOfRange(sharedKeyBytes.size - 32, sharedKeyBytes.size)
// Pad with leading zeros if needed } else {
ByteArray(32 - sharedKeyBytes.size) + sharedKeyBytes // Pad with leading zeros if needed
} ByteArray(32 - sharedKeyBytes.size) + sharedKeyBytes
}
val key = SecretKeySpec(sharedKey, "AES") val key = SecretKeySpec(sharedKey, "AES")
// Generate random IV // Generate random IV
val iv = ByteArray(16) val iv = ByteArray(16)
SecureRandom().nextBytes(iv) SecureRandom().nextBytes(iv)
val ivSpec = IvParameterSpec(iv) val ivSpec = IvParameterSpec(iv)
// Encrypt with AES // Encrypt with AES
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec) cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8)) val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
// Normalize ephemeral private key to 32 bytes // Normalize ephemeral private key to 32 bytes
val normalizedPrivateKey = if (ephemeralPrivateKeyBytes.size > 32) { val normalizedPrivateKey =
ephemeralPrivateKeyBytes.copyOfRange(ephemeralPrivateKeyBytes.size - 32, ephemeralPrivateKeyBytes.size) if (ephemeralPrivateKeyBytes.size > 32) {
} else { ephemeralPrivateKeyBytes.copyOfRange(
ephemeralPrivateKeyBytes ephemeralPrivateKeyBytes.size - 32,
} ephemeralPrivateKeyBytes.size
)
} else {
ephemeralPrivateKeyBytes
}
// Return base64(iv:ciphertext:ephemeralPrivateKey) // Return base64(iv:ciphertext:ephemeralPrivateKey)
val ivHex = iv.joinToString("") { "%02x".format(it) } val ivHex = iv.joinToString("") { "%02x".format(it) }
val ctHex = encrypted.joinToString("") { "%02x".format(it) } val ctHex = encrypted.joinToString("") { "%02x".format(it) }
val ephemeralPrivateKeyHex = normalizedPrivateKey.joinToString("") { "%02x".format(it) } val ephemeralPrivateKeyHex = normalizedPrivateKey.joinToString("") { "%02x".format(it) }
val combined = "$ivHex:$ctHex:$ephemeralPrivateKeyHex" val combined = "$ivHex:$ctHex:$ephemeralPrivateKeyHex"
return Base64.encodeToString(combined.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) return Base64.encodeToString(combined.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
} }
/** /**
* Decrypt data using ECDH + AES * Decrypt data using ECDH + AES
* *
* Algorithm: * Algorithm:
* 1. Parse iv, ciphertext, and ephemeralPrivateKey from base64 * 1. Parse iv, ciphertext, and ephemeralPrivateKey from base64
* 2. Compute ephemeral public key from ephemeral private key * 2. Compute ephemeral public key from ephemeral private key
@@ -543,55 +559,57 @@ object CryptoManager {
val decoded = String(Base64.decode(encryptedData, Base64.NO_WRAP), Charsets.UTF_8) val decoded = String(Base64.decode(encryptedData, Base64.NO_WRAP), Charsets.UTF_8)
val parts = decoded.split(":") val parts = decoded.split(":")
if (parts.size != 3) return null if (parts.size != 3) return null
val iv = parts[0].chunked(2).map { it.toInt(16).toByte() }.toByteArray() val iv = parts[0].chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val ciphertext = parts[1].chunked(2).map { it.toInt(16).toByte() }.toByteArray() val ciphertext = parts[1].chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val ephemeralPrivateKeyBytes = parts[2].chunked(2).map { it.toInt(16).toByte() }.toByteArray() val ephemeralPrivateKeyBytes =
parts[2].chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1") val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
// Compute ephemeral public key from ephemeral private key // Compute ephemeral public key from ephemeral private key
val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes) val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes)
val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt) val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt)
val ephemeralPublicKeySpec = ECPublicKeySpec(ephemeralPublicKeyPoint, ecSpec) val ephemeralPublicKeySpec = ECPublicKeySpec(ephemeralPublicKeyPoint, ecSpec)
val keyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME) val keyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME)
val ephemeralPublicKey = keyFactory.generatePublic(ephemeralPublicKeySpec) val ephemeralPublicKey = keyFactory.generatePublic(ephemeralPublicKeySpec)
// Parse private key // Parse private key
val privateKeyBytes = privateKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() val privateKeyBytes =
privateKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val privateKeyBigInt = BigInteger(1, privateKeyBytes) val privateKeyBigInt = BigInteger(1, privateKeyBytes)
val privateKeySpec = ECPrivateKeySpec(privateKeyBigInt, ecSpec) val privateKeySpec = ECPrivateKeySpec(privateKeyBigInt, ecSpec)
val privateKey = keyFactory.generatePrivate(privateKeySpec) val privateKey = keyFactory.generatePrivate(privateKeySpec)
// Compute shared secret using ECDH // Compute shared secret using ECDH
val keyAgreement = javax.crypto.KeyAgreement.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME) val keyAgreement =
javax.crypto.KeyAgreement.getInstance(
"ECDH",
BouncyCastleProvider.PROVIDER_NAME
)
keyAgreement.init(privateKey) keyAgreement.init(privateKey)
keyAgreement.doPhase(ephemeralPublicKey, true) keyAgreement.doPhase(ephemeralPublicKey, true)
val sharedSecret = keyAgreement.generateSecret() val sharedSecret = keyAgreement.generateSecret()
// Use first 32 bytes (x-coordinate) as AES key // Use first 32 bytes (x-coordinate) as AES key
val sharedKey = sharedSecret.copyOfRange(1, 33) val sharedKey = sharedSecret.copyOfRange(1, 33)
val key = SecretKeySpec(sharedKey, "AES") val key = SecretKeySpec(sharedKey, "AES")
// Decrypt with AES-256-CBC // Decrypt with AES-256-CBC
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
val decrypted = cipher.doFinal(ciphertext) val decrypted = cipher.doFinal(ciphertext)
String(decrypted, Charsets.UTF_8) String(decrypted, Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
/** /**
* Encrypt data using XChaCha20-Poly1305 * Encrypt data using XChaCha20-Poly1305
* *
* Returns: { * Returns: { ciphertext: hex string, nonce: hex string (24 bytes), key: hex string (32 bytes) }
* ciphertext: hex string,
* nonce: hex string (24 bytes),
* key: hex string (32 bytes)
* }
*/ */
fun chacha20Encrypt(data: String): ChaCha20Result { fun chacha20Encrypt(data: String): ChaCha20Result {
// Generate random key (32 bytes) and nonce (24 bytes) // Generate random key (32 bytes) and nonce (24 bytes)
@@ -599,22 +617,22 @@ object CryptoManager {
val nonce = ByteArray(24) val nonce = ByteArray(24)
SecureRandom().nextBytes(key) SecureRandom().nextBytes(key)
SecureRandom().nextBytes(nonce) SecureRandom().nextBytes(nonce)
// Encrypt using XChaCha20-Poly1305 // Encrypt using XChaCha20-Poly1305
val cipher = XChaCha20Poly1305(key) val cipher = XChaCha20Poly1305(key)
val plaintext = data.toByteArray(Charsets.UTF_8) val plaintext = data.toByteArray(Charsets.UTF_8)
val ciphertext = cipher.encrypt(nonce, plaintext) val ciphertext = cipher.encrypt(nonce, plaintext)
return ChaCha20Result( return ChaCha20Result(
ciphertext = ciphertext.joinToString("") { "%02x".format(it) }, ciphertext = ciphertext.joinToString("") { "%02x".format(it) },
nonce = nonce.joinToString("") { "%02x".format(it) }, nonce = nonce.joinToString("") { "%02x".format(it) },
key = key.joinToString("") { "%02x".format(it) } key = key.joinToString("") { "%02x".format(it) }
) )
} }
/** /**
* Decrypt data using XChaCha20-Poly1305 * Decrypt data using XChaCha20-Poly1305
* *
* @param ciphertextHex Hex-encoded ciphertext * @param ciphertextHex Hex-encoded ciphertext
* @param nonceHex Hex-encoded nonce (24 bytes) * @param nonceHex Hex-encoded nonce (24 bytes)
* @param keyHex Hex-encoded key (32 bytes) * @param keyHex Hex-encoded key (32 bytes)
@@ -624,10 +642,10 @@ object CryptoManager {
val ciphertext = ciphertextHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() val ciphertext = ciphertextHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val nonce = nonceHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() val nonce = nonceHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val key = keyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() val key = keyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val cipher = XChaCha20Poly1305(key) val cipher = XChaCha20Poly1305(key)
val decrypted = cipher.decrypt(nonce, ciphertext) val decrypted = cipher.decrypt(nonce, ciphertext)
String(decrypted, Charsets.UTF_8) String(decrypted, Charsets.UTF_8)
} catch (e: Exception) { } catch (e: Exception) {
null null
@@ -635,13 +653,6 @@ object CryptoManager {
} }
} }
data class KeyPairData( data class KeyPairData(val privateKey: String, val publicKey: String)
val privateKey: String,
val publicKey: String
)
data class ChaCha20Result( data class ChaCha20Result(val ciphertext: String, val nonce: String, val key: String)
val ciphertext: String,
val nonce: String,
val key: String
)

View File

@@ -367,10 +367,24 @@ interface DialogDao {
AND i_have_sent = 1 AND i_have_sent = 1
AND last_message_timestamp > 0 AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC ORDER BY last_message_timestamp DESC
LIMIT 30
""" """
) )
fun getDialogsFlow(account: String): Flow<List<DialogEntity>> fun getDialogsFlow(account: String): Flow<List<DialogEntity>>
/** Получить все диалоги с пагинацией */
@Query(
"""
SELECT * FROM dialogs
WHERE account = :account
AND i_have_sent = 1
AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC
LIMIT :limit OFFSET :offset
"""
)
suspend fun getDialogsPaged(account: String, limit: Int, offset: Int): List<DialogEntity>
/** /**
* Получить requests - диалоги где нам писали, но мы не отвечали Исключает пустые диалоги (без * Получить requests - диалоги где нам писали, но мы не отвечали Исключает пустые диалоги (без
* сообщений) * сообщений)
@@ -382,6 +396,7 @@ interface DialogDao {
AND i_have_sent = 0 AND i_have_sent = 0
AND last_message_timestamp > 0 AND last_message_timestamp > 0
ORDER BY last_message_timestamp DESC ORDER BY last_message_timestamp DESC
LIMIT 30
""" """
) )
fun getRequestsFlow(account: String): Flow<List<DialogEntity>> fun getRequestsFlow(account: String): Flow<List<DialogEntity>>

View File

@@ -117,11 +117,18 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
return return
} }
// 🔥 Показываем skeleton пока данные грузятся
_isLoading.value = true
// 🔥 Очищаем кэш запрошенных user info при смене аккаунта // 🔥 Очищаем кэш запрошенных user info при смене аккаунта
requestedUserInfoKeys.clear() requestedUserInfoKeys.clear()
currentAccount = publicKey currentAccount = publicKey
currentPrivateKey = privateKey currentPrivateKey = privateKey
// 🚀 Pre-warm PBKDF2 ключ — чтобы к моменту дешифровки кэш был горячий
viewModelScope.launch(Dispatchers.Default) { CryptoManager.getPbkdf2Key(privateKey) }
// Подписываемся на обычные диалоги // Подписываемся на обычные диалоги
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
viewModelScope.launch { viewModelScope.launch {
@@ -268,6 +275,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
.distinctUntilChanged() // 🔥 ИСПРАВЛЕНИЕ: Игнорируем дублирующиеся списки .distinctUntilChanged() // 🔥 ИСПРАВЛЕНИЕ: Игнорируем дублирующиеся списки
.collect { decryptedDialogs -> .collect { decryptedDialogs ->
_dialogs.value = decryptedDialogs _dialogs.value = decryptedDialogs
// 🚀 Убираем skeleton после первой загрузки
if (_isLoading.value) _isLoading.value = false
// 🟢 Подписываемся на онлайн-статусы всех собеседников // 🟢 Подписываемся на онлайн-статусы всех собеседников
// 📁 Исключаем Saved Messages - не нужно подписываться на свой собственный // 📁 Исключаем Saved Messages - не нужно подписываться на свой собственный

View File

@@ -25,7 +25,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -89,7 +89,6 @@ fun MessageInputBar(
inputFocusTrigger: Int = 0 inputFocusTrigger: Int = 0
) { ) {
val hasReply = replyMessages.isNotEmpty() val hasReply = replyMessages.isNotEmpty()
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
@@ -206,7 +205,8 @@ fun MessageInputBar(
showKeyboard = { showKeyboard = {
editTextView?.let { editText -> editTextView?.let { editText ->
editText.requestFocus() editText.requestFocus()
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) @Suppress("DEPRECATION")
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
} }
}, },
hideEmoji = { onToggleEmojiPicker(false) } hideEmoji = { onToggleEmojiPicker(false) }

View File

@@ -214,7 +214,7 @@ private fun OnlineIndicator(modifier: Modifier = Modifier) {
*/ */
@Composable @Composable
fun AvatarPicker( fun AvatarPicker(
onAvatarSelected: (String) -> Unit @Suppress("UNUSED_PARAMETER") onAvatarSelected: (String) -> Unit
) { ) {
// TODO: Реализовать выбор изображения через ActivityResultContract // TODO: Реализовать выбор изображения через ActivityResultContract
// 1. Использовать rememberLauncherForActivityResult с ActivityResultContracts.GetContent() // 1. Использовать rememberLauncherForActivityResult с ActivityResultContracts.GetContent()

View File

@@ -129,7 +129,6 @@ object OptimizedEmojiCache {
isPreloading = true isPreloading = true
// Берем первые N эмодзи из категории Smileys (самые популярные) // Берем первые N эмодзи из категории Smileys (самые популярные)
val smileyCategory = EMOJI_CATEGORIES.find { it.key == "Smileys" }
val smileysToPreload = emojisByCategory?.get("Smileys") val smileysToPreload = emojisByCategory?.get("Smileys")
?.take(PRELOAD_COUNT) ?.take(PRELOAD_COUNT)
?: emptyList() ?: emptyList()

View File

@@ -0,0 +1,33 @@
plugins {
id("com.android.test")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.rosetta.messenger.baselineprofile"
compileSdk = 34
defaultConfig {
minSdk = 28 // Baseline Profiles require API 28+
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
targetProjectPath = ":app"
}
dependencies {
implementation("androidx.test.ext:junit:1.1.5")
implementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("androidx.test.uiautomator:uiautomator:2.2.0")
implementation("androidx.benchmark:benchmark-macro-junit4:1.2.2")
}

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,66 @@
package com.rosetta.messenger.baselineprofile
import androidx.benchmark.macro.ExperimentalBaselineProfilesApi
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Baseline Profile Generator для Rosetta Messenger
*
* Запуск:
* ./gradlew :baselineprofile:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.rosetta.messenger.baselineprofile.BaselineProfileGenerator
*
* Результат:
* Профиль будет сгенерирован и сохранён — скопируйте его в app/src/main/baseline-prof.txt
*
* Требования:
* - Эмулятор или устройство API 28+ (лучше userdebug/rooted)
* - Приложение должно быть установлено
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun generateBaselineProfile() = baselineProfileRule.collectBaselineProfile(
packageName = "com.rosetta.messenger"
) {
// 1. Cold start — запуск приложения
pressHome()
startActivityAndWait()
// 2. Ждём загрузки (Splash → Auth/Main)
device.waitForIdle()
Thread.sleep(3000) // Ждём splash + auth flow
// 3. Если попали на экран логина — пропускаем (профиль соберёт auth UI)
// Если попали на список чатов — скроллим
val chatList = device.wait(Until.hasObject(By.scrollable(true)), 5000)
if (chatList) {
// Скролл списка чатов вниз и обратно
val scrollable = device.findObject(By.scrollable(true))
scrollable?.let {
it.scroll(androidx.test.uiautomator.Direction.DOWN, 2.0f)
device.waitForIdle()
Thread.sleep(500)
it.scroll(androidx.test.uiautomator.Direction.UP, 2.0f)
device.waitForIdle()
Thread.sleep(500)
}
}
// 4. Ждём перед завершением
device.waitForIdle()
Thread.sleep(1000)
}
}

View File

@@ -0,0 +1,61 @@
package com.rosetta.messenger.baselineprofile
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Startup Benchmark для замера холодного старта Rosetta
*
* Запуск:
* ./gradlew :baselineprofile:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.rosetta.messenger.baselineprofile.StartupBenchmark
*
* Сравнивает 3 режима компиляции:
* - None (интерпретатор) — worst case
* - BaselineProfile — с нашими профилями
* - Full (полная AOT) — best case
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startupNoCompilation() = startup(CompilationMode.None())
@Test
fun startupBaselineProfile() = startup(CompilationMode.Partial())
@Test
fun startupFullCompilation() = startup(CompilationMode.Full())
private fun startup(compilationMode: CompilationMode) {
benchmarkRule.measureRepeated(
packageName = "com.rosetta.messenger",
metrics = listOf(StartupTimingMetric(), FrameTimingMetric()),
compilationMode = compilationMode,
iterations = 5,
startupMode = StartupMode.COLD,
setupBlock = {
pressHome()
}
) {
startActivityAndWait()
// Ждём появления контента (чат-лист или auth)
device.waitForIdle()
Thread.sleep(2000)
}
}
}

View File

@@ -16,3 +16,4 @@ dependencyResolutionManagement {
rootProject.name = "rosetta-android" rootProject.name = "rosetta-android"
include(":app") include(":app")
include(":baselineprofile")