feat: Extract replyToMessageId from attachments and implement custom PBKDF2 key derivation

This commit is contained in:
k1ngsterr1
2026-01-16 02:32:19 +05:00
parent f8ba10be54
commit 25d4d5cdcc
3 changed files with 141 additions and 33 deletions

View File

@@ -610,11 +610,13 @@ object MessageCrypto {
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}") android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}") android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
// Convert plainKeyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior // Convert plainKeyAndNonce to string then back to bytes - simulate JS behavior:
// which replaces invalid UTF-8 sequences with U+FFFD // Buffer.from(key.toString('utf-8'), 'utf8')
val password = bytesToJsUtf8String(plainKeyAndNonce) val password = bytesToJsUtf8String(plainKeyAndNonce)
android.util.Log.d("ReplyDebug", " - password length: ${password.length}") val passwordBytes = password.toByteArray(Charsets.UTF_8)
android.util.Log.d("ReplyDebug", " - password bytes (first 20): ${password.toByteArray(Charsets.UTF_8).take(20).joinToString(",")}") android.util.Log.d("ReplyDebug", " - password string length: ${password.length}")
android.util.Log.d("ReplyDebug", " - password bytes length: ${passwordBytes.size}")
android.util.Log.d("ReplyDebug", " - password bytes hex: ${passwordBytes.joinToString("") { "%02x".format(it) }}")
// Compress with pako (deflate) // Compress with pako (deflate)
val deflater = java.util.zip.Deflater() val deflater = java.util.zip.Deflater()
@@ -625,17 +627,12 @@ object MessageCrypto {
deflater.end() deflater.end()
val compressed = compressedBuffer.copyOf(compressedSize) val compressed = compressedBuffer.copyOf(compressedSize)
// PBKDF2 key derivation (matching RN: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000})) // PBKDF2 key derivation using custom implementation with raw bytes
// CRITICAL: Must use SHA256 to match React Native (not SHA1!) // This matches JavaScript's pbkdf2Sync(Buffer.from(password, 'utf8'), ...) exactly
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") val salt = "rosetta".toByteArray(Charsets.UTF_8)
val spec = javax.crypto.spec.PBEKeySpec( val keyBytes = pbkdf2WithBytes(passwordBytes, salt, 1000, 32)
password.toCharArray(),
"rosetta".toByteArray(Charsets.UTF_8), android.util.Log.d("ReplyDebug", " - Derived key hex: ${keyBytes.joinToString("") { "%02x".format(it) }}")
1000,
256
)
val secretKey = factory.generateSecret(spec)
val keyBytes = secretKey.encoded
// Generate random IV (16 bytes) // Generate random IV (16 bytes)
val iv = ByteArray(16) val iv = ByteArray(16)
@@ -679,12 +676,53 @@ object MessageCrypto {
} }
} }
/**
* Custom PBKDF2-HMAC-SHA256 implementation that takes raw bytes as password.
* This matches JavaScript's pbkdf2Sync(Buffer.from(password, 'utf8'), ...) behavior exactly.
*/
private fun pbkdf2WithBytes(passwordBytes: ByteArray, salt: ByteArray, iterations: Int, keyLength: Int): ByteArray {
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(passwordBytes, "HmacSHA256"))
val hashLength = 32 // SHA256 produces 32 bytes
val numBlocks = (keyLength + hashLength - 1) / hashLength
val result = ByteArray(numBlocks * hashLength)
for (blockNum in 1..numBlocks) {
// U1 = PRF(Password, Salt || INT_32_BE(i))
val blockBytes = ByteArray(4)
blockBytes[0] = (blockNum shr 24).toByte()
blockBytes[1] = (blockNum shr 16).toByte()
blockBytes[2] = (blockNum shr 8).toByte()
blockBytes[3] = blockNum.toByte()
mac.reset()
mac.update(salt)
mac.update(blockBytes)
var u = mac.doFinal()
var block = u.copyOf()
// U2...Uc
for (j in 2..iterations) {
mac.reset()
u = mac.doFinal(u)
for (k in block.indices) {
block[k] = (block[k].toInt() xor u[k].toInt()).toByte()
}
}
System.arraycopy(block, 0, result, (blockNum - 1) * hashLength, hashLength)
}
return result.copyOf(keyLength)
}
/** /**
* Расшифровка reply blob полученного по сети * Расшифровка reply blob полученного по сети
* *
* Совместим с React Native: * Совместим с React Native:
* 1. Parse "ivBase64:ciphertextBase64" format * 1. Parse "ivBase64:ciphertextBase64" format
* 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8') * 2. Generate PBKDF2 key from ChaCha key as bytes via Buffer.from(password, 'utf8')
* 3. AES-256-CBC decryption * 3. AES-256-CBC decryption
* 4. Decompress with pako (inflate) * 4. Decompress with pako (inflate)
* *
@@ -720,22 +758,21 @@ object MessageCrypto {
android.util.Log.d("ReplyDebug", " - Decoded IV size: ${iv.size}") android.util.Log.d("ReplyDebug", " - Decoded IV size: ${iv.size}")
android.util.Log.d("ReplyDebug", " - Decoded ciphertext size: ${ciphertext.size}") android.util.Log.d("ReplyDebug", " - Decoded ciphertext size: ${ciphertext.size}")
// Password from plainKeyAndNonce - use same JS-like UTF-8 conversion // Password from plainKeyAndNonce - convert to string then back to bytes (like JS does)
// This simulates: Buffer.from(key.toString('utf-8'), 'utf8')
val password = bytesToJsUtf8String(plainKeyAndNonce) val password = bytesToJsUtf8String(plainKeyAndNonce)
android.util.Log.d("ReplyDebug", " - Password length: ${password.length}") val passwordBytes = password.toByteArray(Charsets.UTF_8)
android.util.Log.d("ReplyDebug", " - Password bytes hex: ${password.toByteArray(Charsets.UTF_8).joinToString("") { "%02x".format(it) }}") android.util.Log.d("ReplyDebug", " - Password string length: ${password.length}")
android.util.Log.d("ReplyDebug", " - Password bytes length: ${passwordBytes.size}")
android.util.Log.d("ReplyDebug", " - Password bytes hex: ${passwordBytes.joinToString("") { "%02x".format(it) }}")
// PBKDF2 key derivation // PBKDF2 key derivation using custom implementation with raw bytes
// CRITICAL: Must use SHA256 to match React Native (not SHA1!) // This matches JavaScript's pbkdf2Sync(Buffer.from(password, 'utf8'), ...) exactly
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") val salt = "rosetta".toByteArray(Charsets.UTF_8)
val spec = javax.crypto.spec.PBEKeySpec( val keyBytes = pbkdf2WithBytes(passwordBytes, salt, 1000, 32)
password.toCharArray(),
"rosetta".toByteArray(Charsets.UTF_8), android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")
1000, android.util.Log.d("ReplyDebug", " - Derived key hex: ${keyBytes.joinToString("") { "%02x".format(it) }}")
256
)
val secretKey = factory.generateSecret(spec)
val keyBytes = secretKey.encoded
android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}") android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")

View File

@@ -284,6 +284,9 @@ class MessageRepository private constructor(private val context: Context) {
privateKey privateKey
) )
// Извлекаем replyToMessageId из attachments
val replyToMessageId = extractReplyToMessageId(packet.attachments)
// 🔒 Шифруем plainMessage с использованием приватного ключа // 🔒 Шифруем plainMessage с использованием приватного ключа
val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey) val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey)
@@ -301,6 +304,7 @@ class MessageRepository private constructor(private val context: Context) {
messageId = messageId, // 🔥 Используем сгенерированный messageId! messageId = messageId, // 🔥 Используем сгенерированный messageId!
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
attachments = attachmentsJson, attachments = attachmentsJson,
replyToMessageId = replyToMessageId, // 🔥 Добавляем replyToMessageId из attachments!
dialogKey = dialogKey dialogKey = dialogKey
) )
@@ -552,6 +556,32 @@ class MessageRepository private constructor(private val context: Context) {
plainMessage plainMessage
} }
// Десериализуем attachments из JSON
val attachmentsList = try {
if (attachments.isNotEmpty() && attachments != "[]") {
val jsonArray = JSONArray(attachments)
(0 until jsonArray.length()).mapNotNull { i ->
try {
val obj = jsonArray.getJSONObject(i)
MessageAttachment(
id = obj.optString("id", ""),
blob = obj.optString("blob", ""),
type = AttachmentType.fromInt(obj.optInt("type", 0)),
preview = obj.optString("preview", ""),
width = obj.optInt("width", 0),
height = obj.optInt("height", 0)
)
} catch (e: Exception) {
null
}
}
} else {
emptyList()
}
} catch (e: Exception) {
emptyList()
}
return Message( return Message(
id = id, id = id,
messageId = messageId, messageId = messageId,
@@ -562,6 +592,7 @@ class MessageRepository private constructor(private val context: Context) {
isFromMe = fromMe == 1, isFromMe = fromMe == 1,
isRead = read == 1, isRead = read == 1,
deliveryStatus = DeliveryStatus.fromInt(delivered), deliveryStatus = DeliveryStatus.fromInt(delivered),
attachments = attachmentsList, // Десериализованные attachments
replyToMessageId = replyToMessageId replyToMessageId = replyToMessageId
) )
} }
@@ -599,6 +630,39 @@ class MessageRepository private constructor(private val context: Context) {
return jsonArray.toString() return jsonArray.toString()
} }
/**
* Извлечение replyToMessageId из attachments
* Ищет MESSAGES (type=1) attachment и парсит message_id
*/
private fun extractReplyToMessageId(attachments: List<MessageAttachment>): String? {
if (attachments.isEmpty()) return null
for (attachment in attachments) {
if (attachment.type == AttachmentType.MESSAGES) {
try {
// Берем blob или preview
val dataJson = attachment.blob.ifEmpty { attachment.preview }
if (dataJson.isEmpty() || dataJson.contains(":")) {
// Пропускаем зашифрованные старые сообщения
continue
}
val messagesArray = JSONArray(dataJson)
if (messagesArray.length() > 0) {
val replyMessage = messagesArray.getJSONObject(0)
val replyMessageId = replyMessage.optString("message_id", "")
if (replyMessageId.isNotEmpty()) {
return replyMessageId
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
return null
}
/** /**
* Сериализация attachments в JSON с расшифровкой MESSAGES blob * Сериализация attachments в JSON с расшифровкой MESSAGES blob
* Для MESSAGES типа blob расшифровывается и сохраняется в preview (как в RN) * Для MESSAGES типа blob расшифровывается и сохраняется в preview (как в RN)

View File

@@ -2485,9 +2485,16 @@ private fun MessageInputBar(
) )
} }
} }
IconButton( // 🔥 Box с clickable вместо IconButton - убираем задержку ripple
onClick = onCloseReply, Box(
modifier = Modifier.size(32.dp) modifier = Modifier
.size(32.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null, // Убираем ripple индикацию для мгновенного клика
onClick = onCloseReply
),
contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
Icons.Default.Close, Icons.Default.Close,