272 lines
10 KiB
Kotlin
272 lines
10 KiB
Kotlin
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<String, KeyPairData>()
|
||
private val privateKeyHashCache = mutableMapOf<String, String>()
|
||
|
||
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<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
|
||
*/
|
||
fun validateSeedPhrase(words: List<String>): 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>): 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<String>): 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
|
||
)
|