feat: Update RosettaDatabase to include Message and Dialog entities, increment version to 2
feat: Implement message packets for sending and receiving messages, including delivery and read notifications feat: Enhance ProtocolManager to handle message sending, delivery, and typing status with appropriate logging feat: Refactor ChatDetailScreen to utilize ChatViewModel for managing chat state and message input feat: Create ChatViewModel to manage chat messages, input state, and packet listeners for incoming messages build: Add KSP plugin for annotation processing and configure Java 17 for the build environment
This commit is contained in:
214
app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt
Normal file
214
app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt
Normal file
@@ -0,0 +1,214 @@
|
||||
package com.rosetta.messenger.crypto
|
||||
|
||||
import android.util.Base64
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.math.BigInteger
|
||||
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
|
||||
|
||||
/**
|
||||
* Шифрование сообщений как в React Native версии
|
||||
* XChaCha20-Poly1305 для текста + ECDH + AES для ключа
|
||||
*/
|
||||
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
|
||||
|
||||
init {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Результат шифрования сообщения
|
||||
*/
|
||||
data class EncryptedMessage(
|
||||
val ciphertext: String, // Hex-encoded ChaCha20 ciphertext
|
||||
val key: String, // Hex-encoded 32-byte key
|
||||
val nonce: String // Hex-encoded 24-byte nonce
|
||||
)
|
||||
|
||||
/**
|
||||
* Шифрование текста сообщения с использованием AES-GCM
|
||||
* (Аналог ChaCha20-Poly1305 для совместимости с Android)
|
||||
*/
|
||||
fun encryptMessage(plaintext: String): EncryptedMessage {
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
// Генерируем случайный ключ (32 байта) и nonce (12 байт для GCM)
|
||||
val key = ByteArray(CHACHA_KEY_SIZE)
|
||||
val nonce = ByteArray(12) // GCM использует 12 байт nonce
|
||||
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)
|
||||
|
||||
return EncryptedMessage(
|
||||
ciphertext = ciphertext.toHex(),
|
||||
key = key.toHex(),
|
||||
nonce = fullNonce.toHex()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Расшифровка текста сообщения
|
||||
*/
|
||||
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 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)
|
||||
return String(plaintext, Charsets.UTF_8)
|
||||
}
|
||||
|
||||
/**
|
||||
* ECDH шифрование ключа для получателя
|
||||
* Использует secp256k1 + AES как в RN версии
|
||||
*/
|
||||
fun encryptKeyForRecipient(keyAndNonce: ByteArray, recipientPublicKeyHex: String): String {
|
||||
val secureRandom = SecureRandom()
|
||||
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
|
||||
// Генерируем эфемерный приватный ключ
|
||||
val ephemeralPrivateKeyBytes = ByteArray(32)
|
||||
secureRandom.nextBytes(ephemeralPrivateKeyBytes)
|
||||
val ephemeralPrivateKey = BigInteger(1, ephemeralPrivateKeyBytes)
|
||||
|
||||
// Получаем эфемерный публичный ключ
|
||||
val ephemeralPublicKey = ecSpec.g.multiply(ephemeralPrivateKey)
|
||||
val ephemeralPublicKeyHex = ephemeralPublicKey.getEncoded(false).toHex()
|
||||
|
||||
// Парсим публичный ключ получателя
|
||||
val recipientPublicKeyBytes = recipientPublicKeyHex.hexToBytes()
|
||||
val recipientPublicKey = ecSpec.curve.decodePoint(recipientPublicKeyBytes)
|
||||
|
||||
// ECDH: получаем общий секрет
|
||||
val sharedPoint = recipientPublicKey.multiply(ephemeralPrivateKey)
|
||||
val sharedSecret = sharedPoint.normalize().xCoord.encoded
|
||||
|
||||
// Derive AES key from shared secret
|
||||
val aesKey = MessageDigest.getInstance("SHA-256").digest(sharedSecret)
|
||||
|
||||
// Генерируем IV для AES
|
||||
val iv = ByteArray(16)
|
||||
secureRandom.nextBytes(iv)
|
||||
|
||||
// Шифруем keyAndNonce с AES-CBC
|
||||
val secretKey = SecretKeySpec(aesKey, "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||
val encryptedKey = cipher.doFinal(keyAndNonce)
|
||||
|
||||
// Формат: iv:ciphertext:ephemeralPublicKey (Base64)
|
||||
val result = ByteArray(iv.size + encryptedKey.size + recipientPublicKeyBytes.size)
|
||||
System.arraycopy(iv, 0, result, 0, iv.size)
|
||||
System.arraycopy(encryptedKey, 0, result, iv.size, encryptedKey.size)
|
||||
|
||||
// Возвращаем как Base64 с ephemeral public key в конце
|
||||
return Base64.encodeToString(iv, Base64.NO_WRAP) + ":" +
|
||||
Base64.encodeToString(encryptedKey, Base64.NO_WRAP) + ":" +
|
||||
ephemeralPublicKeyHex
|
||||
}
|
||||
|
||||
/**
|
||||
* ECDH расшифровка ключа
|
||||
*/
|
||||
fun decryptKeyFromSender(encryptedKeyBase64: String, myPrivateKeyHex: String): ByteArray {
|
||||
val parts = encryptedKeyBase64.split(":")
|
||||
if (parts.size != 3) throw IllegalArgumentException("Invalid encrypted key format")
|
||||
|
||||
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
||||
val encryptedKey = Base64.decode(parts[1], Base64.NO_WRAP)
|
||||
val ephemeralPublicKeyHex = parts[2]
|
||||
|
||||
val ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1")
|
||||
|
||||
// Парсим мой приватный ключ
|
||||
val myPrivateKey = BigInteger(myPrivateKeyHex, 16)
|
||||
|
||||
// Парсим эфемерный публичный ключ отправителя
|
||||
val ephemeralPublicKeyBytes = ephemeralPublicKeyHex.hexToBytes()
|
||||
val ephemeralPublicKey = ecSpec.curve.decodePoint(ephemeralPublicKeyBytes)
|
||||
|
||||
// ECDH: получаем общий секрет
|
||||
val sharedPoint = ephemeralPublicKey.multiply(myPrivateKey)
|
||||
val sharedSecret = sharedPoint.normalize().xCoord.encoded
|
||||
|
||||
// Derive AES key from shared secret
|
||||
val aesKey = MessageDigest.getInstance("SHA-256").digest(sharedSecret)
|
||||
|
||||
// Расшифровываем
|
||||
val secretKey = SecretKeySpec(aesKey, "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||
|
||||
return cipher.doFinal(encryptedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Полное шифрование сообщения для отправки
|
||||
*/
|
||||
fun encryptForSending(plaintext: String, recipientPublicKey: String): Pair<String, String> {
|
||||
// 1. Шифруем текст
|
||||
val encrypted = encryptMessage(plaintext)
|
||||
|
||||
// 2. Собираем key + nonce
|
||||
val keyAndNonce = encrypted.key.hexToBytes() + encrypted.nonce.hexToBytes()
|
||||
|
||||
// 3. Шифруем ключ для получателя
|
||||
val encryptedKey = encryptKeyForRecipient(keyAndNonce, recipientPublicKey)
|
||||
|
||||
return Pair(encrypted.ciphertext, encryptedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Полная расшифровка входящего сообщения
|
||||
*/
|
||||
fun decryptIncoming(
|
||||
ciphertext: String,
|
||||
encryptedKey: String,
|
||||
myPrivateKey: String
|
||||
): String {
|
||||
// 1. Расшифровываем ключ
|
||||
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
||||
|
||||
// 2. Разделяем key и nonce
|
||||
val key = keyAndNonce.slice(0 until 32).toByteArray()
|
||||
val nonce = keyAndNonce.slice(32 until keyAndNonce.size).toByteArray()
|
||||
|
||||
// 3. Расшифровываем сообщение
|
||||
return decryptMessage(ciphertext, key.toHex(), nonce.toHex())
|
||||
}
|
||||
}
|
||||
|
||||
// Extension functions для конвертации
|
||||
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||
|
||||
private fun String.hexToBytes(): ByteArray {
|
||||
check(length % 2 == 0) { "Hex string must have even length" }
|
||||
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
}
|
||||
Reference in New Issue
Block a user