feat: Implement reply blob encryption and decryption, enhance message uniqueness in ChatDetailScreen, and utilize AppleEmojiText for emoji display

This commit is contained in:
k1ngsterr1
2026-01-13 05:46:24 +05:00
parent b1a334c954
commit c52b6c1799
4 changed files with 342 additions and 61 deletions

View File

@@ -40,6 +40,14 @@ object MessageCrypto {
val nonce: String // Hex-encoded 24-byte nonce
)
/**
* Результат расшифровки входящего сообщения
*/
data class DecryptedIncoming(
val plaintext: String, // Расшифрованный текст
val plainKeyAndNonce: ByteArray // Raw key+nonce для расшифровки attachments
)
/**
* XChaCha20-Poly1305 шифрование (совместимо с @noble/ciphers в RN)
*
@@ -531,10 +539,19 @@ object MessageCrypto {
return originalBytes
}
/**
* Результат шифрования для отправки
*/
data class EncryptedForSending(
val ciphertext: String, // Hex-encoded encrypted message
val encryptedKey: String, // ECDH+AES encrypted key (for recipient)
val plainKeyAndNonce: ByteArray // Raw key+nonce for encrypting attachments
)
/**
* Полное шифрование сообщения для отправки
*/
fun encryptForSending(plaintext: String, recipientPublicKey: String): Pair<String, String> {
fun encryptForSending(plaintext: String, recipientPublicKey: String): EncryptedForSending {
android.util.Log.d("MessageCrypto", "=".repeat(100))
android.util.Log.d("MessageCrypto", "🚀🚀🚀 START ENCRYPTION FOR SENDING 🚀🚀🚀")
android.util.Log.d("MessageCrypto", "=".repeat(100))
@@ -581,17 +598,18 @@ object MessageCrypto {
android.util.Log.d("MessageCrypto", " • Encrypted key: ${encryptedKey.take(60)}... (${encryptedKey.length} chars)")
android.util.Log.d("MessageCrypto", "=".repeat(100) + "\n")
return kotlin.Pair(encrypted.ciphertext, encryptedKey)
return EncryptedForSending(encrypted.ciphertext, encryptedKey, keyAndNonce)
}
/**
* Полная расшифровка входящего сообщения
* Возвращает текст и plainKeyAndNonce для расшифровки attachments
*/
fun decryptIncoming(
fun decryptIncomingFull(
ciphertext: String,
encryptedKey: String,
myPrivateKey: String
): String {
): DecryptedIncoming {
android.util.Log.d("MessageCrypto", "=".repeat(100))
android.util.Log.d("MessageCrypto", "🔓🔓🔓 START DECRYPTION OF INCOMING MESSAGE 🔓🔓🔓")
android.util.Log.d("MessageCrypto", "=".repeat(100))
@@ -635,9 +653,18 @@ object MessageCrypto {
android.util.Log.d("MessageCrypto", "FINAL OUTPUT: '$plaintext'")
android.util.Log.d("MessageCrypto", "=".repeat(100) + "\n")
return plaintext
return DecryptedIncoming(plaintext, keyAndNonce)
}
/**
* Совместимая версия decryptIncoming (возвращает только текст)
*/
fun decryptIncoming(
ciphertext: String,
encryptedKey: String,
myPrivateKey: String
): String = decryptIncomingFull(ciphertext, encryptedKey, myPrivateKey).plaintext
/**
* Расшифровка MESSAGES attachment blob
* Формат: ivBase64:ciphertextBase64
@@ -719,6 +746,139 @@ object MessageCrypto {
null
}
}
/**
* 🔥 Шифрует reply blob для attachments (как в React Native)
* Использует PBKDF2 + AES-CBC с тем же ключом что и основное сообщение
*
* В RN: encodeWithPassword(key.toString('utf-8'), JSON.stringify(reply))
* где key = Buffer.concat([chacha_key, nonce]) - 56 bytes as UTF-8 string
*
* Формат выхода: "ivBase64:ciphertextBase64" (совместим с desktop)
*/
fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String {
return try {
android.util.Log.d("MessageCrypto", "🔐 Encrypting reply blob...")
android.util.Log.d("MessageCrypto", " - ReplyJson length: ${replyJson.length}")
android.util.Log.d("MessageCrypto", " - PlainKeyAndNonce length: ${plainKeyAndNonce.size}")
// Convert keyAndNonce to UTF-8 string (as password) - same as RN: key.toString('utf-8')
val password = String(plainKeyAndNonce, Charsets.UTF_8)
android.util.Log.d("MessageCrypto", " - Password length: ${password.length}")
// Compress with pako (deflate)
val deflater = java.util.zip.Deflater()
deflater.setInput(replyJson.toByteArray(Charsets.UTF_8))
deflater.finish()
val compressedBuffer = ByteArray(replyJson.length * 2 + 100)
val compressedSize = deflater.deflate(compressedBuffer)
deflater.end()
val compressed = compressedBuffer.copyOf(compressedSize)
android.util.Log.d("MessageCrypto", " - Compressed size: ${compressed.size}")
// PBKDF2 key derivation (matching RN: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000}))
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val spec = javax.crypto.spec.PBEKeySpec(
password.toCharArray(),
"rosetta".toByteArray(Charsets.UTF_8),
1000,
256
)
val secretKey = factory.generateSecret(spec)
val keyBytes = secretKey.encoded
android.util.Log.d("MessageCrypto", " - PBKDF2 key derived: ${keyBytes.size} bytes")
// Generate random IV (16 bytes)
val iv = ByteArray(16)
java.security.SecureRandom().nextBytes(iv)
android.util.Log.d("MessageCrypto", " - IV generated: ${iv.size} bytes")
// AES-CBC encryption
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val keySpec = SecretKeySpec(keyBytes, "AES")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val ciphertext = cipher.doFinal(compressed)
android.util.Log.d("MessageCrypto", " - Ciphertext size: ${ciphertext.size}")
// Format: "ivBase64:ciphertextBase64" (same as RN new format)
val ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP)
val ctBase64 = Base64.encodeToString(ciphertext, Base64.NO_WRAP)
val result = "$ivBase64:$ctBase64"
android.util.Log.d("MessageCrypto", " ✅ Reply blob encrypted: ${result.take(50)}...")
result
} catch (e: Exception) {
android.util.Log.e("MessageCrypto", "❌ Failed to encrypt reply blob", e)
// Fallback: return plaintext (for backwards compatibility)
replyJson
}
}
/**
* 🔥 Расшифровывает reply blob из attachments (как в React Native)
* Формат входа: "ivBase64:ciphertextBase64"
*/
fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String {
return try {
android.util.Log.d("MessageCrypto", "🔓 Decrypting reply blob...")
android.util.Log.d("MessageCrypto", " - EncryptedBlob length: ${encryptedBlob.length}")
android.util.Log.d("MessageCrypto", " - PlainKeyAndNonce length: ${plainKeyAndNonce.size}")
// Check if it's encrypted format (contains ':')
if (!encryptedBlob.contains(':')) {
android.util.Log.d("MessageCrypto", " - Plain JSON detected, returning as-is")
return encryptedBlob
}
// Parse ivBase64:ciphertextBase64
val parts = encryptedBlob.split(':')
if (parts.size != 2) {
android.util.Log.e("MessageCrypto", " - Invalid format, expected iv:ciphertext")
return encryptedBlob
}
val iv = Base64.decode(parts[0], Base64.DEFAULT)
val ciphertext = Base64.decode(parts[1], Base64.DEFAULT)
android.util.Log.d("MessageCrypto", " - IV size: ${iv.size}, Ciphertext size: ${ciphertext.size}")
// Password from keyAndNonce
val password = String(plainKeyAndNonce, Charsets.UTF_8)
// PBKDF2 key derivation
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val spec = javax.crypto.spec.PBEKeySpec(
password.toCharArray(),
"rosetta".toByteArray(Charsets.UTF_8),
1000,
256
)
val secretKey = factory.generateSecret(spec)
val keyBytes = secretKey.encoded
// AES-CBC decryption
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val keySpec = SecretKeySpec(keyBytes, "AES")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decompressed = cipher.doFinal(ciphertext)
// Decompress with inflate
val inflater = java.util.zip.Inflater()
inflater.setInput(decompressed)
val outputBuffer = ByteArray(decompressed.size * 10)
val outputSize = inflater.inflate(outputBuffer)
inflater.end()
val plaintext = String(outputBuffer, 0, outputSize, Charsets.UTF_8)
android.util.Log.d("MessageCrypto", " ✅ Reply blob decrypted: ${plaintext.take(50)}...")
plaintext
} catch (e: Exception) {
android.util.Log.e("MessageCrypto", "❌ Failed to decrypt reply blob: ${e.message}", e)
// Return as-is, might be plain JSON
encryptedBlob
}
}
}
// Extension functions для конвертации