feat: Implement XChaCha20-Poly1305 encryption and decryption in MessageCrypto for enhanced security

This commit is contained in:
k1ngsterr1
2026-01-10 22:34:59 +05:00
parent 9baa7f444a
commit 7216cc0d0b

View File

@@ -1,6 +1,10 @@
package com.rosetta.messenger.crypto package com.rosetta.messenger.crypto
import android.util.Base64 import android.util.Base64
import org.bouncycastle.crypto.engines.ChaCha7539Engine
import org.bouncycastle.crypto.macs.Poly1305
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.math.BigInteger import java.math.BigInteger
@@ -8,7 +12,6 @@ import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.security.Security import java.security.Security
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@@ -19,9 +22,8 @@ import javax.crypto.spec.SecretKeySpec
object MessageCrypto { object MessageCrypto {
private const val CHACHA_KEY_SIZE = 32 private const val CHACHA_KEY_SIZE = 32
private const val CHACHA_NONCE_SIZE = 24 private const val XCHACHA_NONCE_SIZE = 24
private const val AES_KEY_SIZE = 32 private const val POLY1305_TAG_SIZE = 16
private const val GCM_TAG_LENGTH = 128
init { init {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
@@ -33,58 +35,242 @@ object MessageCrypto {
* Результат шифрования сообщения * Результат шифрования сообщения
*/ */
data class EncryptedMessage( data class EncryptedMessage(
val ciphertext: String, // Hex-encoded ChaCha20 ciphertext val ciphertext: String, // Hex-encoded XChaCha20-Poly1305 ciphertext
val key: String, // Hex-encoded 32-byte key val key: String, // Hex-encoded 32-byte key
val nonce: String // Hex-encoded 24-byte nonce val nonce: String // Hex-encoded 24-byte nonce
) )
/** /**
* Шифрование текста сообщения с использованием AES-GCM * XChaCha20-Poly1305 шифрование (совместимо с @noble/ciphers в RN)
* (Аналог ChaCha20-Poly1305 для совместимости с Android) *
* XChaCha20-Poly1305 использует 24-byte nonce:
* - Первые 16 байт nonce используются в HChaCha20 для derive subkey
* - Последние 8 байт nonce используются как counter для ChaCha20
*/ */
fun encryptMessage(plaintext: String): EncryptedMessage { fun encryptMessage(plaintext: String): EncryptedMessage {
val secureRandom = SecureRandom() val secureRandom = SecureRandom()
// Генерируем случайный ключ (32 байта) и nonce (12 байт для GCM) // Генерируем случайный ключ (32 байта) и nonce (24 байта для XChaCha20)
val key = ByteArray(CHACHA_KEY_SIZE) val key = ByteArray(CHACHA_KEY_SIZE)
val nonce = ByteArray(12) // GCM использует 12 байт nonce val nonce = ByteArray(XCHACHA_NONCE_SIZE)
secureRandom.nextBytes(key) secureRandom.nextBytes(key)
secureRandom.nextBytes(nonce) secureRandom.nextBytes(nonce)
// Шифруем AES-GCM val ciphertext = xchacha20Poly1305Encrypt(plaintext.toByteArray(Charsets.UTF_8), key, nonce)
val secretKey = SecretKeySpec(key, "AES")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, GCMParameterSpec(GCM_TAG_LENGTH, nonce))
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
// Дополняем nonce до 24 байт для совместимости с RN форматом
val fullNonce = ByteArray(CHACHA_NONCE_SIZE)
System.arraycopy(nonce, 0, fullNonce, 0, nonce.size)
return EncryptedMessage( return EncryptedMessage(
ciphertext = ciphertext.toHex(), ciphertext = ciphertext.toHex(),
key = key.toHex(), key = key.toHex(),
nonce = fullNonce.toHex() nonce = nonce.toHex()
) )
} }
/** /**
* Расшифровка текста сообщения * XChaCha20-Poly1305 encrypt implementation
*/
private fun xchacha20Poly1305Encrypt(plaintext: ByteArray, key: ByteArray, nonce: ByteArray): ByteArray {
// Step 1: Derive subkey using HChaCha20
val subkey = hchacha20(key, nonce.copyOfRange(0, 16))
// Step 2: Create ChaCha20 nonce (4 zeros + last 8 bytes of original nonce)
val chacha20Nonce = ByteArray(12)
// First 4 bytes are 0
System.arraycopy(nonce, 16, chacha20Nonce, 4, 8)
// Step 3: Encrypt with ChaCha20
val engine = ChaCha7539Engine()
engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
val ciphertext = ByteArray(plaintext.size)
engine.processBytes(plaintext, 0, plaintext.size, ciphertext, 0)
// Step 4: Generate Poly1305 tag
// For AEAD, we need to compute Poly1305 over AAD + ciphertext with proper padding
val poly1305Key = ByteArray(32)
val zeros = ByteArray(32)
val polyKeyEngine = ChaCha7539Engine()
polyKeyEngine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
polyKeyEngine.processBytes(zeros, 0, 32, poly1305Key, 0)
val mac = Poly1305()
mac.init(KeyParameter(poly1305Key))
// No AAD in our case, just process ciphertext
mac.update(ciphertext, 0, ciphertext.size)
// Pad to 16 bytes
val padding = (16 - (ciphertext.size % 16)) % 16
if (padding > 0) {
mac.update(ByteArray(padding), 0, padding)
}
// Length of AAD (0) as 64-bit LE
mac.update(ByteArray(8), 0, 8)
// Length of ciphertext as 64-bit LE
val ctLen = longToLittleEndian(ciphertext.size.toLong())
mac.update(ctLen, 0, 8)
val tag = ByteArray(POLY1305_TAG_SIZE)
mac.doFinal(tag, 0)
// Return ciphertext + tag
return ciphertext + tag
}
/**
* HChaCha20 - derives a 256-bit subkey from a 256-bit key and 128-bit nonce
*/
private fun hchacha20(key: ByteArray, nonce: ByteArray): ByteArray {
val state = IntArray(16)
// Constants "expand 32-byte k"
state[0] = 0x61707865
state[1] = 0x3320646e
state[2] = 0x79622d32
state[3] = 0x6b206574
// Key
for (i in 0..7) {
state[4 + i] = littleEndianToInt(key, i * 4)
}
// Nonce
for (i in 0..3) {
state[12 + i] = littleEndianToInt(nonce, i * 4)
}
// 20 rounds
for (i in 0 until 10) {
// Column rounds
quarterRound(state, 0, 4, 8, 12)
quarterRound(state, 1, 5, 9, 13)
quarterRound(state, 2, 6, 10, 14)
quarterRound(state, 3, 7, 11, 15)
// Diagonal rounds
quarterRound(state, 0, 5, 10, 15)
quarterRound(state, 1, 6, 11, 12)
quarterRound(state, 2, 7, 8, 13)
quarterRound(state, 3, 4, 9, 14)
}
// Output first 4 and last 4 words
val subkey = ByteArray(32)
intToLittleEndian(state[0], subkey, 0)
intToLittleEndian(state[1], subkey, 4)
intToLittleEndian(state[2], subkey, 8)
intToLittleEndian(state[3], subkey, 12)
intToLittleEndian(state[12], subkey, 16)
intToLittleEndian(state[13], subkey, 20)
intToLittleEndian(state[14], subkey, 24)
intToLittleEndian(state[15], subkey, 28)
return subkey
}
private fun quarterRound(state: IntArray, a: Int, b: Int, c: Int, d: Int) {
state[a] += state[b]; state[d] = rotl(state[d] xor state[a], 16)
state[c] += state[d]; state[b] = rotl(state[b] xor state[c], 12)
state[a] += state[b]; state[d] = rotl(state[d] xor state[a], 8)
state[c] += state[d]; state[b] = rotl(state[b] xor state[c], 7)
}
private fun rotl(v: Int, c: Int): Int = (v shl c) or (v ushr (32 - c))
private fun littleEndianToInt(bs: ByteArray, off: Int): Int {
return (bs[off].toInt() and 0xff) or
((bs[off + 1].toInt() and 0xff) shl 8) or
((bs[off + 2].toInt() and 0xff) shl 16) or
((bs[off + 3].toInt() and 0xff) shl 24)
}
private fun intToLittleEndian(n: Int, bs: ByteArray, off: Int) {
bs[off] = n.toByte()
bs[off + 1] = (n ushr 8).toByte()
bs[off + 2] = (n ushr 16).toByte()
bs[off + 3] = (n ushr 24).toByte()
}
private fun longToLittleEndian(n: Long): ByteArray {
val bs = ByteArray(8)
bs[0] = n.toByte()
bs[1] = (n ushr 8).toByte()
bs[2] = (n ushr 16).toByte()
bs[3] = (n ushr 24).toByte()
bs[4] = (n ushr 32).toByte()
bs[5] = (n ushr 40).toByte()
bs[6] = (n ushr 48).toByte()
bs[7] = (n ushr 56).toByte()
return bs
}
/**
* Расшифровка текста сообщения (XChaCha20-Poly1305)
*/ */
fun decryptMessage(ciphertext: String, keyHex: String, nonceHex: String): String { fun decryptMessage(ciphertext: String, keyHex: String, nonceHex: String): String {
val key = keyHex.hexToBytes() val key = keyHex.hexToBytes()
val nonce = nonceHex.hexToBytes().take(12).toByteArray() // GCM использует 12 байт val nonce = nonceHex.hexToBytes()
val ciphertextBytes = ciphertext.hexToBytes() val ciphertextWithTag = ciphertext.hexToBytes()
val secretKey = SecretKeySpec(key, "AES") val plaintext = xchacha20Poly1305Decrypt(ciphertextWithTag, key, nonce)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(GCM_TAG_LENGTH, nonce))
val plaintext = cipher.doFinal(ciphertextBytes)
return String(plaintext, Charsets.UTF_8) return String(plaintext, Charsets.UTF_8)
} }
/**
* XChaCha20-Poly1305 decrypt implementation
*/
private fun xchacha20Poly1305Decrypt(ciphertextWithTag: ByteArray, key: ByteArray, nonce: ByteArray): ByteArray {
if (ciphertextWithTag.size < POLY1305_TAG_SIZE) {
throw IllegalArgumentException("Ciphertext too short")
}
val ciphertext = ciphertextWithTag.copyOfRange(0, ciphertextWithTag.size - POLY1305_TAG_SIZE)
val tag = ciphertextWithTag.copyOfRange(ciphertextWithTag.size - POLY1305_TAG_SIZE, ciphertextWithTag.size)
// Step 1: Derive subkey using HChaCha20
val subkey = hchacha20(key, nonce.copyOfRange(0, 16))
// Step 2: Create ChaCha20 nonce
val chacha20Nonce = ByteArray(12)
System.arraycopy(nonce, 16, chacha20Nonce, 4, 8)
// Step 3: Verify Poly1305 tag
val poly1305Key = ByteArray(32)
val zeros = ByteArray(32)
val polyKeyEngine = ChaCha7539Engine()
polyKeyEngine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
polyKeyEngine.processBytes(zeros, 0, 32, poly1305Key, 0)
val mac = Poly1305()
mac.init(KeyParameter(poly1305Key))
mac.update(ciphertext, 0, ciphertext.size)
val padding = (16 - (ciphertext.size % 16)) % 16
if (padding > 0) {
mac.update(ByteArray(padding), 0, padding)
}
mac.update(ByteArray(8), 0, 8)
val ctLen = longToLittleEndian(ciphertext.size.toLong())
mac.update(ctLen, 0, 8)
val computedTag = ByteArray(POLY1305_TAG_SIZE)
mac.doFinal(computedTag, 0)
if (!tag.contentEquals(computedTag)) {
throw SecurityException("Authentication failed")
}
// Step 4: Decrypt with ChaCha20
val engine = ChaCha7539Engine()
engine.init(false, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
val plaintext = ByteArray(ciphertext.size)
engine.processBytes(ciphertext, 0, ciphertext.size, plaintext, 0)
return plaintext
}
/** /**
* ECDH шифрование ключа для получателя * ECDH шифрование ключа для получателя
* Использует secp256k1 + AES как в RN версии * Использует secp256k1 + AES как в RN версии