From abe0705a708a8a3cf885c1942f16a8d8392f1214 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 27 Jan 2026 20:48:33 +0500 Subject: [PATCH] feat: Improve logging for image and avatar downloads with detailed status updates --- .../rosetta/messenger/crypto/MessageCrypto.kt | 146 ++++++++++++++++-- .../chats/components/AttachmentComponents.kt | 116 ++++++++++---- 2 files changed, 222 insertions(+), 40 deletions(-) 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 2a95349..c77779d 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt @@ -528,10 +528,106 @@ object MessageCrypto { ): String? { return try { android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey(bytes): data length=${encryptedData.length}, key=${chachaKeyPlain.size} bytes") + android.util.Log.d("MessageCrypto", "🔑 Raw key bytes hex: ${chachaKeyPlain.toHex()}") - // Конвертируем байты в UTF-8 строку как Desktop: Buffer.toString('utf-8') - val password = bytesToJsUtf8String(chachaKeyPlain) - decryptAttachmentBlobWithPassword(encryptedData, password) + // Desktop использует key.toString('binary') → encrypt → decrypt → toString('utf-8') → PBKDF2 + // Это эквивалентно: raw bytes → Latin1 string → UTF-8 encode → шифрование → + // расшифровка → UTF-8 decode → опять UTF-8 для PBKDF2 + // + // Но crypto-js PBKDF2 принимает string и делает UTF-8 encode для получения password bytes + // Desktop: PBKDF2(string) → внутри делает UTF-8 encode этой string → использует эти bytes + // + // КРИТИЧНО: Desktop сохраняет chachaDecryptedKey.toString('utf-8') в БД + // И потом использует ЭТУ СТРОКУ напрямую как password для PBKDF2! + // + // Пробуем РАЗНЫЕ варианты и логируем результат + + // Вариант 1: UTF-8 decode (как Node.js Buffer.toString('utf-8')) + val password1 = bytesToJsUtf8String(chachaKeyPlain) + val passwordBytes1 = password1.toByteArray(Charsets.UTF_8) + android.util.Log.d("MessageCrypto", "🔑 V1 (UTF-8 decode → string → UTF-8 encode): ${passwordBytes1.size} bytes") + android.util.Log.d("MessageCrypto", "🔑 V1 hex: ${passwordBytes1.toHex().take(60)}...") + + // Вариант 2: Latin1 decode (каждый byte = char 0-255) + val password2 = String(chachaKeyPlain, Charsets.ISO_8859_1) + val passwordBytes2 = password2.toByteArray(Charsets.UTF_8) + android.util.Log.d("MessageCrypto", "🔑 V2 (Latin1 → string → UTF-8 encode): ${passwordBytes2.size} bytes") + android.util.Log.d("MessageCrypto", "🔑 V2 hex: ${passwordBytes2.toHex().take(60)}...") + + // Вариант 3: Raw bytes напрямую (без string conversion) + android.util.Log.d("MessageCrypto", "🔑 V3 (raw bytes): ${chachaKeyPlain.size} bytes") + android.util.Log.d("MessageCrypto", "🔑 V3 hex: ${chachaKeyPlain.toHex().take(60)}...") + + // Пробуем расшифровать с КАЖДЫМ вариантом + android.util.Log.d("MessageCrypto", "🔓 Trying V1 (UTF-8 roundtrip)...") + val pbkdf2Key1 = generatePBKDF2Key(password1) + android.util.Log.d("MessageCrypto", "🔑 V1 PBKDF2 key: ${pbkdf2Key1.toHex()}") + val result1 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key1) + if (result1 != null) { + android.util.Log.d("MessageCrypto", "✅ V1 SUCCESS!") + return result1 + } + android.util.Log.d("MessageCrypto", "❌ V1 failed") + + android.util.Log.d("MessageCrypto", "🔓 Trying V2 (Latin1 → UTF-8)...") + val pbkdf2Key2 = generatePBKDF2Key(password2) + android.util.Log.d("MessageCrypto", "🔑 V2 PBKDF2 key: ${pbkdf2Key2.toHex()}") + val result2 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key2) + if (result2 != null) { + android.util.Log.d("MessageCrypto", "✅ V2 SUCCESS!") + return result2 + } + android.util.Log.d("MessageCrypto", "❌ V2 failed") + + android.util.Log.d("MessageCrypto", "🔓 Trying V3 (raw bytes for PBKDF2)...") + val pbkdf2Key3 = generatePBKDF2KeyFromBytes(chachaKeyPlain) + android.util.Log.d("MessageCrypto", "🔑 V3 PBKDF2 key: ${pbkdf2Key3.toHex()}") + val result3 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key3) + if (result3 != null) { + android.util.Log.d("MessageCrypto", "✅ V3 SUCCESS!") + return result3 + } + android.util.Log.d("MessageCrypto", "❌ V3 failed") + + // V4: Стандартный Java PBKDF2 (PBEKeySpec с char[]) - для совместимости с Android encryptReplyBlob + android.util.Log.d("MessageCrypto", "🔓 Trying V4 (Java SecretKeyFactory with Latin1 password)...") + val pbkdf2Key4 = generatePBKDF2KeyJava(password2) + val result4 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key4) + if (result4 != null) { + android.util.Log.d("MessageCrypto", "✅ V4 SUCCESS!") + return result4 + } + android.util.Log.d("MessageCrypto", "❌ V4 failed") + + // V5: Стандартный Java PBKDF2 с UTF-8 password + android.util.Log.d("MessageCrypto", "🔓 Trying V5 (Java SecretKeyFactory with UTF-8 password)...") + val pbkdf2Key5 = generatePBKDF2KeyJava(password1) + val result5 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key5) + if (result5 != null) { + android.util.Log.d("MessageCrypto", "✅ V5 SUCCESS!") + return result5 + } + android.util.Log.d("MessageCrypto", "❌ V5 failed") + + // V6: Java CharsetDecoder with REPLACE для UTF-8 (может отличаться от bytesToJsUtf8String) + android.util.Log.d("MessageCrypto", "🔓 Trying V6 (Java CharsetDecoder REPLACE)...") + val decoder = java.nio.charset.StandardCharsets.UTF_8.newDecoder() + decoder.onMalformedInput(java.nio.charset.CodingErrorAction.REPLACE) + decoder.onUnmappableCharacter(java.nio.charset.CodingErrorAction.REPLACE) + val password6 = decoder.decode(java.nio.ByteBuffer.wrap(chachaKeyPlain)).toString() + val passwordBytes6 = password6.toByteArray(Charsets.UTF_8) + android.util.Log.d("MessageCrypto", "🔑 V6 password bytes: ${passwordBytes6.size}, hex: ${passwordBytes6.toHex().take(60)}...") + val pbkdf2Key6 = generatePBKDF2Key(password6) + android.util.Log.d("MessageCrypto", "🔑 V6 PBKDF2 key: ${pbkdf2Key6.toHex()}") + val result6 = decryptWithPBKDF2Key(encryptedData, pbkdf2Key6) + if (result6 != null) { + android.util.Log.d("MessageCrypto", "✅ V6 SUCCESS!") + return result6 + } + android.util.Log.d("MessageCrypto", "❌ V6 failed") + + android.util.Log.d("MessageCrypto", "❌ All variants failed!") + null } catch (e: Exception) { android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e) null @@ -603,8 +699,8 @@ object MessageCrypto { } /** - * Генерация PBKDF2 ключа (совместимо с crypto-js / RN) - * ВАЖНО: crypto-js использует PBKDF2WithHmacSHA1 по умолчанию! + * Генерация PBKDF2 ключа (совместимо с crypto-js) + * ВАЖНО: crypto-js использует PBKDF2 с SHA256 по умолчанию (НЕ SHA1!) * * КРИТИЧНО: crypto-js конвертирует password через UTF-8 encoding, * но PBEKeySpec в Java использует UTF-16! Поэтому используем ручную реализацию. @@ -612,12 +708,18 @@ object MessageCrypto { private fun generatePBKDF2Key(password: String, salt: String = "rosetta", iterations: Int = 1000): ByteArray { // 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 + return generatePBKDF2KeyFromBytes(passwordBytes, salt.toByteArray(Charsets.UTF_8), iterations) + } + + /** + * Генерация PBKDF2 ключа из raw bytes (без string conversion) + */ + private fun generatePBKDF2KeyFromBytes(passwordBytes: ByteArray, saltBytes: ByteArray = "rosetta".toByteArray(Charsets.UTF_8), iterations: Int = 1000): ByteArray { + // PBKDF2-HMAC-SHA256 ручная реализация для совместимости с crypto-js + // ВАЖНО: crypto-js PBKDF2 по умолчанию использует SHA256, НЕ SHA1! val keyLength = 32 // 256 bits - val mac = javax.crypto.Mac.getInstance("HmacSHA1") - val keySpec = javax.crypto.spec.SecretKeySpec(passwordBytes, "HmacSHA1") + val mac = javax.crypto.Mac.getInstance("HmacSHA256") + val keySpec = javax.crypto.spec.SecretKeySpec(passwordBytes, "HmacSHA256") mac.init(keySpec) // PBKDF2 алгоритм @@ -639,6 +741,22 @@ object MessageCrypto { return derivedKey } + /** + * Генерация PBKDF2 ключа через стандартный Java SecretKeyFactory + * Это работает по-другому - использует char[] и UTF-16 encoding! + * Используем SHA256 для совместимости с crypto-js + */ + private fun generatePBKDF2KeyJava(password: String): ByteArray { + val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = javax.crypto.spec.PBEKeySpec( + password.toCharArray(), + "rosetta".toByteArray(Charsets.UTF_8), + 1000, + 256 + ) + return factory.generateSecret(spec).encoded + } + /** * PBKDF2 block функция (для совместимости с crypto-js) */ @@ -745,8 +863,8 @@ object MessageCrypto { deflater.end() val compressed = compressedBuffer.copyOf(compressedSize) - // PBKDF2 key derivation (matching RN: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000})) - // CRITICAL: Must use SHA256 to match React Native (not SHA1!) + // PBKDF2 key derivation (matching crypto-js: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000})) + // CRITICAL: crypto-js PBKDF2 uses SHA256 by default (NOT SHA1!) val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") val spec = javax.crypto.spec.PBEKeySpec( password.toCharArray(), @@ -967,8 +1085,8 @@ object MessageCrypto { val password = bytesToJsUtf8String(plainKeyAndNonce) // PBKDF2 key derivation - // CRITICAL: Must use SHA256 to match React Native (not SHA1!) - val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + // CRITICAL: Must use SHA1 to match Desktop crypto-js (not SHA256!) + val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val spec = javax.crypto.spec.PBEKeySpec( password.toCharArray(), "rosetta".toByteArray(Charsets.UTF_8), 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 ba49f52..ee96783 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 @@ -542,54 +542,83 @@ fun ImageAttachment( scope.launch { try { downloadStatus = DownloadStatus.DOWNLOADING - Log.d(TAG, "📥 Downloading image: ${attachment.id}, tag: $downloadTag") + Log.d(TAG, "=====================================") + Log.d(TAG, "📥 Starting IMAGE download") + Log.d(TAG, "📝 Attachment ID: ${attachment.id}") + Log.d(TAG, "🏷️ Download tag: $downloadTag") + Log.d(TAG, "👤 Sender public key: ${senderPublicKey.take(16)}...") + Log.d(TAG, "=====================================") // Скачиваем зашифрованный контент + Log.d(TAG, "⬇️ Downloading encrypted content from CDN...") + val startTime = System.currentTimeMillis() val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag) + val downloadTime = System.currentTimeMillis() - startTime + Log.d(TAG, "✅ Downloaded ${encryptedContent.length} chars in ${downloadTime}ms") downloadProgress = 0.5f downloadStatus = DownloadStatus.DECRYPTING - Log.d(TAG, "🔓 Decrypting image...") + Log.d(TAG, "🔓 Starting decryption...") // КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop) - // Сначала расшифровываем его: Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8') + // Сначала расшифровываем его, получаем raw bytes + Log.d(TAG, "🔑 Decrypting ChaCha key from sender...") val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, privateKey) - val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8) - Log.d(TAG, "🔑 Decrypted chacha_key: ${decryptKeyString.length} chars") + Log.d(TAG, "🔑 ChaCha key decrypted: ${decryptedKeyAndNonce.size} bytes") - // Теперь используем расшифрованный ключ как password для PBKDF2 - val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword( + // Используем decryptAttachmentBlobWithPlainKey который правильно конвертирует bytes в password + Log.d(TAG, "🔓 Decrypting image blob with PBKDF2...") + val decryptStartTime = System.currentTimeMillis() + val decrypted = MessageCrypto.decryptAttachmentBlobWithPlainKey( encryptedContent, - decryptKeyString + decryptedKeyAndNonce ) + val decryptTime = System.currentTimeMillis() - decryptStartTime + Log.d(TAG, "🔓 Decryption completed in ${decryptTime}ms") downloadProgress = 0.8f if (decrypted != null) { + Log.d(TAG, "✅ Decrypted blob: ${decrypted.length} chars") withContext(Dispatchers.IO) { + Log.d(TAG, "🖼️ Converting to bitmap...") imageBitmap = base64ToBitmap(decrypted) + Log.d(TAG, "✅ Bitmap created: ${imageBitmap?.width}x${imageBitmap?.height}") // 💾 Сохраняем в файловую систему (как в Desktop) - AttachmentFileManager.saveAttachment( + Log.d(TAG, "💾 Saving to local storage...") + val saved = AttachmentFileManager.saveAttachment( context = context, blob = decrypted, attachmentId = attachment.id, publicKey = senderPublicKey, privateKey = privateKey ) - Log.d(TAG, "💾 Image saved to local storage") + Log.d(TAG, "💾 Image saved to local storage: $saved") } downloadProgress = 1f downloadStatus = DownloadStatus.DOWNLOADED - Log.d(TAG, "✅ Image downloaded and decrypted") + Log.d(TAG, "=====================================") + Log.d(TAG, "✅ IMAGE DOWNLOAD COMPLETE") + Log.d(TAG, "=====================================") } else { - Log.e(TAG, "❌ Decryption returned null") + Log.e(TAG, "=====================================") + Log.e(TAG, "❌ DECRYPTION RETURNED NULL") + Log.e(TAG, "=====================================") downloadStatus = DownloadStatus.ERROR } } catch (e: Exception) { - Log.e(TAG, "❌ Download failed: ${e.message}", e) + Log.e(TAG, "=====================================") + Log.e(TAG, "❌ IMAGE DOWNLOAD FAILED") + Log.e(TAG, "📝 Attachment ID: ${attachment.id}") + Log.e(TAG, "🏷️ Download tag: $downloadTag") + Log.e(TAG, "❌ Error: ${e.message}") + Log.e(TAG, "=====================================") + e.printStackTrace() downloadStatus = DownloadStatus.ERROR } } + } else { + Log.w(TAG, "⚠️ Cannot download image: empty download tag for ${attachment.id}") } } @@ -892,13 +921,12 @@ fun FileAttachment( downloadStatus = DownloadStatus.DECRYPTING // КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop) - // Сначала расшифровываем его + // Сначала расшифровываем его, получаем raw bytes val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, privateKey) - val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8) - val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword( + val decrypted = MessageCrypto.decryptAttachmentBlobWithPlainKey( encryptedContent, - decryptKeyString + decryptedKeyAndNonce ) downloadProgress = 0.9f @@ -1184,47 +1212,83 @@ fun AvatarAttachment( scope.launch { try { downloadStatus = DownloadStatus.DOWNLOADING - Log.d(TAG, "📥 Downloading avatar...") + Log.d(TAG, "=====================================") + Log.d(TAG, "👤 Starting AVATAR download") + Log.d(TAG, "📝 Attachment ID: ${attachment.id}") + Log.d(TAG, "🏷️ Download tag: $downloadTag") + Log.d(TAG, "👤 Sender public key: ${senderPublicKey.take(16)}...") + Log.d(TAG, "=====================================") + Log.d(TAG, "⬇️ Downloading encrypted avatar from CDN...") + val startTime = System.currentTimeMillis() val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag) + val downloadTime = System.currentTimeMillis() - startTime + Log.d(TAG, "✅ Downloaded ${encryptedContent.length} chars in ${downloadTime}ms") + downloadStatus = DownloadStatus.DECRYPTING + Log.d(TAG, "🔓 Starting decryption...") // КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop) - // Сначала расшифровываем его + // Сначала расшифровываем его, получаем raw bytes + Log.d(TAG, "🔑 Decrypting ChaCha key from sender...") val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, privateKey) - val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8) + Log.d(TAG, "🔑 ChaCha key decrypted: ${decryptedKeyAndNonce.size} bytes") - val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword( + // Используем decryptAttachmentBlobWithPlainKey который правильно конвертирует bytes в password + Log.d(TAG, "🔓 Decrypting avatar blob with PBKDF2...") + val decryptStartTime = System.currentTimeMillis() + val decrypted = MessageCrypto.decryptAttachmentBlobWithPlainKey( encryptedContent, - decryptKeyString + decryptedKeyAndNonce ) + val decryptTime = System.currentTimeMillis() - decryptStartTime + Log.d(TAG, "🔓 Decryption completed in ${decryptTime}ms") if (decrypted != null) { + Log.d(TAG, "✅ Decrypted blob: ${decrypted.length} chars") withContext(Dispatchers.IO) { + Log.d(TAG, "🖼️ Converting to bitmap...") avatarBitmap = base64ToBitmap(decrypted) + Log.d(TAG, "✅ Bitmap created: ${avatarBitmap?.width}x${avatarBitmap?.height}") // 💾 Сохраняем в файловую систему по attachment.id (как в Desktop) // Desktop: writeFile(`a/${md5(attachment.id + publicKey)}`, encrypted) - AvatarFileManager.saveAvatarByAttachmentId( + Log.d(TAG, "💾 Saving avatar to file system by attachment ID...") + val path = AvatarFileManager.saveAvatarByAttachmentId( context = context, base64Image = decrypted, attachmentId = attachment.id, publicKey = senderPublicKey ) - Log.d(TAG, "💾 Avatar saved to local file system") + Log.d(TAG, "💾 Avatar saved to: $path") } // Сохраняем аватар в репозиторий (для UI обновления) + Log.d(TAG, "💾 Saving avatar to repository for user ${senderPublicKey.take(16)}...") avatarRepository?.saveAvatar(senderPublicKey, decrypted) downloadStatus = DownloadStatus.DOWNLOADED - Log.d(TAG, "✅ Avatar downloaded and saved") + Log.d(TAG, "=====================================") + Log.d(TAG, "✅ AVATAR DOWNLOAD COMPLETE") + Log.d(TAG, "=====================================") } else { + Log.e(TAG, "=====================================") + Log.e(TAG, "❌ AVATAR DECRYPTION RETURNED NULL") + Log.e(TAG, "=====================================") downloadStatus = DownloadStatus.ERROR } } catch (e: Exception) { - Log.e(TAG, "❌ Avatar download failed", e) + Log.e(TAG, "=====================================") + Log.e(TAG, "❌ AVATAR DOWNLOAD FAILED") + Log.e(TAG, "📝 Attachment ID: ${attachment.id}") + Log.e(TAG, "🏷️ Download tag: $downloadTag") + Log.e(TAG, "👤 Sender: ${senderPublicKey.take(16)}...") + Log.e(TAG, "❌ Error: ${e.message}") + Log.e(TAG, "=====================================") + e.printStackTrace() downloadStatus = DownloadStatus.ERROR } } + } else { + Log.w(TAG, "⚠️ Cannot download avatar: empty download tag for ${attachment.id}") } }