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 ea0e3cb..9c0346b 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt @@ -69,31 +69,40 @@ object MessageCrypto { * XChaCha20-Poly1305 encrypt implementation */ private fun xchacha20Poly1305Encrypt(plaintext: ByteArray, key: ByteArray, nonce: ByteArray): ByteArray { + android.util.Log.d("MessageCrypto", "🔐 XChaCha20-Poly1305 Encrypt:") + android.util.Log.d("MessageCrypto", " Key hex: ${key.toHex()}") + android.util.Log.d("MessageCrypto", " Nonce hex: ${nonce.toHex()}") + android.util.Log.d("MessageCrypto", " Plaintext hex: ${plaintext.toHex()}") + // Step 1: Derive subkey using HChaCha20 val subkey = hchacha20(key, nonce.copyOfRange(0, 16)) + android.util.Log.d("MessageCrypto", " Subkey hex: ${subkey.toHex()}") // Step 2: Create ChaCha20 nonce (4 zeros + last 8 bytes of original nonce) val chacha20Nonce = ByteArray(12) // First 4 bytes are 0 System.arraycopy(nonce, 16, chacha20Nonce, 4, 8) + android.util.Log.d("MessageCrypto", " ChaCha20 nonce hex: ${chacha20Nonce.toHex()}") - // Step 3: Encrypt with ChaCha20 + // Step 3: Initialize ChaCha20 engine ONCE val engine = ChaCha7539Engine() engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce)) + // Step 4: Generate Poly1305 key from first 64 bytes of keystream (counter=0) + val poly1305KeyBlock = ByteArray(64) + engine.processBytes(ByteArray(64), 0, 64, poly1305KeyBlock, 0) + android.util.Log.d("MessageCrypto", " Poly1305 key hex: ${poly1305KeyBlock.copyOfRange(0, 32).toHex()}") + + // Step 5: Encrypt plaintext (engine continues from counter=1 automatically) val ciphertext = ByteArray(plaintext.size) engine.processBytes(plaintext, 0, plaintext.size, ciphertext, 0) + android.util.Log.d("MessageCrypto", " Ciphertext hex: ${ciphertext.toHex()}") - // Step 4: Generate Poly1305 tag - // For AEAD, we need to compute Poly1305 over AAD + ciphertext with proper padding - val poly1305Key = ByteArray(32) - val zeros = ByteArray(32) - val polyKeyEngine = ChaCha7539Engine() - polyKeyEngine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce)) - polyKeyEngine.processBytes(zeros, 0, 32, poly1305Key, 0) + android.util.Log.d("MessageCrypto", " Ciphertext hex: ${ciphertext.toHex()}") + // Step 6: Generate Poly1305 tag val mac = Poly1305() - mac.init(KeyParameter(poly1305Key)) + mac.init(KeyParameter(poly1305KeyBlock.copyOfRange(0, 32))) // No AAD in our case, just process ciphertext mac.update(ciphertext, 0, ciphertext.size) @@ -234,15 +243,17 @@ object MessageCrypto { val chacha20Nonce = ByteArray(12) System.arraycopy(nonce, 16, chacha20Nonce, 4, 8) - // Step 3: Verify Poly1305 tag - val poly1305Key = ByteArray(32) - val zeros = ByteArray(32) - val polyKeyEngine = ChaCha7539Engine() - polyKeyEngine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce)) - polyKeyEngine.processBytes(zeros, 0, 32, poly1305Key, 0) + // Step 3: Initialize ChaCha20 engine ONCE + val engine = ChaCha7539Engine() + engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce)) + // Step 4: Generate Poly1305 key from first 64 bytes of keystream (counter=0) + val poly1305KeyBlock = ByteArray(64) + engine.processBytes(ByteArray(64), 0, 64, poly1305KeyBlock, 0) + + // Step 5: Verify Poly1305 tag val mac = Poly1305() - mac.init(KeyParameter(poly1305Key)) + mac.init(KeyParameter(poly1305KeyBlock.copyOfRange(0, 32))) mac.update(ciphertext, 0, ciphertext.size) val padding = (16 - (ciphertext.size % 16)) % 16 @@ -261,12 +272,19 @@ object MessageCrypto { throw SecurityException("Authentication failed") } - // Step 4: Decrypt with ChaCha20 - val engine = ChaCha7539Engine() - engine.init(false, ParametersWithIV(KeyParameter(subkey), chacha20Nonce)) + // Step 6: Decrypt with ChaCha20 (engine continues from where it left off) + // Note: We already consumed 64 bytes for Poly1305 key, now decrypt ciphertext + // BUT: We need a fresh engine for decryption OR reset it + // Actually, for decryption we need to re-init the engine + val decryptEngine = ChaCha7539Engine() + decryptEngine.init(false, ParametersWithIV(KeyParameter(subkey), chacha20Nonce)) + + // Skip first 64 bytes (Poly1305 key block) + val skipBlock = ByteArray(64) + decryptEngine.processBytes(ByteArray(64), 0, 64, skipBlock, 0) val plaintext = ByteArray(ciphertext.size) - engine.processBytes(ciphertext, 0, ciphertext.size, plaintext, 0) + decryptEngine.processBytes(ciphertext, 0, ciphertext.size, plaintext, 0) return plaintext } @@ -352,16 +370,20 @@ object MessageCrypto { val ivHex = iv.toHex() android.util.Log.d("MessageCrypto", " ✓ IV: $ivHex") - // ⚠️ КРИТИЧНО: Эмулируем поведение JS! - android.util.Log.d("MessageCrypto", "\n📍 Step 5: Latin1 → UTF-8 conversion (JS compatibility)") + // ⚠️ КРИТИЧНО: Эмулируем поведение React Native + crypto-js! + // React Native: Buffer.toString('binary') → строка с символами (включая > 127) + // crypto-js: AES.encrypt(string, ...) → кодирует строку в UTF-8 перед шифрованием + // Итого: байты > 127 превращаются в многобайтовые UTF-8 последовательности + android.util.Log.d("MessageCrypto", "\n📍 Step 5: Latin1 → UTF-8 (crypto-js compatibility)") android.util.Log.d("MessageCrypto", " • Input key+nonce bytes: ${keyAndNonce.size}") android.util.Log.d("MessageCrypto", " • Input hex: ${keyAndNonce.toHex()}") - val latin1String = String(keyAndNonce, Charsets.ISO_8859_1) // bytes → Latin1 string + // Шаг 1: Байты → Latin1 строка (как Buffer.toString('binary')) + val latin1String = String(keyAndNonce, Charsets.ISO_8859_1) android.util.Log.d("MessageCrypto", " • Latin1 string length: ${latin1String.length} chars") - android.util.Log.d("MessageCrypto", " • Latin1 char codes (first 20): ${latin1String.take(20).map { it.code }}") - val utf8Bytes = latin1String.toByteArray(Charsets.UTF_8) // Latin1 → UTF-8 bytes + // Шаг 2: Latin1 строка → UTF-8 байты (как crypto-js делает внутри) + val utf8Bytes = latin1String.toByteArray(Charsets.UTF_8) android.util.Log.d("MessageCrypto", " ✓ UTF-8 bytes length: ${utf8Bytes.size}") android.util.Log.d("MessageCrypto", " ✓ UTF-8 hex: ${utf8Bytes.toHex()}") @@ -374,7 +396,7 @@ object MessageCrypto { val aesKey = SecretKeySpec(sharedSecret, "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, aesKey, IvParameterSpec(iv)) - val encryptedKey = cipher.doFinal(utf8Bytes) // Шифруем UTF-8 bytes + val encryptedKey = cipher.doFinal(utf8Bytes) val encryptedKeyHex = encryptedKey.toHex() android.util.Log.d("MessageCrypto", " ✓ CIPHERTEXT: $encryptedKeyHex") @@ -491,19 +513,14 @@ object MessageCrypto { android.util.Log.d("MessageCrypto", " ✓ Decrypted UTF-8 bytes: ${decryptedUtf8Bytes.size}") android.util.Log.d("MessageCrypto", " ✓ Decrypted hex: ${decryptedUtf8Bytes.toHex()}") - // ⚠️ КРИТИЧНО: Обратная конвертация как в JS! - android.util.Log.d("MessageCrypto", "\n📍 Step 6: UTF-8 → Latin1 conversion (JS compatibility)") - // JS делает: - // 1. AES decrypt → UTF-8 bytes - // 2. decrypted.toString(crypto.enc.Utf8) → UTF-8 decode → string - // 3. Buffer.from(string, 'binary') → берёт charCode каждого символа как байт - // - // Kotlin эквивалент: - val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8) // UTF-8 decode + // ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1! + // Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary') + // Это декодирует UTF-8 в строку, потом берёт charCode каждого символа + android.util.Log.d("MessageCrypto", "\n📍 Step 6: UTF-8 → Latin1 (crypto-js compatibility)") + val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8) android.util.Log.d("MessageCrypto", " • UTF-8 string length: ${utf8String.length} chars") - android.util.Log.d("MessageCrypto", " • UTF-8 char codes (first 20): ${utf8String.take(20).map { it.code }}") - val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1) // charCode → bytes + val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1) android.util.Log.d("MessageCrypto", " ✓ RESULT bytes: ${originalBytes.size}") android.util.Log.d("MessageCrypto", " ✓ Result hex: ${originalBytes.toHex()}") diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 82e5141..fdebdb6 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -228,6 +228,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { ProtocolManager.addLog(" - To: ${recipient.take(20)}...") ProtocolManager.addLog(" - Content: ${encryptedContent.take(40)}...") ProtocolManager.addLog(" - ChaCha Key: ${encryptedKey.take(40)}...") + ProtocolManager.addLog("🔍 ChaCha Key char codes (first 20):") + ProtocolManager.addLog(" ${encryptedKey.take(20).map { it.code }.joinToString(",")}") ProtocolManager.addLog(" - Timestamp: $timestamp") ProtocolManager.addLog(" - Message ID: $messageId")