feat: Implement reply blob encryption and decryption, enhance message uniqueness in ChatDetailScreen, and utilize AppleEmojiText for emoji display
This commit is contained in:
@@ -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 для конвертации
|
||||
|
||||
Reference in New Issue
Block a user