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 b45bc29..2a95349 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt @@ -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 (�) + * + * @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. diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index 67bb466..7db6dbd 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -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, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index f9f247c..cc59918 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -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) {