feat: Update authorization logic for compatibility with crypto_new; enhance key generation and public key format
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -522,7 +522,8 @@ fun EmojiButton(
|
||||
interactionSource = interactionSource,
|
||||
indication = null
|
||||
) {
|
||||
onClick(unifiedToEmoji(unified))
|
||||
// 🔥 Отправляем эмодзи в формате :emoji_code: как в десктопе
|
||||
onClick(":emoji_$unified:")
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
@@ -391,7 +391,8 @@ private fun OptimizedEmojiButton(
|
||||
indication = null, // 🚀 Убираем ripple
|
||||
onClickLabel = "Select emoji"
|
||||
) {
|
||||
onClick(unifiedToEmoji(unified))
|
||||
// 🔥 Отправляем эмодзи в формате :emoji_code: как в десктопе
|
||||
onClick(":emoji_$unified:")
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user