feat: Enhance reply handling with detailed logging and update encryption method to SHA256
This commit is contained in:
@@ -592,24 +592,29 @@ object MessageCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Шифрует reply blob для attachments (как в React Native)
|
||||
* Использует PBKDF2 + AES-CBC с тем же ключом что и основное сообщение
|
||||
* Шифрование reply blob для передачи по сети
|
||||
*
|
||||
* В RN: encodeWithPassword(key.toString('utf-8'), JSON.stringify(reply))
|
||||
* где key = Buffer.concat([chacha_key, nonce]) - 56 bytes
|
||||
* Совместим с React Native:
|
||||
* 1. Compress with pako (deflate)
|
||||
* 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8')
|
||||
* 3. AES-256-CBC encryption
|
||||
*
|
||||
* ВАЖНО: JavaScript Buffer.toString('utf-8') на невалидных UTF-8 байтах
|
||||
* заменяет их на U+FFFD (replacement character). Это поведение нужно
|
||||
* воспроизвести в Kotlin для совместимости.
|
||||
* @param replyJson - JSON string to encrypt
|
||||
* @param plainKeyAndNonce - 56 bytes (32 key + 24 nonce)
|
||||
*
|
||||
* Формат выхода: "ivBase64:ciphertextBase64" (совместим с desktop)
|
||||
*/
|
||||
fun encryptReplyBlob(replyJson: String, plainKeyAndNonce: ByteArray): String {
|
||||
return try {
|
||||
android.util.Log.d("ReplyDebug", "🔐 encryptReplyBlob called:")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// Convert keyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior
|
||||
// Convert plainKeyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior
|
||||
// which replaces invalid UTF-8 sequences with U+FFFD
|
||||
val password = bytesToJsUtf8String(plainKeyAndNonce)
|
||||
android.util.Log.d("ReplyDebug", " - password length: ${password.length}")
|
||||
android.util.Log.d("ReplyDebug", " - password bytes (first 20): ${password.toByteArray(Charsets.UTF_8).take(20).joinToString(",")}")
|
||||
|
||||
// Compress with pako (deflate)
|
||||
val deflater = java.util.zip.Deflater()
|
||||
@@ -621,7 +626,8 @@ object MessageCrypto {
|
||||
val compressed = compressedBuffer.copyOf(compressedSize)
|
||||
|
||||
// PBKDF2 key derivation (matching RN: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000}))
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
// CRITICAL: Must use SHA256 to match React Native (not SHA1!)
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = javax.crypto.spec.PBEKeySpec(
|
||||
password.toCharArray(),
|
||||
"rosetta".toByteArray(Charsets.UTF_8),
|
||||
@@ -674,31 +680,54 @@ object MessageCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 Расшифровывает reply blob из attachments (как в React Native)
|
||||
* Формат входа: "ivBase64:ciphertextBase64"
|
||||
* Расшифровка reply blob полученного по сети
|
||||
*
|
||||
* Совместим с React Native:
|
||||
* 1. Parse "ivBase64:ciphertextBase64" format
|
||||
* 2. Generate PBKDF2 key from ChaCha key (32 bytes) as string via Buffer.toString('utf-8')
|
||||
* 3. AES-256-CBC decryption
|
||||
* 4. Decompress with pako (inflate)
|
||||
*
|
||||
* @param encryptedBlob - "ivBase64:ciphertextBase64" format
|
||||
* @param plainKeyAndNonce - 56 bytes (32 key + 24 nonce)
|
||||
*/
|
||||
fun decryptReplyBlob(encryptedBlob: String, plainKeyAndNonce: ByteArray): String {
|
||||
return try {
|
||||
android.util.Log.d("ReplyDebug", "🔓 decryptReplyBlob called:")
|
||||
android.util.Log.d("ReplyDebug", " - Input length: ${encryptedBlob.length}")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}")
|
||||
android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// Check if it's encrypted format (contains ':')
|
||||
if (!encryptedBlob.contains(':')) {
|
||||
android.util.Log.d("ReplyDebug", " - No ':' found, returning as-is")
|
||||
return encryptedBlob
|
||||
}
|
||||
|
||||
// Parse ivBase64:ciphertextBase64
|
||||
val parts = encryptedBlob.split(':')
|
||||
if (parts.size != 2) {
|
||||
android.util.Log.d("ReplyDebug", " - Invalid format (not 2 parts), returning as-is")
|
||||
return encryptedBlob
|
||||
}
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - IV part length: ${parts[0].length}")
|
||||
android.util.Log.d("ReplyDebug", " - Ciphertext part length: ${parts[1].length}")
|
||||
|
||||
val iv = Base64.decode(parts[0], Base64.DEFAULT)
|
||||
val ciphertext = Base64.decode(parts[1], Base64.DEFAULT)
|
||||
|
||||
// Password from keyAndNonce - use same JS-like UTF-8 conversion
|
||||
android.util.Log.d("ReplyDebug", " - Decoded IV size: ${iv.size}")
|
||||
android.util.Log.d("ReplyDebug", " - Decoded ciphertext size: ${ciphertext.size}")
|
||||
|
||||
// Password from plainKeyAndNonce - use same JS-like UTF-8 conversion
|
||||
val password = bytesToJsUtf8String(plainKeyAndNonce)
|
||||
android.util.Log.d("ReplyDebug", " - Password length: ${password.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Password bytes hex: ${password.toByteArray(Charsets.UTF_8).joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
// PBKDF2 key derivation
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
// CRITICAL: Must use SHA256 to match React Native (not SHA1!)
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = javax.crypto.spec.PBEKeySpec(
|
||||
password.toCharArray(),
|
||||
"rosetta".toByteArray(Charsets.UTF_8),
|
||||
@@ -708,6 +737,8 @@ object MessageCrypto {
|
||||
val secretKey = factory.generateSecret(spec)
|
||||
val keyBytes = secretKey.encoded
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")
|
||||
|
||||
// AES-CBC decryption
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val keySpec = SecretKeySpec(keyBytes, "AES")
|
||||
@@ -715,6 +746,8 @@ object MessageCrypto {
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
|
||||
val decompressed = cipher.doFinal(ciphertext)
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - Decrypted (compressed) size: ${decompressed.size}")
|
||||
|
||||
// Decompress with inflate
|
||||
val inflater = java.util.zip.Inflater()
|
||||
inflater.setInput(decompressed)
|
||||
@@ -723,8 +756,14 @@ object MessageCrypto {
|
||||
inflater.end()
|
||||
val plaintext = String(outputBuffer, 0, outputSize, Charsets.UTF_8)
|
||||
|
||||
android.util.Log.d("ReplyDebug", " - Decompressed plaintext length: ${plaintext.length}")
|
||||
android.util.Log.d("ReplyDebug", " - Plaintext preview: ${plaintext.take(100)}")
|
||||
android.util.Log.d("ReplyDebug", "✅ decryptReplyBlob success")
|
||||
|
||||
plaintext
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ReplyDebug", "❌ decryptReplyBlob failed:", e)
|
||||
android.util.Log.e("ReplyDebug", " - Exception: ${e.javaClass.simpleName}: ${e.message}")
|
||||
// Return as-is, might be plain JSON
|
||||
encryptedBlob
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user