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:
k1ngsterr1
2026-01-10 22:15:27 +05:00
parent 286706188b
commit 6014d23d69
12 changed files with 1643 additions and 142 deletions

View 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()
}