feat: Update PBKDF2 key derivation to use SHA256 and simplify password handling for compatibility with React Native

This commit is contained in:
k1ngsterr1
2026-01-16 02:39:43 +05:00
parent 25d4d5cdcc
commit 7e710a3160

View File

@@ -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)
@@ -676,53 +679,12 @@ 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 полученного по сети
*
* Совместим с 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}")