feat: Implement new encryption algorithms ECDH and XChaCha20-Poly1305 with chunking support

This commit is contained in:
k1ngsterr1
2026-01-15 00:53:32 +05:00
parent a079d5fffa
commit dfc6d3f462
3 changed files with 639 additions and 23 deletions

View File

@@ -18,6 +18,7 @@ import java.security.spec.PKCS8EncodedKeySpec
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.Inflater
import com.google.crypto.tink.subtle.XChaCha20Poly1305
/**
* Cryptography module for Rosetta Messenger
@@ -147,34 +148,72 @@ object CryptoManager {
* - Key size: 256 bit
* - AES-256-CBC с PKCS5/PKCS7 padding
* - Compression: zlib deflate (pako.deflate в JS)
* - Формат: base64(iv):base64(ciphertext)
* - Chunking для данных > 10MB
* - Формат single chunk: base64(iv):base64(ciphertext)
* - Формат chunked: "CHNK:" + chunks joined by "::"
*/
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")
val CHUNK_SIZE = 10 * 1024 * 1024 // 10MB
// 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"
// 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 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 {
// Single chunk (original behavior)
// 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"
}
}
/**
@@ -187,10 +226,69 @@ object CryptoManager {
* - Key size: 256 bit
* - AES-256-CBC с PKCS5/PKCS7 padding
* - Decompression: zlib inflate (pako.inflate в JS)
* - Формат: base64(iv):base64(ciphertext)
* - Supports old format (base64-encoded hex "iv:ciphertext")
* - Supports new format (base64 "iv:ciphertext")
* - Supports chunked format ("CHNK:" + chunks joined by "::")
*/
fun decryptWithPassword(encryptedData: String, password: String): String? {
return try {
// 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()
// 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 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)
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)
// 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 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)
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
@@ -215,6 +313,20 @@ object CryptoManager {
}
}
/**
* Check if data is in old format (base64-encoded hex with ":")
*/
private fun isOldFormat(data: String): Boolean {
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' }
}
} catch (e: Exception) {
false
}
}
/**
* RAW Deflate сжатие (без zlib header)
*
@@ -263,9 +375,186 @@ 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)
* 3. Use x-coordinate of shared point as AES key
* 4. Encrypt data with AES-256-CBC
* 5. Return: base64(iv:ciphertext:ephemeralPrivateKey)
*/
fun encrypt(data: String, publicKeyHex: String): String {
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
val keyPairGenerator = KeyPairGenerator.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME)
keyPairGenerator.initialize(ecSpec, SecureRandom())
// Generate ephemeral key pair
val ephemeralKeyPair = keyPairGenerator.generateKeyPair()
val ephemeralPrivateKey = ephemeralKeyPair.private as org.bouncycastle.jce.interfaces.ECPrivateKey
val ephemeralPublicKey = ephemeralKeyPair.public as org.bouncycastle.jce.interfaces.ECPublicKey
// Parse recipient's public key
val recipientPublicKeyBytes = publicKeyHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
val recipientPublicKeyPoint = ecSpec.curve.decodePoint(recipientPublicKeyBytes)
val recipientPublicKeySpec = ECPublicKeySpec(recipientPublicKeyPoint, ecSpec)
val keyFactory = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME)
val recipientPublicKey = keyFactory.generatePublic(recipientPublicKeySpec)
// Compute shared secret using ECDH
val keyAgreement = javax.crypto.KeyAgreement.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME)
keyAgreement.init(ephemeralPrivateKey)
keyAgreement.doPhase(recipientPublicKey, 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")
// 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))
// Get ephemeral private key bytes
val ephemeralPrivateKeyBytes = ephemeralPrivateKey.d.toByteArray()
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
* 3. Compute shared secret using ECDH (privateKey × ephemeralPublicKey)
* 4. Use x-coordinate of shared point as AES key
* 5. Decrypt data with AES-256-CBC
*/
fun decrypt(encryptedData: String, privateKeyHex: String): String? {
return try {
// Decode base64
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 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 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)
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)
* }
*/
fun chacha20Encrypt(data: String): ChaCha20Result {
// Generate random key (32 bytes) and nonce (24 bytes)
val key = ByteArray(32)
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) }
)
}
/**
* Decrypt data using XChaCha20-Poly1305
*
* @param ciphertextHex Hex-encoded ciphertext
* @param nonceHex Hex-encoded nonce (24 bytes)
* @param keyHex Hex-encoded key (32 bytes)
*/
fun chacha20Decrypt(ciphertextHex: String, nonceHex: String, keyHex: String): String? {
return try {
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
}
}
}
data class KeyPairData(
val privateKey: String,
val publicKey: String
)
data class ChaCha20Result(
val ciphertext: String,
val nonce: String,
val key: String
)