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:
@@ -1,47 +1,108 @@
|
||||
# Baseline Profile для Rosetta Messenger
|
||||
# Предкомпилирует критические функции при установке APK
|
||||
# Baseline Profile for Rosetta Messenger
|
||||
# AOT-compiles critical classes and methods at APK install time
|
||||
|
||||
# ============ Keyboard & Animation (приоритет #1) ============
|
||||
# MessageInputBar - основной источник JIT лага
|
||||
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 и навигация
|
||||
# 1. APP STARTUP
|
||||
HSPLcom/rosetta/messenger/RosettaApplication;->onCreate()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) ============
|
||||
# Часто используемые Compose компоненты
|
||||
HSPLandroidx/compose/foundation/lazy/LazyListState;->scrollToItem(IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
|
||||
HSPLandroidx/compose/ui/text/input/TextFieldValue;-><init>(Ljava/lang/String;JLandroidx/compose/ui/text/TextRange;)V
|
||||
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;
|
||||
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
|
||||
# 2. CRYPTO
|
||||
HSPLcom/rosetta/messenger/crypto/CryptoManager;->getPbkdf2Key(Ljava/lang/String;)Ljavax/crypto/spec/SecretKeySpec;
|
||||
HSPLcom/rosetta/messenger/crypto/CryptoManager;->decryptWithPassword(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
|
||||
HSPLcom/rosetta/messenger/crypto/CryptoManager;->decryptWithPasswordInternal(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
|
||||
HSPLcom/rosetta/messenger/crypto/CryptoManager;->isOldFormat(Ljava/lang/String;)Z
|
||||
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;
|
||||
|
||||
@@ -1,94 +1,95 @@
|
||||
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.MnemonicException
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.jce.spec.ECPrivateKeySpec
|
||||
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
|
||||
* Implements BIP39 seed phrase generation and secp256k1 key derivation
|
||||
* Cryptography module for Rosetta Messenger Implements BIP39 seed phrase generation and secp256k1
|
||||
* key derivation
|
||||
*/
|
||||
object CryptoManager {
|
||||
|
||||
|
||||
private const val PBKDF2_ITERATIONS = 1000
|
||||
private const val KEY_SIZE = 256
|
||||
private const val SALT = "rosetta"
|
||||
|
||||
|
||||
// 🚀 ОПТИМИЗАЦИЯ: Кэш для генерации ключей (seedPhrase -> KeyPair)
|
||||
private val keyPairCache = mutableMapOf<String, KeyPairData>()
|
||||
private val privateKeyHashCache = mutableMapOf<String, String>()
|
||||
|
||||
|
||||
// 🚀 КРИТИЧЕСКАЯ ОПТИМИЗАЦИЯ: Кэш для PBKDF2 производных ключей
|
||||
// PBKDF2 с 1000 итерациями - очень тяжелая операция!
|
||||
// Кэшируем производный ключ для каждого пароля чтобы не вычислять его каждый раз
|
||||
private val pbkdf2KeyCache = mutableMapOf<String, SecretKeySpec>()
|
||||
|
||||
|
||||
// 🚀 ОПТИМИЗАЦИЯ: Lock-free кэш для расшифрованных сообщений
|
||||
// ConcurrentHashMap вместо synchronized LinkedHashMap — убирает контention при параллельной расшифровке
|
||||
// ConcurrentHashMap вместо synchronized LinkedHashMap — убирает контention при параллельной
|
||||
// расшифровке
|
||||
private const val DECRYPTION_CACHE_SIZE = 2000
|
||||
private val decryptionCache = ConcurrentHashMap<String, String>(DECRYPTION_CACHE_SIZE, 0.75f, 4)
|
||||
|
||||
|
||||
init {
|
||||
// Add BouncyCastle provider for secp256k1 support
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🚀 Получить или вычислить PBKDF2 ключ для пароля (кэшируется)
|
||||
* 🚀 Получить или вычислить PBKDF2 ключ для пароля (кэшируется) Public для pre-warm при логине
|
||||
* (чтобы кэш был горячий к моменту дешифровки)
|
||||
*/
|
||||
private fun getPbkdf2Key(password: String): SecretKeySpec {
|
||||
fun getPbkdf2Key(password: String): SecretKeySpec {
|
||||
return pbkdf2KeyCache.getOrPut(password) {
|
||||
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)
|
||||
SecretKeySpec(secretKey.encoded, "AES")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🧹 Очистить кэши при logout
|
||||
*/
|
||||
|
||||
/** 🧹 Очистить кэши при logout */
|
||||
fun clearCaches() {
|
||||
pbkdf2KeyCache.clear()
|
||||
decryptionCache.clear()
|
||||
keyPairCache.clear()
|
||||
privateKeyHashCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new 12-word BIP39 seed phrase
|
||||
*/
|
||||
|
||||
/** Generate a new 12-word BIP39 seed phrase */
|
||||
fun generateSeedPhrase(): List<String> {
|
||||
val secureRandom = SecureRandom()
|
||||
val entropy = ByteArray(16) // 128 bits = 12 words
|
||||
secureRandom.nextBytes(entropy)
|
||||
|
||||
|
||||
val mnemonicCode = MnemonicCode.INSTANCE
|
||||
return mnemonicCode.toMnemonic(entropy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a seed phrase
|
||||
*/
|
||||
|
||||
/** Validate a seed phrase */
|
||||
fun validateSeedPhrase(words: List<String>): Boolean {
|
||||
return try {
|
||||
val mnemonicCode = MnemonicCode.INSTANCE
|
||||
@@ -98,15 +99,15 @@ object CryptoManager {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert seed phrase to private key (32 bytes hex string)
|
||||
*
|
||||
*
|
||||
* Алгоритм (совместим с 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
|
||||
* let seed = await mnemonicToSeed(phrase); // BIP39 → 64 bytes
|
||||
@@ -118,96 +119,94 @@ object CryptoManager {
|
||||
// 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))
|
||||
|
||||
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate key pair from seed phrase using secp256k1 curve
|
||||
*
|
||||
*
|
||||
* Алгоритм (совместим с Desktop desktop-rosetta):
|
||||
* - privateKey = SHA256(hex(BIP39_seed(mnemonic))) - 32 байта
|
||||
* - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта
|
||||
*
|
||||
*
|
||||
* 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<String>): KeyPairData {
|
||||
val cacheKey = seedPhrase.joinToString(" ")
|
||||
|
||||
|
||||
// Проверяем кэш
|
||||
keyPairCache[cacheKey]?.let { return it }
|
||||
|
||||
keyPairCache[cacheKey]?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
// Генерируем приватный ключ через SHA256
|
||||
val privateKeyHex = seedPhraseToPrivateKey(seedPhrase)
|
||||
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
|
||||
|
||||
// Преобразуем hex в bytes (32 байта)
|
||||
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 publicKeyPoint = ecSpec.g.multiply(privateKeyBigInt)
|
||||
|
||||
|
||||
// ⚡ ВАЖНО: Используем СЖАТЫЙ формат (compressed=true) - 33 байта вместо 65
|
||||
// Это совместимо с crypto_new где используется: secp256k1.getPublicKey(..., true)
|
||||
val publicKeyHex = publicKeyPoint.getEncoded(true)
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
|
||||
val keyPair = KeyPairData(
|
||||
privateKey = privateKeyHex,
|
||||
publicKey = publicKeyHex
|
||||
)
|
||||
|
||||
val publicKeyHex = publicKeyPoint.getEncoded(true).joinToString("") { "%02x".format(it) }
|
||||
|
||||
val keyPair = KeyPairData(privateKey = privateKeyHex, 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 {
|
||||
// Проверяем кэш
|
||||
privateKeyHashCache[privateKey]?.let { return it }
|
||||
|
||||
privateKeyHashCache[privateKey]?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val data = (privateKey + SALT).toByteArray()
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(data)
|
||||
val hashHex = hash.joinToString("") { "%02x".format(it) }
|
||||
|
||||
|
||||
// Сохраняем в кэш
|
||||
privateKeyHashCache[privateKey] = hashHex
|
||||
if (privateKeyHashCache.size > 10) {
|
||||
privateKeyHashCache.remove(privateKeyHashCache.keys.first())
|
||||
}
|
||||
|
||||
|
||||
return hashHex
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt data with password using PBKDF2 + AES
|
||||
*
|
||||
*
|
||||
* ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts):
|
||||
* - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию
|
||||
* - Salt: "rosetta"
|
||||
@@ -222,38 +221,44 @@ object CryptoManager {
|
||||
fun encryptWithPassword(data: String, password: String): String {
|
||||
// Compress data (zlib deflate - совместимо с pako.deflate в JS)
|
||||
val compressed = compress(data.toByteArray(Charsets.UTF_8))
|
||||
|
||||
|
||||
val CHUNK_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
|
||||
// Check if we need chunking
|
||||
if (compressed.size > CHUNK_SIZE) {
|
||||
// Chunk the compressed data
|
||||
val chunks = compressed.toList().chunked(CHUNK_SIZE).map { it.toByteArray() }
|
||||
val encryptedChunks = mutableListOf<String>()
|
||||
|
||||
|
||||
for (chunk in chunks) {
|
||||
// Derive key using PBKDF2-HMAC-SHA1
|
||||
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 key = SecretKeySpec(secretKey.encoded, "AES")
|
||||
|
||||
|
||||
// Generate random IV
|
||||
val iv = ByteArray(16)
|
||||
SecureRandom().nextBytes(iv)
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
|
||||
|
||||
// Encrypt with AES
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
|
||||
val encrypted = cipher.doFinal(chunk)
|
||||
|
||||
|
||||
// Store as ivBase64:ctBase64
|
||||
val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||
val ctBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||
encryptedChunks.add("$ivBase64:$ctBase64")
|
||||
}
|
||||
|
||||
|
||||
// Return chunked format: "CHNK:" + chunks joined by "::"
|
||||
return "CHNK:" + encryptedChunks.joinToString("::")
|
||||
} else {
|
||||
@@ -261,52 +266,60 @@ object CryptoManager {
|
||||
// Derive key using PBKDF2-HMAC-SHA1 (⚠️ SHA1, не SHA256!)
|
||||
// crypto-js по умолчанию использует SHA1 для PBKDF2
|
||||
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 key = SecretKeySpec(secretKey.encoded, "AES")
|
||||
|
||||
|
||||
// Generate random IV
|
||||
val iv = ByteArray(16)
|
||||
SecureRandom().nextBytes(iv)
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
|
||||
|
||||
// Encrypt with AES
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
|
||||
val encrypted = cipher.doFinal(compressed)
|
||||
|
||||
|
||||
// Return iv:ciphertext in Base64
|
||||
val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||
val ctBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||
|
||||
|
||||
return "$ivBase64:$ctBase64"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decrypt data with password
|
||||
*
|
||||
*
|
||||
* ⚠️ ВАЖНО: Совместимость с JS (crypto-js) и React Native (cryptoJSI.ts):
|
||||
* - PBKDF2WithHmacSHA1 (не SHA256!) - crypto-js использует SHA1 по умолчанию
|
||||
* - Salt: "rosetta"
|
||||
* - Iterations: 1000
|
||||
* - Iterations: 1000
|
||||
* - Key size: 256 bit
|
||||
* - AES-256-CBC с PKCS5/PKCS7 padding
|
||||
* - Decompression: zlib inflate (pako.inflate в JS)
|
||||
* - Supports old format (base64-encoded hex "iv:ciphertext")
|
||||
* - Supports new format (base64 "iv:ciphertext")
|
||||
* - Supports chunked format ("CHNK:" + chunks joined by "::")
|
||||
*
|
||||
*
|
||||
* 🚀 ОПТИМИЗАЦИЯ: Кэширование PBKDF2 ключа и расшифрованных сообщений
|
||||
*/
|
||||
fun decryptWithPassword(encryptedData: String, password: String): String? {
|
||||
// 🚀 ОПТИМИЗАЦИЯ: Lock-free проверка кэша (ConcurrentHashMap)
|
||||
val cacheKey = "$password:$encryptedData"
|
||||
decryptionCache[cacheKey]?.let { return it }
|
||||
|
||||
decryptionCache[cacheKey]?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
return try {
|
||||
val result = decryptWithPasswordInternal(encryptedData, password)
|
||||
|
||||
|
||||
// 🚀 Сохраняем в кэш (lock-free)
|
||||
if (result != null) {
|
||||
// Ограничиваем размер кэша
|
||||
@@ -317,106 +330,103 @@ object CryptoManager {
|
||||
}
|
||||
decryptionCache[cacheKey] = result
|
||||
}
|
||||
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔐 Внутренняя функция расшифровки (без кэширования результата)
|
||||
*/
|
||||
|
||||
/** 🔐 Внутренняя функция расшифровки (без кэширования результата) */
|
||||
private fun decryptWithPasswordInternal(encryptedData: String, password: String): String? {
|
||||
return try {
|
||||
// 🚀 Получаем кэшированный PBKDF2 ключ
|
||||
val key = getPbkdf2Key(password)
|
||||
|
||||
|
||||
// Check for old format: base64-encoded string containing hex
|
||||
if (isOldFormat(encryptedData)) {
|
||||
val decoded = String(Base64.decode(encryptedData, Base64.NO_WRAP), Charsets.UTF_8)
|
||||
val parts = decoded.split(":")
|
||||
if (parts.size != 2) return null
|
||||
|
||||
|
||||
val iv = parts[0].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 (используем кэшированный ключ!)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||
val decrypted = cipher.doFinal(ciphertext)
|
||||
|
||||
|
||||
return String(decrypted, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
|
||||
// Check for chunked format
|
||||
if (encryptedData.startsWith("CHNK:")) {
|
||||
val chunkStrings = encryptedData.substring(5).split("::")
|
||||
val decompressedParts = mutableListOf<ByteArray>()
|
||||
|
||||
|
||||
for (chunkString in chunkStrings) {
|
||||
val parts = chunkString.split(":")
|
||||
if (parts.size != 2) return null
|
||||
|
||||
|
||||
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
||||
val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP)
|
||||
|
||||
|
||||
// Decrypt with AES-256-CBC (используем кэшированный ключ!)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||
val decrypted = cipher.doFinal(ciphertext)
|
||||
|
||||
|
||||
decompressedParts.add(decrypted)
|
||||
}
|
||||
|
||||
|
||||
// Concatenate all decrypted chunks
|
||||
val allBytes = decompressedParts.fold(ByteArray(0)) { acc, arr -> acc + arr }
|
||||
|
||||
|
||||
// Decompress the concatenated data
|
||||
return String(decompress(allBytes), Charsets.UTF_8)
|
||||
}
|
||||
|
||||
|
||||
// New format: base64 "iv:ciphertext"
|
||||
val parts = encryptedData.split(":")
|
||||
if (parts.size != 2) return null
|
||||
|
||||
|
||||
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
||||
val ciphertext = Base64.decode(parts[1], Base64.NO_WRAP)
|
||||
|
||||
|
||||
// Decrypt with AES-256-CBC (используем кэшированный ключ!)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||
val decrypted = cipher.doFinal(ciphertext)
|
||||
|
||||
|
||||
// Decompress (zlib inflate - совместимо с pako.inflate в JS)
|
||||
String(decompress(decrypted), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
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 {
|
||||
// 🚀 Fast path: new format always contains ':' at plaintext level (iv:ct)
|
||||
// Old format is a single base64 blob without ':' in the encoded string
|
||||
if (data.contains(':')) return false
|
||||
if (data.startsWith("CHNK:")) return false
|
||||
|
||||
|
||||
return try {
|
||||
val decoded = String(Base64.decode(data, Base64.NO_WRAP), Charsets.UTF_8)
|
||||
decoded.contains(":") && decoded.split(":").all { part ->
|
||||
part.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
|
||||
}
|
||||
decoded.contains(":") &&
|
||||
decoded.split(":").all { part ->
|
||||
part.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RAW Deflate сжатие (без zlib header)
|
||||
*
|
||||
*
|
||||
* ⚠️ ВАЖНО: nowrap=true для совместимости с pako.deflate() в JS!
|
||||
* - pako.deflate() создаёт RAW deflate поток (без 2-byte zlib header)
|
||||
* - Java Deflater() по умолчанию создаёт zlib поток (с header 78 9C)
|
||||
@@ -427,7 +437,7 @@ object CryptoManager {
|
||||
val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, true)
|
||||
deflater.setInput(data)
|
||||
deflater.finish()
|
||||
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
while (!deflater.finished()) {
|
||||
@@ -438,10 +448,10 @@ object CryptoManager {
|
||||
outputStream.close()
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RAW Inflate декомпрессия (без zlib header)
|
||||
*
|
||||
*
|
||||
* ⚠️ ВАЖНО: nowrap=true для совместимости с pako.inflate() в JS!
|
||||
* - pako.inflate() ожидает RAW deflate поток
|
||||
* - Java Inflater() по умолчанию ожидает zlib поток (с header)
|
||||
@@ -451,7 +461,7 @@ object CryptoManager {
|
||||
// nowrap=true = RAW inflate (совместимо с pako.inflate)
|
||||
val inflater = Inflater(true)
|
||||
inflater.setInput(data)
|
||||
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(1024)
|
||||
while (!inflater.finished()) {
|
||||
@@ -462,10 +472,10 @@ object CryptoManager {
|
||||
outputStream.close()
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt data using ECDH + AES
|
||||
*
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Generate ephemeral key pair
|
||||
* 2. Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey)
|
||||
@@ -475,61 +485,67 @@ object CryptoManager {
|
||||
*/
|
||||
fun encrypt(data: String, publicKeyHex: String): String {
|
||||
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
|
||||
|
||||
// Generate ephemeral private key (random 32 bytes)
|
||||
val ephemeralPrivateKeyBytes = ByteArray(32)
|
||||
SecureRandom().nextBytes(ephemeralPrivateKeyBytes)
|
||||
val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes)
|
||||
|
||||
|
||||
// Generate ephemeral public key from private key
|
||||
val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt)
|
||||
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
// Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey)
|
||||
val sharedPoint = recipientPublicKeyPoint.multiply(ephemeralPrivateKeyBigInt).normalize()
|
||||
|
||||
|
||||
// Use x-coordinate of shared point as AES key (32 bytes)
|
||||
val sharedKeyBytes = sharedPoint.affineXCoord.encoded
|
||||
val sharedKey = if (sharedKeyBytes.size >= 32) {
|
||||
sharedKeyBytes.copyOfRange(sharedKeyBytes.size - 32, sharedKeyBytes.size)
|
||||
} else {
|
||||
// Pad with leading zeros if needed
|
||||
ByteArray(32 - sharedKeyBytes.size) + sharedKeyBytes
|
||||
}
|
||||
val sharedKey =
|
||||
if (sharedKeyBytes.size >= 32) {
|
||||
sharedKeyBytes.copyOfRange(sharedKeyBytes.size - 32, sharedKeyBytes.size)
|
||||
} else {
|
||||
// Pad with leading zeros if needed
|
||||
ByteArray(32 - sharedKeyBytes.size) + sharedKeyBytes
|
||||
}
|
||||
val key = SecretKeySpec(sharedKey, "AES")
|
||||
|
||||
|
||||
// Generate random IV
|
||||
val iv = ByteArray(16)
|
||||
SecureRandom().nextBytes(iv)
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
|
||||
|
||||
// Encrypt with AES
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
|
||||
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
||||
|
||||
|
||||
// Normalize ephemeral private key to 32 bytes
|
||||
val normalizedPrivateKey = if (ephemeralPrivateKeyBytes.size > 32) {
|
||||
ephemeralPrivateKeyBytes.copyOfRange(ephemeralPrivateKeyBytes.size - 32, ephemeralPrivateKeyBytes.size)
|
||||
} else {
|
||||
ephemeralPrivateKeyBytes
|
||||
}
|
||||
|
||||
val normalizedPrivateKey =
|
||||
if (ephemeralPrivateKeyBytes.size > 32) {
|
||||
ephemeralPrivateKeyBytes.copyOfRange(
|
||||
ephemeralPrivateKeyBytes.size - 32,
|
||||
ephemeralPrivateKeyBytes.size
|
||||
)
|
||||
} else {
|
||||
ephemeralPrivateKeyBytes
|
||||
}
|
||||
|
||||
// Return base64(iv:ciphertext:ephemeralPrivateKey)
|
||||
val ivHex = iv.joinToString("") { "%02x".format(it) }
|
||||
val ctHex = encrypted.joinToString("") { "%02x".format(it) }
|
||||
val ephemeralPrivateKeyHex = normalizedPrivateKey.joinToString("") { "%02x".format(it) }
|
||||
val combined = "$ivHex:$ctHex:$ephemeralPrivateKeyHex"
|
||||
|
||||
|
||||
return Base64.encodeToString(combined.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decrypt data using ECDH + AES
|
||||
*
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Parse iv, ciphertext, and ephemeralPrivateKey from base64
|
||||
* 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 parts = decoded.split(":")
|
||||
if (parts.size != 3) return null
|
||||
|
||||
|
||||
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 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")
|
||||
|
||||
|
||||
// Compute ephemeral public key from ephemeral private key
|
||||
val ephemeralPrivateKeyBigInt = BigInteger(1, ephemeralPrivateKeyBytes)
|
||||
val ephemeralPublicKeyPoint = ecSpec.g.multiply(ephemeralPrivateKeyBigInt)
|
||||
val ephemeralPublicKeySpec = ECPublicKeySpec(ephemeralPublicKeyPoint, ecSpec)
|
||||
val keyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME)
|
||||
val ephemeralPublicKey = keyFactory.generatePublic(ephemeralPublicKeySpec)
|
||||
|
||||
|
||||
// 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 privateKeySpec = ECPrivateKeySpec(privateKeyBigInt, ecSpec)
|
||||
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||
|
||||
|
||||
// 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.doPhase(ephemeralPublicKey, true)
|
||||
val sharedSecret = keyAgreement.generateSecret()
|
||||
|
||||
|
||||
// Use first 32 bytes (x-coordinate) as AES key
|
||||
val sharedKey = sharedSecret.copyOfRange(1, 33)
|
||||
val key = SecretKeySpec(sharedKey, "AES")
|
||||
|
||||
|
||||
// Decrypt with AES-256-CBC
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||
val decrypted = cipher.doFinal(ciphertext)
|
||||
|
||||
|
||||
String(decrypted, Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt data using XChaCha20-Poly1305
|
||||
*
|
||||
* Returns: {
|
||||
* ciphertext: hex string,
|
||||
* nonce: hex string (24 bytes),
|
||||
* key: hex string (32 bytes)
|
||||
* }
|
||||
*
|
||||
* Returns: { ciphertext: hex string, nonce: hex string (24 bytes), key: hex string (32 bytes) }
|
||||
*/
|
||||
fun chacha20Encrypt(data: String): ChaCha20Result {
|
||||
// Generate random key (32 bytes) and nonce (24 bytes)
|
||||
@@ -599,22 +617,22 @@ object CryptoManager {
|
||||
val nonce = ByteArray(24)
|
||||
SecureRandom().nextBytes(key)
|
||||
SecureRandom().nextBytes(nonce)
|
||||
|
||||
|
||||
// Encrypt using XChaCha20-Poly1305
|
||||
val cipher = XChaCha20Poly1305(key)
|
||||
val plaintext = data.toByteArray(Charsets.UTF_8)
|
||||
val ciphertext = cipher.encrypt(nonce, plaintext)
|
||||
|
||||
|
||||
return ChaCha20Result(
|
||||
ciphertext = ciphertext.joinToString("") { "%02x".format(it) },
|
||||
nonce = nonce.joinToString("") { "%02x".format(it) },
|
||||
key = key.joinToString("") { "%02x".format(it) }
|
||||
ciphertext = ciphertext.joinToString("") { "%02x".format(it) },
|
||||
nonce = nonce.joinToString("") { "%02x".format(it) },
|
||||
key = key.joinToString("") { "%02x".format(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decrypt data using XChaCha20-Poly1305
|
||||
*
|
||||
*
|
||||
* @param ciphertextHex Hex-encoded ciphertext
|
||||
* @param nonceHex Hex-encoded nonce (24 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 nonce = nonceHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
val key = keyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
|
||||
|
||||
val cipher = XChaCha20Poly1305(key)
|
||||
val decrypted = cipher.decrypt(nonce, ciphertext)
|
||||
|
||||
|
||||
String(decrypted, Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -635,13 +653,6 @@ object CryptoManager {
|
||||
}
|
||||
}
|
||||
|
||||
data class KeyPairData(
|
||||
val privateKey: String,
|
||||
val publicKey: String
|
||||
)
|
||||
data class KeyPairData(val privateKey: String, val publicKey: String)
|
||||
|
||||
data class ChaCha20Result(
|
||||
val ciphertext: String,
|
||||
val nonce: String,
|
||||
val key: String
|
||||
)
|
||||
data class ChaCha20Result(val ciphertext: String, val nonce: String, val key: String)
|
||||
|
||||
@@ -367,10 +367,24 @@ interface DialogDao {
|
||||
AND i_have_sent = 1
|
||||
AND last_message_timestamp > 0
|
||||
ORDER BY last_message_timestamp DESC
|
||||
LIMIT 30
|
||||
"""
|
||||
)
|
||||
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 - диалоги где нам писали, но мы не отвечали Исключает пустые диалоги (без
|
||||
* сообщений)
|
||||
@@ -382,6 +396,7 @@ interface DialogDao {
|
||||
AND i_have_sent = 0
|
||||
AND last_message_timestamp > 0
|
||||
ORDER BY last_message_timestamp DESC
|
||||
LIMIT 30
|
||||
"""
|
||||
)
|
||||
fun getRequestsFlow(account: String): Flow<List<DialogEntity>>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -117,11 +117,18 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
return
|
||||
}
|
||||
|
||||
// 🔥 Показываем skeleton пока данные грузятся
|
||||
_isLoading.value = true
|
||||
|
||||
// 🔥 Очищаем кэш запрошенных user info при смене аккаунта
|
||||
requestedUserInfoKeys.clear()
|
||||
|
||||
currentAccount = publicKey
|
||||
currentPrivateKey = privateKey
|
||||
|
||||
// 🚀 Pre-warm PBKDF2 ключ — чтобы к моменту дешифровки кэш был горячий
|
||||
viewModelScope.launch(Dispatchers.Default) { CryptoManager.getPbkdf2Key(privateKey) }
|
||||
|
||||
// Подписываемся на обычные диалоги
|
||||
@OptIn(FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
@@ -268,6 +275,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
.distinctUntilChanged() // 🔥 ИСПРАВЛЕНИЕ: Игнорируем дублирующиеся списки
|
||||
.collect { decryptedDialogs ->
|
||||
_dialogs.value = decryptedDialogs
|
||||
// 🚀 Убираем skeleton после первой загрузки
|
||||
if (_isLoading.value) _isLoading.value = false
|
||||
|
||||
// 🟢 Подписываемся на онлайн-статусы всех собеседников
|
||||
// 📁 Исключаем Saved Messages - не нужно подписываться на свой собственный
|
||||
|
||||
@@ -25,7 +25,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -89,7 +89,6 @@ fun MessageInputBar(
|
||||
inputFocusTrigger: Int = 0
|
||||
) {
|
||||
val hasReply = replyMessages.isNotEmpty()
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
@@ -206,7 +205,8 @@ fun MessageInputBar(
|
||||
showKeyboard = {
|
||||
editTextView?.let { editText ->
|
||||
editText.requestFocus()
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
|
||||
@Suppress("DEPRECATION")
|
||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
},
|
||||
hideEmoji = { onToggleEmojiPicker(false) }
|
||||
|
||||
@@ -214,7 +214,7 @@ private fun OnlineIndicator(modifier: Modifier = Modifier) {
|
||||
*/
|
||||
@Composable
|
||||
fun AvatarPicker(
|
||||
onAvatarSelected: (String) -> Unit
|
||||
@Suppress("UNUSED_PARAMETER") onAvatarSelected: (String) -> Unit
|
||||
) {
|
||||
// TODO: Реализовать выбор изображения через ActivityResultContract
|
||||
// 1. Использовать rememberLauncherForActivityResult с ActivityResultContracts.GetContent()
|
||||
|
||||
@@ -129,7 +129,6 @@ object OptimizedEmojiCache {
|
||||
isPreloading = true
|
||||
|
||||
// Берем первые N эмодзи из категории Smileys (самые популярные)
|
||||
val smileyCategory = EMOJI_CATEGORIES.find { it.key == "Smileys" }
|
||||
val smileysToPreload = emojisByCategory?.get("Smileys")
|
||||
?.take(PRELOAD_COUNT)
|
||||
?: emptyList()
|
||||
|
||||
Reference in New Issue
Block a user