feat: Implement XChaCha20-Poly1305 encryption and decryption in MessageCrypto for enhanced security
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
package com.rosetta.messenger.crypto
|
||||
|
||||
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.provider.BouncyCastleProvider
|
||||
import java.math.BigInteger
|
||||
@@ -8,7 +12,6 @@ import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@@ -19,9 +22,8 @@ import javax.crypto.spec.SecretKeySpec
|
||||
object MessageCrypto {
|
||||
|
||||
private const val CHACHA_KEY_SIZE = 32
|
||||
private const val CHACHA_NONCE_SIZE = 24
|
||||
private const val AES_KEY_SIZE = 32
|
||||
private const val GCM_TAG_LENGTH = 128
|
||||
private const val XCHACHA_NONCE_SIZE = 24
|
||||
private const val POLY1305_TAG_SIZE = 16
|
||||
|
||||
init {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
@@ -33,58 +35,242 @@ object MessageCrypto {
|
||||
* Результат шифрования сообщения
|
||||
*/
|
||||
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 nonce: String // Hex-encoded 24-byte nonce
|
||||
)
|
||||
|
||||
/**
|
||||
* Шифрование текста сообщения с использованием AES-GCM
|
||||
* (Аналог ChaCha20-Poly1305 для совместимости с Android)
|
||||
* XChaCha20-Poly1305 шифрование (совместимо с @noble/ciphers в RN)
|
||||
*
|
||||
* XChaCha20-Poly1305 использует 24-byte nonce:
|
||||
* - Первые 16 байт nonce используются в HChaCha20 для derive subkey
|
||||
* - Последние 8 байт nonce используются как counter для ChaCha20
|
||||
*/
|
||||
fun encryptMessage(plaintext: String): EncryptedMessage {
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
// Генерируем случайный ключ (32 байта) и nonce (12 байт для GCM)
|
||||
// Генерируем случайный ключ (32 байта) и nonce (24 байта для XChaCha20)
|
||||
val key = ByteArray(CHACHA_KEY_SIZE)
|
||||
val nonce = ByteArray(12) // GCM использует 12 байт nonce
|
||||
val nonce = ByteArray(XCHACHA_NONCE_SIZE)
|
||||
secureRandom.nextBytes(key)
|
||||
secureRandom.nextBytes(nonce)
|
||||
|
||||
// Шифруем AES-GCM
|
||||
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)
|
||||
val ciphertext = xchacha20Poly1305Encrypt(plaintext.toByteArray(Charsets.UTF_8), key, nonce)
|
||||
|
||||
return EncryptedMessage(
|
||||
ciphertext = ciphertext.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 {
|
||||
val key = keyHex.hexToBytes()
|
||||
val nonce = nonceHex.hexToBytes().take(12).toByteArray() // GCM использует 12 байт
|
||||
val ciphertextBytes = ciphertext.hexToBytes()
|
||||
val nonce = nonceHex.hexToBytes()
|
||||
val ciphertextWithTag = ciphertext.hexToBytes()
|
||||
|
||||
val secretKey = SecretKeySpec(key, "AES")
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(GCM_TAG_LENGTH, nonce))
|
||||
|
||||
val plaintext = cipher.doFinal(ciphertextBytes)
|
||||
val plaintext = xchacha20Poly1305Decrypt(ciphertextWithTag, key, nonce)
|
||||
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 шифрование ключа для получателя
|
||||
* Использует secp256k1 + AES как в RN версии
|
||||
|
||||
Reference in New Issue
Block a user