package com.rosetta.messenger.crypto 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 /** * 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() private val privateKeyHashCache = mutableMapOf() init { // Add BouncyCastle provider for secp256k1 support if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(BouncyCastleProvider()) } } /** * Generate a new 12-word BIP39 seed phrase */ fun generateSeedPhrase(): List { 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 */ fun validateSeedPhrase(words: List): Boolean { return try { val mnemonicCode = MnemonicCode.INSTANCE mnemonicCode.check(words) true } catch (e: MnemonicException) { false } } /** * Convert seed phrase to private key (64 bytes hex string) */ fun seedPhraseToPrivateKey(seedPhrase: List): String { val mnemonicCode = MnemonicCode.INSTANCE val seed = MnemonicCode.toSeed(seedPhrase, "") // Convert to hex string (128 characters for 64 bytes) return seed.joinToString("") { "%02x".format(it) } } /** * Generate key pair from seed phrase using secp256k1 curve * 🚀 ОПТИМИЗАЦИЯ: Кэшируем результаты для избежания повторных вычислений */ fun generateKeyPairFromSeed(seedPhrase: List): KeyPairData { val cacheKey = seedPhrase.joinToString(" ") // Проверяем кэш keyPairCache[cacheKey]?.let { return it } 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) .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) .joinToString("") { "%02x".format(it) } val keyPair = KeyPairData( privateKey = privateKeyHex.take(64), 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")) * 🚀 ОПТИМИЗАЦИЯ: Кэшируем хэши для избежания повторных вычислений */ fun generatePrivateKeyHash(privateKey: String): String { // Проверяем кэш 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" * - Iterations: 1000 * - Key size: 256 bit * - AES-256-CBC с PKCS5/PKCS7 padding * - Compression: zlib deflate (pako.deflate в JS) * - Формат: base64(iv):base64(ciphertext) */ fun encryptWithPassword(data: String, password: String): String { // Compress data (zlib deflate - совместимо с pako.deflate в JS) val compressed = compress(data.toByteArray(Charsets.UTF_8)) // 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 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 * - Key size: 256 bit * - AES-256-CBC с PKCS5/PKCS7 padding * - Decompression: zlib inflate (pako.inflate в JS) * - Формат: base64(iv):base64(ciphertext) */ fun decryptWithPassword(encryptedData: String, password: String): String? { return try { 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) // Derive key using PBKDF2-HMAC-SHA1 (⚠️ SHA1, не SHA256!) val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") 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") // 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 } } /** * RAW Deflate сжатие (без zlib header) * * ⚠️ ВАЖНО: nowrap=true для совместимости с pako.deflate() в JS! * - pako.deflate() создаёт RAW deflate поток (без 2-byte zlib header) * - Java Deflater() по умолчанию создаёт zlib поток (с header 78 9C) * - Поэтому используем Deflater(level, true) где true = nowrap */ private fun compress(data: ByteArray): ByteArray { // nowrap=true = RAW deflate (совместимо с pako.deflate) val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, true) deflater.setInput(data) deflater.finish() val outputStream = ByteArrayOutputStream() val buffer = ByteArray(1024) while (!deflater.finished()) { val count = deflater.deflate(buffer) outputStream.write(buffer, 0, count) } deflater.end() // Освобождаем ресурсы outputStream.close() return outputStream.toByteArray() } /** * RAW Inflate декомпрессия (без zlib header) * * ⚠️ ВАЖНО: nowrap=true для совместимости с pako.inflate() в JS! * - pako.inflate() ожидает RAW deflate поток * - Java Inflater() по умолчанию ожидает zlib поток (с header) * - Поэтому используем Inflater(true) где true = nowrap */ private fun decompress(data: ByteArray): ByteArray { // nowrap=true = RAW inflate (совместимо с pako.inflate) val inflater = Inflater(true) inflater.setInput(data) val outputStream = ByteArrayOutputStream() val buffer = ByteArray(1024) while (!inflater.finished()) { val count = inflater.inflate(buffer) outputStream.write(buffer, 0, count) } inflater.end() // Освобождаем ресурсы outputStream.close() return outputStream.toByteArray() } } data class KeyPairData( val privateKey: String, val publicKey: String )