feat: Update authorization logic for compatibility with crypto_new; enhance key generation and public key format

This commit is contained in:
k1ngsterr1
2026-01-16 04:53:48 +05:00
parent 306e854646
commit caf1d246d3
7 changed files with 774 additions and 15 deletions

View File

@@ -67,18 +67,37 @@ object CryptoManager {
}
/**
* Convert seed phrase to private key (64 bytes hex string)
* Convert seed phrase to private key (32 bytes hex string)
*
* ⚠️ НОВЫЙ МЕТОД (crypto_new): Использует SHA256(seedPhrase) вместо BIP39
* Совместимо с JavaScript реализацией crypto_new/crypto.ts:
* ```js
* const privateKey = sha256.create().update(seed).digest().toHex().toString();
* ```
*/
fun seedPhraseToPrivateKey(seedPhrase: List<String>): String {
val mnemonicCode = MnemonicCode.INSTANCE
val seed = MnemonicCode.toSeed(seedPhrase, "")
// Новый метод: SHA256(seedPhrase joined by space)
val seedString = seedPhrase.joinToString(" ")
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(seedString.toByteArray(Charsets.UTF_8))
// Convert to hex string (128 characters for 64 bytes)
return seed.joinToString("") { "%02x".format(it) }
// Convert to hex string (64 characters for 32 bytes)
return hash.joinToString("") { "%02x".format(it) }
}
/**
* Generate key pair from seed phrase using secp256k1 curve
*
* ⚠️ НОВЫЙ МЕТОД (crypto_new):
* - privateKey = SHA256(seedPhrase) - 32 байта
* - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта
*
* Совместимо с JavaScript реализацией crypto_new/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 {
@@ -87,23 +106,27 @@ object CryptoManager {
// Проверяем кэш
keyPairCache[cacheKey]?.let { return it }
// Генерируем приватный ключ через SHA256
val privateKeyHex = seedPhraseToPrivateKey(seedPhrase)
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
// Use first 32 bytes of private key for secp256k1
val privateKeyBytes = privateKeyHex.take(64).chunked(2)
// Преобразуем hex в bytes (32 байта)
val privateKeyBytes = privateKeyHex.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
val privateKeyBigInt = BigInteger(1, privateKeyBytes)
// Generate public key from private key
// Генерируем публичный ключ из приватного
val publicKeyPoint = ecSpec.g.multiply(privateKeyBigInt)
val publicKeyHex = publicKeyPoint.getEncoded(false)
// ⚡ ВАЖНО: Используем СЖАТЫЙ формат (compressed=true) - 33 байта вместо 65
// Это совместимо с crypto_new где используется: secp256k1.getPublicKey(..., true)
val publicKeyHex = publicKeyPoint.getEncoded(true)
.joinToString("") { "%02x".format(it) }
val keyPair = KeyPairData(
privateKey = privateKeyHex.take(64),
privateKey = privateKeyHex,
publicKey = publicKeyHex
)

View File

@@ -81,6 +81,9 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
"[\\x{3030}]|[\\x{303D}]|" +
"[\\x{3297}]|[\\x{3299}]"
)
// 🔥 Паттерн для :emoji_XXXX: формата (как в десктопе)
val EMOJI_CODE_PATTERN: Pattern = Pattern.compile(":emoji_([a-fA-F0-9_-]+):")
// Кэш для bitmap и drawable
private val bitmapCache = LruCache<String, Bitmap>(500)
@@ -128,19 +131,46 @@ class AppleEmojiEditTextView @JvmOverloads constructor(
try {
val textStr = editable.toString()
val matcher = EMOJI_PATTERN.matcher(textStr)
val cursorPosition = selectionStart
// 🔥 Собираем все позиции эмодзи (и Unicode, и :emoji_code:)
data class EmojiMatch(val start: Int, val end: Int, val unified: String, val isCodeFormat: Boolean)
val emojiMatches = mutableListOf<EmojiMatch>()
// 1. Ищем :emoji_XXXX: формат
val codeMatcher = EMOJI_CODE_PATTERN.matcher(textStr)
while (codeMatcher.find()) {
val unified = codeMatcher.group(1) ?: continue
emojiMatches.add(EmojiMatch(codeMatcher.start(), codeMatcher.end(), unified, true))
}
// 2. Ищем реальные Unicode эмодзи
val matcher = EMOJI_PATTERN.matcher(textStr)
while (matcher.find()) {
val emoji = matcher.group()
val start = matcher.start()
val end = matcher.end()
// Проверяем что этот диапазон не перекрывается с :emoji_XXXX:
val overlaps = emojiMatches.any {
(start >= it.start && start < it.end) ||
(end > it.start && end <= it.end)
}
if (!overlaps) {
emojiMatches.add(EmojiMatch(start, end, emojiToUnified(emoji), false))
}
}
// 3. Обрабатываем все найденные эмодзи
for (match in emojiMatches) {
val start = match.start
val end = match.end
// Проверяем, есть ли уже ImageSpan
val existingSpans = editable.getSpans(start, end, ImageSpan::class.java)
if (existingSpans.isNotEmpty()) continue
val unified = emojiToUnified(emoji)
val unified = match.unified
var drawable = drawableCache.get(unified)
if (drawable == null) {

View File

@@ -522,7 +522,8 @@ fun EmojiButton(
interactionSource = interactionSource,
indication = null
) {
onClick(unifiedToEmoji(unified))
// 🔥 Отправляем эмодзи в формате :emoji_code: как в десктопе
onClick(":emoji_$unified:")
},
contentAlignment = Alignment.Center
) {

View File

@@ -391,7 +391,8 @@ private fun OptimizedEmojiButton(
indication = null, // 🚀 Убираем ripple
onClickLabel = "Select emoji"
) {
onClick(unifiedToEmoji(unified))
// 🔥 Отправляем эмодзи в формате :emoji_code: как в десктопе
onClick(":emoji_$unified:")
},
contentAlignment = Alignment.Center
) {

View File

@@ -0,0 +1,192 @@
/**
* Тест совместимости Android crypto с crypto_new (JavaScript)
*
* Этот файл проверяет, что Android и JavaScript генерируют одинаковые ключи
* из одинаковой seed phrase.
*
* Для запуска теста:
* 1. Запустите этот тест в Android приложении
* 2. Скопируйте seed phrase из логов
* 3. Запустите test-crypto-new-compat.js с той же seed phrase
* 4. Сравните результаты - они должны совпадать!
*/
package com.rosetta.messenger.crypto
import org.junit.Test
import org.junit.Assert.*
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
class CryptoNewCompatibilityTest {
init {
// Добавляем BouncyCastle provider
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(BouncyCastleProvider())
}
}
@Test
fun testKeyGenerationFromFixedSeedPhrase() {
// Фиксированная seed phrase для тестирования
val seedPhrase = listOf(
"abandon", "abandon", "abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon", "abandon", "about"
)
println("=== Crypto New Compatibility Test ===")
println("Seed phrase: ${seedPhrase.joinToString(" ")}")
println()
// Генерируем ключи
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
// Выводим результаты
println("Android Results:")
println("Private Key: ${keyPair.privateKey}")
println("Public Key: ${keyPair.publicKey}")
println("Private Key Hash: $privateKeyHash")
println()
// Проверяем формат
assertEquals("Private key should be 64 hex chars (32 bytes)", 64, keyPair.privateKey.length)
assertEquals("Public key should be 66 hex chars (33 bytes compressed)", 66, keyPair.publicKey.length)
assertEquals("Private key hash should be 64 hex chars", 64, privateKeyHash.length)
// Проверяем что публичный ключ сжатый (начинается с 02 или 03)
assertTrue(
"Public key should start with 02 or 03 (compressed format)",
keyPair.publicKey.startsWith("02") || keyPair.publicKey.startsWith("03")
)
println("✅ Format checks passed!")
println()
println("Now run this in JavaScript (crypto_new/crypto.ts):")
println("```javascript")
println("const { generateKeyPairFromSeed, generateHashFromPrivateKey } = require('./crypto');")
println("const seedPhrase = '${seedPhrase.joinToString(" ")}';")
println("const keyPair = await generateKeyPairFromSeed(seedPhrase);")
println("const hash = await generateHashFromPrivateKey(keyPair.privateKey);")
println("console.log('Private Key:', keyPair.privateKey);")
println("console.log('Public Key:', keyPair.publicKey);")
println("console.log('Hash:', hash);")
println("```")
println()
println("Expected JavaScript results:")
println("Private Key: ${keyPair.privateKey}")
println("Public Key: ${keyPair.publicKey}")
println("Hash: $privateKeyHash")
}
@Test
fun testMultipleSeedPhrases() {
val testCases = listOf(
listOf("test", "seed", "phrase", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"),
listOf("hello", "world", "crypto", "test", "android", "kotlin", "secp256k1", "sha256", "compressed", "public", "key", "format"),
listOf("abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about")
)
println("=== Multiple Seed Phrases Test ===")
testCases.forEachIndexed { index, seedPhrase ->
println("Test case ${index + 1}:")
println("Seed: ${seedPhrase.joinToString(" ")}")
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
val hash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
println("Private: ${keyPair.privateKey}")
println("Public: ${keyPair.publicKey}")
println("Hash: $hash")
// Verify format
assertEquals(64, keyPair.privateKey.length)
assertEquals(66, keyPair.publicKey.length)
assertTrue(keyPair.publicKey.startsWith("02") || keyPair.publicKey.startsWith("03"))
println("✅ Passed")
println()
}
}
@Test
fun testPrivateKeyHashGeneration() {
val privateKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
val hash = CryptoManager.generatePrivateKeyHash(privateKey)
println("=== Private Key Hash Test ===")
println("Private Key: $privateKey")
println("Hash (Android): $hash")
println()
println("JavaScript equivalent:")
println("```javascript")
println("const privateKey = '$privateKey';")
println("const hash = sha256.create().update(privateKey + 'rosetta').digest().toHex().toString();")
println("console.log('Hash (JS):', hash);")
println("```")
println()
println("Hashes should match!")
assertEquals(64, hash.length)
}
@Test
fun testPublicKeyCompression() {
// Тестируем что все публичные ключи действительно сжаты
val seedPhrases = listOf(
listOf("word", "word", "word", "word", "word", "word", "word", "word", "word", "word", "word", "word"),
listOf("test", "test", "test", "test", "test", "test", "test", "test", "test", "test", "test", "test"),
listOf("crypto", "crypto", "crypto", "crypto", "crypto", "crypto", "crypto", "crypto", "crypto", "crypto", "crypto", "crypto")
)
println("=== Public Key Compression Test ===")
seedPhrases.forEach { seedPhrase ->
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
// Проверяем что ключ сжатый (33 байта = 66 hex chars)
assertEquals("Public key must be compressed (33 bytes)", 66, keyPair.publicKey.length)
// Проверяем префикс (02 для четного Y, 03 для нечетного Y)
val prefix = keyPair.publicKey.substring(0, 2)
assertTrue(
"Compressed public key must start with 02 or 03",
prefix == "02" || prefix == "03"
)
println("${seedPhrase.joinToString(" ")}: ${keyPair.publicKey}")
}
println()
println("✅ All public keys are compressed!")
}
@Test
fun testCachingMechanism() {
val seedPhrase = listOf("cache", "test", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten")
println("=== Caching Test ===")
// Первый вызов - генерация
val start1 = System.currentTimeMillis()
val keyPair1 = CryptoManager.generateKeyPairFromSeed(seedPhrase)
val time1 = System.currentTimeMillis() - start1
// Второй вызов - из кэша
val start2 = System.currentTimeMillis()
val keyPair2 = CryptoManager.generateKeyPairFromSeed(seedPhrase)
val time2 = System.currentTimeMillis() - start2
println("First call (generation): ${time1}ms")
println("Second call (from cache): ${time2}ms")
println("Speedup: ${time1.toFloat() / time2.toFloat()}x")
// Проверяем что результаты идентичны
assertEquals(keyPair1.privateKey, keyPair2.privateKey)
assertEquals(keyPair1.publicKey, keyPair2.publicKey)
println("✅ Cache is working correctly!")
}
}