feat: Add direct encrypted key testing for password in decryptAttachmentBlob function

This commit is contained in:
k1ngsterr1
2026-01-24 17:57:40 +05:00
parent 8c87b12c5f
commit 8fe0afea20
3 changed files with 155 additions and 42 deletions

View File

@@ -437,14 +437,16 @@ object MessageCrypto {
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
val decryptedUtf8Bytes = cipher.doFinal(encryptedKey)
android.util.Log.d("MessageCrypto", "🔓 AES decrypted raw bytes: ${decryptedUtf8Bytes.size}, hex=${decryptedUtf8Bytes.toHex()}")
// ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
// Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
// Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
android.util.Log.d("MessageCrypto", "🔓 UTF-8 string length: ${utf8String.length}, chars: ${utf8String.take(20).map { it.code }}")
val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)
android.util.Log.d("MessageCrypto", "🔓 Latin1 bytes: ${originalBytes.size}, hex=${originalBytes.toHex().take(80)}...")
return originalBytes
}
@@ -514,39 +516,67 @@ object MessageCrypto {
* Формат: ivBase64:ciphertextBase64
* Использует PBKDF2 + AES-256-CBC + zlib decompression
*
* КРИТИЧНО: Desktop использует ВЕСЬ keyAndNonce (56 bytes) как password!
* Desktop: chachaDecryptedKey.toString('utf-8') - конвертирует все 56 байт в UTF-8 строку
*
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ (32 bytes)
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ+nonce (56 bytes: 32 key + 24 nonce)
*/
fun decryptAttachmentBlobWithPlainKey(
encryptedData: String,
chachaKeyPlain: ByteArray
): String? {
return try {
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey: data length=${encryptedData.length}, key=${chachaKeyPlain.size} bytes")
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey(bytes): data length=${encryptedData.length}, key=${chachaKeyPlain.size} bytes")
// ВАЖНО: Для attachment используем только первые 32 bytes (ChaCha key без nonce)
val keyOnly = chachaKeyPlain.copyOfRange(0, 32)
// 1. Конвертируем key в строку используя bytesToJsUtf8String
// чтобы совпадало с JS Buffer.toString('utf-8') который заменяет
// невалидные UTF-8 последовательности на U+FFFD
val chachaKeyString = bytesToJsUtf8String(keyOnly)
android.util.Log.d("MessageCrypto", "🔑 ChaCha key string length: ${chachaKeyString.length}")
// 2. Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
val pbkdf2Key = generatePBKDF2Key(chachaKeyString)
android.util.Log.d("MessageCrypto", "🔑 PBKDF2 key: ${pbkdf2Key.size} bytes")
// 3. Расшифровываем AES-256-CBC
val result = decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
android.util.Log.d("MessageCrypto", "✅ Decryption result: ${if (result != null) "success (${result.length} chars)" else "null"}")
result
// Конвертируем байты в UTF-8 строку как Desktop: Buffer.toString('utf-8')
val password = bytesToJsUtf8String(chachaKeyPlain)
decryptAttachmentBlobWithPassword(encryptedData, password)
} catch (e: Exception) {
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e)
null
}
}
/**
* Расшифровка attachment blob с уже готовым паролем (Latin1 string)
* Используется когда chachaKey сохранён в БД как Latin1 string (raw bytes)
*
* КРИТИЧНО: Desktop делает Buffer.from(str, 'binary').toString('utf-8')
* Это эквивалентно взятию charCode каждого символа (0-255) как байт,
* потом UTF-8 decode этих байтов с заменой невалидных на U+FFFD (<28>)
*
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
* @param passwordLatin1 Latin1 string (56 chars, каждый char = один байт 0-255)
*/
fun decryptAttachmentBlobWithPassword(
encryptedData: String,
passwordLatin1: String
): String? {
return try {
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPassword: data length=${encryptedData.length}, password=${passwordLatin1.length} chars")
// Конвертируем Latin1 string → bytes → UTF-8 string (эмулируем Desktop)
// Desktop: Buffer.from(str, 'binary').toString('utf-8')
val passwordBytes = passwordLatin1.toByteArray(Charsets.ISO_8859_1)
val passwordUtf8 = bytesToJsUtf8String(passwordBytes)
android.util.Log.d("MessageCrypto", "🔑 Password UTF-8: ${passwordUtf8.length} chars")
// Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
// Crypto-js конвертирует passwordUtf8 string в UTF-8 bytes для PBKDF2
val pbkdf2Key = generatePBKDF2Key(passwordUtf8)
android.util.Log.d("MessageCrypto", "🔑 PBKDF2 key: ${pbkdf2Key.toHex()}")
// Расшифровываем AES-256-CBC + zlib decompress
val result = decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
android.util.Log.d("MessageCrypto", "✅ Decryption: ${if (result != null) "success (${result.length} chars)" else "FAILED"}")
result
} catch (e: Exception) {
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPassword failed: ${e.message}", e)
null
}
}
/**
* Расшифровка MESSAGES attachment blob (Legacy - с RSA расшифровкой ключа)
* Формат: ivBase64:ciphertextBase64
@@ -560,11 +590,11 @@ object MessageCrypto {
return try {
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlob: data length=${encryptedData.length}, key length=${encryptedKey.length}")
// 1. Расшифровываем ChaCha ключ (как для сообщений)
// 1. Расшифровываем ChaCha ключ+nonce (56 bytes) через ECDH
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes")
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes, hex=${keyAndNonce.toHex().take(40)}...")
// 2. Используем новую функцию
// 2. Используем ВСЕ 56 байт как password для PBKDF2
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
} catch (e: Exception) {
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
@@ -575,16 +605,66 @@ object MessageCrypto {
/**
* Генерация PBKDF2 ключа (совместимо с crypto-js / RN)
* ВАЖНО: crypto-js использует PBKDF2WithHmacSHA1 по умолчанию!
*
* КРИТИЧНО: crypto-js конвертирует password через UTF-8 encoding,
* но PBEKeySpec в Java использует UTF-16! Поэтому используем ручную реализацию.
*/
private fun generatePBKDF2Key(password: String, salt: String = "rosetta", iterations: Int = 1000): ByteArray {
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val spec = javax.crypto.spec.PBEKeySpec(
password.toCharArray(),
salt.toByteArray(Charsets.UTF_8),
iterations,
256 // 32 bytes
)
return factory.generateSecret(spec).encoded
// Crypto-js: WordArray.create(password) использует UTF-8
val passwordBytes = password.toByteArray(Charsets.UTF_8)
val saltBytes = salt.toByteArray(Charsets.UTF_8)
// PBKDF2-HMAC-SHA1 ручная реализация для совместимости с crypto-js
val keyLength = 32 // 256 bits
val mac = javax.crypto.Mac.getInstance("HmacSHA1")
val keySpec = javax.crypto.spec.SecretKeySpec(passwordBytes, "HmacSHA1")
mac.init(keySpec)
// PBKDF2 алгоритм
val hLen = mac.macLength
val dkLen = keyLength
val l = (dkLen + hLen - 1) / hLen
val r = dkLen - (l - 1) * hLen
val derivedKey = ByteArray(dkLen)
var offset = 0
for (i in 1..l) {
val block = pbkdf2Block(mac, saltBytes, iterations, i)
val copyLen = if (i < l) hLen else r
System.arraycopy(block, 0, derivedKey, offset, copyLen)
offset += copyLen
}
return derivedKey
}
/**
* PBKDF2 block функция (для совместимости с crypto-js)
*/
private fun pbkdf2Block(mac: javax.crypto.Mac, salt: ByteArray, iterations: Int, blockIndex: Int): ByteArray {
// U1 = PRF(Password, Salt || INT_32_BE(i))
mac.reset()
mac.update(salt)
mac.update(byteArrayOf(
(blockIndex shr 24).toByte(),
(blockIndex shr 16).toByte(),
(blockIndex shr 8).toByte(),
blockIndex.toByte()
))
var u = mac.doFinal()
val result = u.clone()
// U2 = PRF(Password, U1), ... , Uc = PRF(Password, Uc-1)
for (j in 2..iterations) {
mac.reset()
u = mac.doFinal(u)
for (k in result.indices) {
result[k] = (result[k].toInt() xor u[k].toInt()).toByte()
}
}
return result
}
/**
@@ -700,6 +780,15 @@ object MessageCrypto {
}
}
/**
* Converts bytes to string mimicking JavaScript's Buffer.toString('utf-8') behavior.
* Uses WHATWG UTF-8 decoder algorithm where each invalid byte produces exactly ONE U+FFFD.
* This is critical for cross-platform compatibility with React Native!
*
* Public wrapper for use in MessageRepository
*/
fun bytesToJsUtf8StringPublic(bytes: ByteArray): String = bytesToJsUtf8String(bytes)
/**
* Converts bytes to string mimicking JavaScript's Buffer.toString('utf-8') behavior.
* Uses WHATWG UTF-8 decoder algorithm where each invalid byte produces exactly ONE U+FFFD.

View File

@@ -194,10 +194,16 @@ class MessageRepository private constructor(private val context: Context) {
scope.launch {
try {
// Шифрование
val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(
val encryptResult = MessageCrypto.encryptForSending(
text.trim(),
toPublicKey
)
val encryptedContent = encryptResult.ciphertext
val encryptedKey = encryptResult.encryptedKey
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
// Desktop хранит зашифрованный ключ, расшифровывает только при использовании
android.util.Log.d("MessageRepository", "🔑 Outgoing chacha_key (encrypted): ${encryptedKey.length} chars")
// Сериализуем attachments в JSON
val attachmentsJson = serializeAttachments(attachments)
@@ -306,6 +312,11 @@ class MessageRepository private constructor(private val context: Context) {
val dialogKey = getDialogKey(packet.fromPublicKey)
try {
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
// Desktop: хранит зашифрованный ключ, расшифровывает только при использовании
// Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8')
android.util.Log.d("MessageRepository", "🔑 Incoming chacha_key (encrypted): ${packet.chachaKey.length} chars")
// Расшифровываем
val plainText = MessageCrypto.decryptIncoming(
packet.content,

View File

@@ -192,11 +192,16 @@ fun ImageAttachment(
downloadStatus = DownloadStatus.DECRYPTING
Log.d(TAG, "🔓 Decrypting image...")
// Расшифровываем
val decrypted = MessageCrypto.decryptAttachmentBlob(
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
// Сначала расшифровываем его: Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8')
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, myPrivateKey)
val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8)
Log.d(TAG, "🔑 Decrypted chacha_key: ${decryptKeyString.length} chars")
// Теперь используем расшифрованный ключ как password для PBKDF2
val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword(
encryptedContent,
chachaKey,
privateKey
decryptKeyString
)
downloadProgress = 0.8f
@@ -390,10 +395,14 @@ fun FileAttachment(
downloadStatus = DownloadStatus.DECRYPTING
val decrypted = MessageCrypto.decryptAttachmentBlob(
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
// Сначала расшифровываем его
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, myPrivateKey)
val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8)
val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword(
encryptedContent,
chachaKey,
privateKey
decryptKeyString
)
downloadProgress = 0.9f
@@ -581,10 +590,14 @@ fun AvatarAttachment(
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
downloadStatus = DownloadStatus.DECRYPTING
val decrypted = MessageCrypto.decryptAttachmentBlob(
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
// Сначала расшифровываем его
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, myPrivateKey)
val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8)
val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword(
encryptedContent,
chachaKey,
privateKey
decryptKeyString
)
if (decrypted != null) {