Files
mobile-android/app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt

272 lines
10 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
)