diff --git a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt index 354495d..57eaf0b 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt @@ -610,13 +610,11 @@ object MessageCrypto { android.util.Log.d("ReplyDebug", " - plainKeyAndNonce size: ${plainKeyAndNonce.size}") android.util.Log.d("ReplyDebug", " - plainKeyAndNonce hex: ${plainKeyAndNonce.joinToString("") { "%02x".format(it) }}") - // Convert plainKeyAndNonce to string then back to bytes - simulate JS behavior: - // Buffer.from(key.toString('utf-8'), 'utf8') + // Convert plainKeyAndNonce to string - simulate JS Buffer.toString('utf-8') behavior + // which replaces invalid UTF-8 sequences with U+FFFD val password = bytesToJsUtf8String(plainKeyAndNonce) - val passwordBytes = password.toByteArray(Charsets.UTF_8) - 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) }}") + 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() @@ -627,12 +625,17 @@ object MessageCrypto { deflater.end() val compressed = compressedBuffer.copyOf(compressedSize) - // PBKDF2 key derivation using custom implementation with raw bytes - // This matches JavaScript's pbkdf2Sync(Buffer.from(password, 'utf8'), ...) exactly - val salt = "rosetta".toByteArray(Charsets.UTF_8) - val keyBytes = pbkdf2WithBytes(passwordBytes, salt, 1000, 32) - - android.util.Log.d("ReplyDebug", " - Derived key hex: ${keyBytes.joinToString("") { "%02x".format(it) }}") + // PBKDF2 key derivation (matching RN: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000})) + // 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), + 1000, + 256 + ) + val secretKey = factory.generateSecret(spec) + val keyBytes = secretKey.encoded // Generate random IV (16 bytes) val iv = ByteArray(16) @@ -675,54 +678,13 @@ object MessageCrypto { String(bytes, Charsets.UTF_8) } } - - /** - * 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 полученного по сети * * Совместим с React Native: * 1. Parse "ivBase64:ciphertextBase64" format - * 2. Generate PBKDF2 key from ChaCha key as bytes via Buffer.from(password, 'utf8') + * 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) * @@ -758,21 +720,22 @@ object MessageCrypto { android.util.Log.d("ReplyDebug", " - Decoded IV size: ${iv.size}") android.util.Log.d("ReplyDebug", " - Decoded ciphertext size: ${ciphertext.size}") - // Password from plainKeyAndNonce - convert to string then back to bytes (like JS does) - // This simulates: Buffer.from(key.toString('utf-8'), 'utf8') + // Password from plainKeyAndNonce - use same JS-like UTF-8 conversion val password = bytesToJsUtf8String(plainKeyAndNonce) - val passwordBytes = password.toByteArray(Charsets.UTF_8) - 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) }}") + 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 using custom implementation with raw bytes - // This matches JavaScript's pbkdf2Sync(Buffer.from(password, 'utf8'), ...) exactly - val salt = "rosetta".toByteArray(Charsets.UTF_8) - val keyBytes = pbkdf2WithBytes(passwordBytes, salt, 1000, 32) - - android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}") - android.util.Log.d("ReplyDebug", " - Derived key hex: ${keyBytes.joinToString("") { "%02x".format(it) }}") + // PBKDF2 key derivation + // 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), + 1000, + 256 + ) + val secretKey = factory.generateSecret(spec) + val keyBytes = secretKey.encoded android.util.Log.d("ReplyDebug", " - Derived key size: ${keyBytes.size}")