@@ -1,94 +1,95 @@
package com.rosetta.messenger.crypto
import android.util.Base64
import com.google.crypto.tink.subtle.XChaCha20Poly1305
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.*
import java.util.concurrent.ConcurrentHashMap
import java.util.zip.Deflater
import java.util.zip.Inflater
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
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
import java.util.concurrent.ConcurrentHashMap
import com.google.crypto.tink.subtle.XChaCha20Poly1305
/**
* Cryptography module for Rosetta Messenger
* Implements BIP39 seed phrase generation and secp256k1 key derivation
* 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 > ( )
// 🚀 КРИТИЧЕСКАЯ ОПТИМИЗАЦИЯ: Кэш для PBKDF2 производных ключей
// PBKDF2 с 1000 итерациями - очень тяжелая операция!
// Кэшируем производный ключ для каждого пароля чтобы не вычислять е г о каждый раз
private val pbkdf2KeyCache = mutableMapOf < String , SecretKeySpec > ( )
// 🚀 ОПТИМИЗАЦИЯ: Lock-free кэш для расшифрованных сообщений
// ConcurrentHashMap вместо synchronized LinkedHashMap — убирает ко нтention при параллельной расшифровке
// ConcurrentHashMap вместо synchronized LinkedHashMap — убирает ко нтention при параллельной
// расшифровке
private const val DECRYPTION _CACHE _SIZE = 2000
private val decryptionCache = ConcurrentHashMap < String , String > ( DECRYPTION _CACHE _SIZE , 0.75f , 4 )
init {
// Add BouncyCastle provider for secp256k1 support
if ( Security . getProvider ( BouncyCastleProvider . PROVIDER _NAME ) == null ) {
Security . addProvider ( BouncyCastleProvider ( ) )
}
}
/**
* 🚀 Получить или вычислить PBKDF2 ключ для пароля (кэшируется)
* 🚀 Получить или вычислить PBKDF2 ключ для пароля (кэшируется) Public для pre-warm при логине
* (чтобы кэш был горячий к моменту дешифровки)
*/
private fun getPbkdf2Key ( password : String ) : SecretKeySpec {
fun getPbkdf2Key ( password : String ) : SecretKeySpec {
return pbkdf2KeyCache . getOrPut ( password ) {
val factory = SecretKeyFactory . getInstance ( " PBKDF2WithHmacSHA1 " )
val spec = PBEKeySpec ( password . toCharArray ( ) , SALT . toByteArray ( Charsets . UTF _8 ) , PBKDF2 _ITERATIONS , KEY _SIZE )
val spec =
PBEKeySpec (
password . toCharArray ( ) ,
SALT . toByteArray ( Charsets . UTF _8 ) ,
PBKDF2 _ITERATIONS ,
KEY _SIZE
)
val secretKey = factory . generateSecret ( spec )
SecretKeySpec ( secretKey . encoded , " AES " )
}
}
/**
* 🧹 Очистить кэши при logout
*/
/** 🧹 Очистить кэши при logout */
fun clearCaches ( ) {
pbkdf2KeyCache . clear ( )
decryptionCache . clear ( )
keyPairCache . clear ( )
privateKeyHashCache . clear ( )
}
/**
* Generate a new 12-word BIP39 seed phrase
*/
/** 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
*/
/** Validate a seed phrase */
fun validateSeedPhrase ( words : List < String > ) : Boolean {
return try {
val mnemonicCode = MnemonicCode . INSTANCE
@@ -98,15 +99,15 @@ object CryptoManager {
false
}
}
/**
* Convert seed phrase to private key (32 bytes hex string)
*
*
* Алгоритм (совместим с Desktop desktop-rosetta):
* 1. BIP39 mnemonicToSeed(phrase, "") → 64 байта (PBKDF2-SHA512, 2048 итераций)
* 2. Конвертация seed в hex-строку (128 символов)
* 3. SHA256(hexSeed) → 32 байта privateKey
*
*
* Desktop эквивалент (SetPassword.tsx + crypto.ts):
* ```js
* let seed = await mnemonicToSeed(phrase); // BIP39 → 64 bytes
@@ -118,96 +119,94 @@ object CryptoManager {
// Step 1: BIP39 mnemonicToSeed — PBKDF2-SHA512 with 2048 iterations
// passphrase = "" (empty), salt = "mnemonic" + passphrase
val bip39Seed = MnemonicCode . toSeed ( seedPhrase , " " )
// Step 2: Convert to hex string (128 chars for 64 bytes)
val hexSeed = bip39Seed . joinToString ( " " ) { " %02x " . format ( it ) }
// Step 3: SHA256(hexSeed) — matches Desktop's sha256.create().update(hex).digest()
val digest = MessageDigest . getInstance ( " SHA-256 " )
val hash = digest . digest ( hexSeed . toByteArray ( Charsets . UTF _8 ) )
return hash . joinToString ( " " ) { " %02x " . format ( it ) }
}
/**
* Generate key pair from seed phrase using secp256k1 curve
*
*
* Алгоритм (совместим с Desktop desktop-rosetta):
* - privateKey = SHA256(hex(BIP39_seed(mnemonic))) - 32 байта
* - publicKey = secp256k1.getPublicKey(privateKey, compressed=true) - 33 байта
*
*
* Desktop эквивалент (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 {
val cacheKey = seedPhrase . joinToString ( " " )
// Проверяем кэш
keyPairCache [ cacheKey ] ?. let { return it }
keyPairCache [ cacheKey ] ?. let {
return it
}
// Генерируем приватный ключ через SHA256
val privateKeyHex = seedPhraseToPrivateKey ( seedPhrase )
val ecSpec = ECNamedCurveTable . getParameterSpec ( " secp256k1 " )
// Преобразуем hex в bytes (32 байта)
val privateKeyBytes = privateKeyHex . chunked ( 2 )
. map { it . toInt ( 16 ) . toByte ( ) }
. toByteArray ( )
val privateKeyBytes = privateKeyHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val privateKeyBigInt = BigInteger ( 1 , privateKeyBytes )
// Генерируем публичный ключ из приватного
val publicKeyPoint = ecSpec . g . multiply ( privateKeyBigInt )
// ⚡ ВАЖНО: Используем СЖАТЫЙ формат (compressed=true) - 33 байта вместо 65
// Это совместимо с crypto_new где используется: secp256k1.getPublicKey(..., true)
val publicKeyHex = publicKeyPoint . getEncoded ( true )
. joinToString ( " " ) { " %02x " . format ( it ) }
val keyPair = KeyPairData (
privateKey = privateKeyHex ,
publicKey = publicKeyHex
)
val publicKeyHex = publicKeyPoint . getEncoded ( true ) . joinToString ( " " ) { " %02x " . format ( it ) }
val keyPair = KeyPairData ( privateKey = privateKeyHex , 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"))
* 🚀 ОПТИМИЗАЦИЯ: Кэшируем хэши для избежания повторных вычислений
* Generate private key hash for protocol (SHA256(privateKey + "rosetta")) 🚀 ОПТИМИЗАЦИЯ:
* Кэшируем хэши для избежания повторных вычислений
*/
fun generatePrivateKeyHash ( privateKey : String ) : String {
// Проверяем кэш
privateKeyHashCache [ privateKey ] ?. let { return it }
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"
@@ -222,38 +221,44 @@ object CryptoManager {
fun encryptWithPassword ( data : String , password : String ) : String {
// Compress data (zlib deflate - совместимо с pako.deflate в JS)
val compressed = compress ( data . toByteArray ( Charsets . UTF _8 ) )
val CHUNK _SIZE = 10 * 1024 * 1024 // 10MB
// Check if we need chunking
if ( compressed . size > CHUNK _SIZE ) {
// Chunk the compressed data
val chunks = compressed . toList ( ) . chunked ( CHUNK _SIZE ) . map { it . toByteArray ( ) }
val encryptedChunks = mutableListOf < String > ( )
for ( chunk in chunks ) {
// Derive key using PBKDF2-HMAC-SHA1
val factory = SecretKeyFactory . getInstance ( " PBKDF2WithHmacSHA1 " )
val spec = PBEKeySpec ( password . toCharArray ( ) , SALT . toByteArray ( Charsets . UTF _8 ) , PBKDF2 _ITERATIONS , KEY _SIZE )
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 ( chunk )
// Store as ivBase64:ctBase64
val ivBase64 = Base64 . encodeToString ( iv , Base64 . NO _WRAP )
val ctBase64 = Base64 . encodeToString ( encrypted , Base64 . NO _WRAP )
encryptedChunks . add ( " $ivBase64 : $ctBase64 " )
}
// Return chunked format: "CHNK:" + chunks joined by "::"
return " CHNK: " + encryptedChunks . joinToString ( " :: " )
} else {
@@ -261,52 +266,60 @@ object CryptoManager {
// 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 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
* - Iterations: 1000
* - Key size: 256 bit
* - AES-256-CBC с PKCS5/PKCS7 padding
* - Decompression: zlib inflate (pako.inflate в JS)
* - Supports old format (base64-encoded hex "iv:ciphertext")
* - Supports new format (base64 "iv:ciphertext")
* - Supports chunked format ("CHNK:" + chunks joined by "::")
*
*
* 🚀 ОПТИМИЗАЦИЯ: Кэширование PBKDF2 ключа и расшифрованных сообщений
*/
fun decryptWithPassword ( encryptedData : String , password : String ) : String ? {
// 🚀 ОПТИМИЗАЦИЯ: Lock-free проверка кэша (ConcurrentHashMap)
val cacheKey = " $password : $encryptedData "
decryptionCache [ cacheKey ] ?. let { return it }
decryptionCache [ cacheKey ] ?. let {
return it
}
return try {
val result = decryptWithPasswordInternal ( encryptedData , password )
// 🚀 Сохраняем в кэш (lock-free)
if ( result != null ) {
// Ограничиваем размер кэша
@@ -317,106 +330,103 @@ object CryptoManager {
}
decryptionCache [ cacheKey ] = result
}
result
} catch ( e : Exception ) {
null
}
}
/**
* 🔐 Внутренняя функция расшифровки (без кэширования результата)
*/
/** 🔐 Внутренняя функция расшифровки (без кэширования результата) */
private fun decryptWithPasswordInternal ( encryptedData : String , password : String ) : String ? {
return try {
// 🚀 Получаем кэшированный PBKDF2 ключ
val key = getPbkdf2Key ( password )
// Check for old format: base64-encoded string containing hex
if ( isOldFormat ( encryptedData ) ) {
val decoded = String ( Base64 . decode ( encryptedData , Base64 . NO _WRAP ) , Charsets . UTF _8 )
val parts = decoded . split ( " : " )
if ( parts . size != 2 ) return null
val iv = parts [ 0 ] . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val ciphertext = parts [ 1 ] . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
// 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 )
return String ( decrypted , Charsets . UTF _8 )
}
// Check for chunked format
if ( encryptedData . startsWith ( " CHNK: " ) ) {
val chunkStrings = encryptedData . substring ( 5 ) . split ( " :: " )
val decompressedParts = mutableListOf < ByteArray > ( )
for ( chunkString in chunkStrings ) {
val parts = chunkString . 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 )
// 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 )
decompressedParts . add ( decrypted )
}
// Concatenate all decrypted chunks
val allBytes = decompressedParts . fold ( ByteArray ( 0 ) ) { acc , arr -> acc + arr }
// Decompress the concatenated data
return String ( decompress ( allBytes ) , Charsets . UTF _8 )
}
// New format: base64 "iv:ciphertext"
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 )
// 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
}
}
/**
* Check if data is in old format (base64-encoded hex with ":")
*/
/** Check if data is in old format (base64-encoded hex with ":") */
private fun isOldFormat ( data : String ) : Boolean {
// 🚀 Fast path: new format always contains ':' at plaintext level (iv:ct)
// Old format is a single base64 blob without ':' in the encoded string
if ( data . contains ( ':' ) ) return false
if ( data . startsWith ( " CHNK: " ) ) return false
return try {
val decoded = String ( Base64 . decode ( data , Base64 . NO _WRAP ) , Charsets . UTF _8 )
decoded . contains ( " : " ) && decoded . split ( " : " ) . all { part ->
part . all { it in '0' .. '9' || it in 'a' .. 'f' || it in 'A' .. 'F' }
}
decoded . contains ( " : " ) &&
decoded . split ( " : " ) . all { part ->
part . all { it in '0' .. '9' || it in 'a' .. 'f' || it in 'A' .. 'F' }
}
} catch ( e : Exception ) {
false
}
}
/**
* RAW Deflate сжатие (без zlib header)
*
*
* ⚠️ ВАЖНО: nowrap=true для совместимости с pako.deflate() в JS!
* - pako.deflate() создаёт RAW deflate поток (без 2-byte zlib header)
* - Java Deflater() по умолчанию создаёт zlib поток (с header 78 9C)
@@ -427,7 +437,7 @@ object CryptoManager {
val deflater = Deflater ( Deflater . DEFAULT _COMPRESSION , true )
deflater . setInput ( data )
deflater . finish ( )
val outputStream = ByteArrayOutputStream ( )
val buffer = ByteArray ( 1024 )
while ( ! deflater . finished ( ) ) {
@@ -438,10 +448,10 @@ object CryptoManager {
outputStream . close ( )
return outputStream . toByteArray ( )
}
/**
* RAW Inflate декомпрессия (без zlib header)
*
*
* ⚠️ ВАЖНО: nowrap=true для совместимости с pako.inflate() в JS!
* - pako.inflate() ожидает RAW deflate поток
* - Java Inflater() по умолчанию ожидает zlib поток (с header)
@@ -451,7 +461,7 @@ object CryptoManager {
// nowrap=true = RAW inflate (совместимо с pako.inflate)
val inflater = Inflater ( true )
inflater . setInput ( data )
val outputStream = ByteArrayOutputStream ( )
val buffer = ByteArray ( 1024 )
while ( !in flater . finished ( ) ) {
@@ -462,10 +472,10 @@ object CryptoManager {
outputStream . close ( )
return outputStream . toByteArray ( )
}
/**
* Encrypt data using ECDH + AES
*
*
* Algorithm:
* 1. Generate ephemeral key pair
* 2. Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey)
@@ -475,61 +485,67 @@ object CryptoManager {
*/
fun encrypt ( data : String , publicKeyHex : String ) : String {
val ecSpec = ECNamedCurveTable . getParameterSpec ( " secp256k1 " )
// Generate ephemeral private key (random 32 bytes)
val ephemeralPrivateKeyBytes = ByteArray ( 32 )
SecureRandom ( ) . nextBytes ( ephemeralPrivateKeyBytes )
val ephemeralPrivateKeyBigInt = BigInteger ( 1 , ephemeralPrivateKeyBytes )
// Generate ephemeral public key from private key
val ephemeralPublicKeyPoint = ecSpec . g . multiply ( ephemeralPrivateKeyBigInt )
// Parse recipient's public key
val recipientPublicKeyBytes = publicKeyHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val recipientPublicKeyBytes =
publicKeyHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val recipientPublicKeyPoint = ecSpec . curve . decodePoint ( recipientPublicKeyBytes )
// Compute shared secret using ECDH (ephemeralPrivateKey × recipientPublicKey)
val sharedPoint = recipientPublicKeyPoint . multiply ( ephemeralPrivateKeyBigInt ) . normalize ( )
// Use x-coordinate of shared point as AES key (32 bytes)
val sharedKeyBytes = sharedPoint . affineXCoord . encoded
val sharedKey = if ( sharedKeyBytes . size >= 32 ) {
sharedKeyBytes . copyOfRange ( sharedKeyBytes . size - 32 , sharedKeyBytes . size )
} else {
// Pad with leading zeros if needed
ByteArray ( 32 - sharedKeyBytes . size ) + sharedKeyBytes
}
val sharedKey =
if ( sharedKeyBytes . size >= 32 ) {
sharedKeyBytes . copyOfRange ( sharedKeyBytes . size - 32 , sharedKeyBytes . size )
} else {
// Pad with leading zeros if needed
ByteArray ( 32 - sharedKeyBytes . size ) + sharedKeyBytes
}
val key = SecretKeySpec ( sharedKey , " 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 ( data . toByteArray ( Charsets . UTF _8 ) )
// Normalize ephemeral private key to 32 bytes
val normalizedPrivateKey = if ( ephemeralPrivateKeyBytes . size > 32 ) {
ephemeralPrivateKeyBytes . copyOfRange ( ephemeralPrivateKeyBytes . size - 32 , ephemeralPrivateKeyBytes . size )
} else {
ephemeralPrivateKeyBytes
}
val normalizedPrivateKey =
if ( ephemeralPrivateKeyBytes . size > 32 ) {
ephemeralPrivateKeyBytes . copyOfRange (
ephemeralPrivateKeyBytes . size - 32 ,
ephemeralPrivateKeyBytes . size
)
} else {
ephemeralPrivateKeyBytes
}
// Return base64(iv:ciphertext:ephemeralPrivateKey)
val ivHex = iv . joinToString ( " " ) { " %02x " . format ( it ) }
val ctHex = encrypted . joinToString ( " " ) { " %02x " . format ( it ) }
val ephemeralPrivateKeyHex = normalizedPrivateKey . joinToString ( " " ) { " %02x " . format ( it ) }
val combined = " $ivHex : $ctHex : $ephemeralPrivateKeyHex "
return Base64 . encodeToString ( combined . toByteArray ( Charsets . UTF _8 ) , Base64 . NO _WRAP )
}
/**
* Decrypt data using ECDH + AES
*
*
* Algorithm:
* 1. Parse iv, ciphertext, and ephemeralPrivateKey from base64
* 2. Compute ephemeral public key from ephemeral private key
@@ -543,55 +559,57 @@ object CryptoManager {
val decoded = String ( Base64 . decode ( encryptedData , Base64 . NO _WRAP ) , Charsets . UTF _8 )
val parts = decoded . split ( " : " )
if ( parts . size != 3 ) return null
val iv = parts [ 0 ] . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val ciphertext = parts [ 1 ] . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val ephemeralPrivateKeyBytes = parts [ 2 ] . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val ephemeralPrivateKeyBytes =
parts [ 2 ] . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val ecSpec = ECNamedCurveTable . getParameterSpec ( " secp256k1 " )
// Compute ephemeral public key from ephemeral private key
val ephemeralPrivateKeyBigInt = BigInteger ( 1 , ephemeralPrivateKeyBytes )
val ephemeralPublicKeyPoint = ecSpec . g . multiply ( ephemeralPrivateKeyBigInt )
val ephemeralPublicKeySpec = ECPublicKeySpec ( ephemeralPublicKeyPoint , ecSpec )
val keyFactory = KeyFactory . getInstance ( " ECDH " , BouncyCastleProvider . PROVIDER _NAME )
val ephemeralPublicKey = keyFactory . generatePublic ( ephemeralPublicKeySpec )
// Parse private key
val privateKeyBytes = privateKeyHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val privateKeyBytes =
privateKeyHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val privateKeyBigInt = BigInteger ( 1 , privateKeyBytes )
val privateKeySpec = ECPrivateKeySpec ( privateKeyBigInt , ecSpec )
val privateKey = keyFactory . generatePrivate ( privateKeySpec )
// Compute shared secret using ECDH
val keyAgreement = javax . crypto . KeyAgreement . getInstance ( " ECDH " , BouncyCastleProvider . PROVIDER _NAME )
val keyAgreement =
javax . crypto . KeyAgreement . getInstance (
" ECDH " ,
BouncyCastleProvider . PROVIDER _NAME
)
keyAgreement . init ( privateKey )
keyAgreement . doPhase ( ephemeralPublicKey , true )
val sharedSecret = keyAgreement . generateSecret ( )
// Use first 32 bytes (x-coordinate) as AES key
val sharedKey = sharedSecret . copyOfRange ( 1 , 33 )
val key = SecretKeySpec ( sharedKey , " 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 )
String ( decrypted , Charsets . UTF _8 )
} catch ( e : Exception ) {
null
}
}
/**
* Encrypt data using XChaCha20-Poly1305
*
* Returns: {
* ciphertext: hex string,
* nonce: hex string (24 bytes),
* key: hex string (32 bytes)
* }
*
* Returns: { ciphertext: hex string, nonce: hex string (24 bytes), key: hex string (32 bytes) }
*/
fun chacha20Encrypt ( data : String ) : ChaCha20Result {
// Generate random key (32 bytes) and nonce (24 bytes)
@@ -599,22 +617,22 @@ object CryptoManager {
val nonce = ByteArray ( 24 )
SecureRandom ( ) . nextBytes ( key )
SecureRandom ( ) . nextBytes ( nonce )
// Encrypt using XChaCha20-Poly1305
val cipher = XChaCha20Poly1305 ( key )
val plaintext = data . toByteArray ( Charsets . UTF _8 )
val ciphertext = cipher . encrypt ( nonce , plaintext )
return ChaCha20Result (
ciphertext = ciphertext . joinToString ( " " ) { " %02x " . format ( it ) } ,
nonce = nonce . joinToString ( " " ) { " %02x " . format ( it ) } ,
key = key . joinToString ( " " ) { " %02x " . format ( it ) }
ciphertext = ciphertext . joinToString ( " " ) { " %02x " . format ( it ) } ,
nonce = nonce . joinToString ( " " ) { " %02x " . format ( it ) } ,
key = key . joinToString ( " " ) { " %02x " . format ( it ) }
)
}
/**
* Decrypt data using XChaCha20-Poly1305
*
*
* @param ciphertextHex Hex-encoded ciphertext
* @param nonceHex Hex-encoded nonce (24 bytes)
* @param keyHex Hex-encoded key (32 bytes)
@@ -624,10 +642,10 @@ object CryptoManager {
val ciphertext = ciphertextHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val nonce = nonceHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val key = keyHex . chunked ( 2 ) . map { it . toInt ( 16 ) . toByte ( ) } . toByteArray ( )
val cipher = XChaCha20Poly1305 ( key )
val decrypted = cipher . decrypt ( nonce , ciphertext )
String ( decrypted , Charsets . UTF _8 )
} catch ( e : Exception ) {
null
@@ -635,13 +653,6 @@ object CryptoManager {
}
}
data class KeyPairData (
val privateKey : String ,
val publicKey : String
)
data class KeyPairData ( val privateKey : String , val publicKey : String )
data class ChaCha20Result (
val ciphertext : String ,
val nonce : String ,
val key : String
)
data class ChaCha20Result ( val ciphertext : String , val nonce : String , val key : String )