feat: Add fallback transport server and enhance file download handling with blurhash support
This commit is contained in:
@@ -513,6 +513,44 @@ object MessageCrypto {
|
||||
* Расшифровка MESSAGES attachment blob
|
||||
* Формат: ivBase64:ciphertextBase64
|
||||
* Использует PBKDF2 + AES-256-CBC + zlib decompression
|
||||
*
|
||||
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
|
||||
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ (32 bytes)
|
||||
*/
|
||||
fun decryptAttachmentBlobWithPlainKey(
|
||||
encryptedData: String,
|
||||
chachaKeyPlain: ByteArray
|
||||
): String? {
|
||||
return try {
|
||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey: 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
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Расшифровка MESSAGES attachment blob (Legacy - с RSA расшифровкой ключа)
|
||||
* Формат: ivBase64:ciphertextBase64
|
||||
* Использует PBKDF2 + AES-256-CBC + zlib decompression
|
||||
*/
|
||||
fun decryptAttachmentBlob(
|
||||
encryptedData: String,
|
||||
@@ -520,20 +558,16 @@ object MessageCrypto {
|
||||
myPrivateKey: String
|
||||
): String? {
|
||||
return try {
|
||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlob: data length=${encryptedData.length}, key length=${encryptedKey.length}")
|
||||
|
||||
// 1. Расшифровываем ChaCha ключ (как для сообщений)
|
||||
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
||||
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes")
|
||||
|
||||
// 2. Конвертируем key+nonce в строку используя bytesToJsUtf8String
|
||||
// чтобы совпадало с JS Buffer.toString('utf-8') который заменяет
|
||||
// невалидные UTF-8 последовательности на U+FFFD
|
||||
val chachaKeyString = bytesToJsUtf8String(keyAndNonce)
|
||||
|
||||
// 3. Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
|
||||
val pbkdf2Key = generatePBKDF2Key(chachaKeyString)
|
||||
|
||||
// 4. Расшифровываем AES-256-CBC
|
||||
decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
|
||||
// 2. Используем новую функцию
|
||||
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -559,13 +593,20 @@ object MessageCrypto {
|
||||
*/
|
||||
private fun decryptWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
|
||||
return try {
|
||||
android.util.Log.d("MessageCrypto", "🔓 decryptWithPBKDF2Key: data length=${encryptedData.length}")
|
||||
android.util.Log.d("MessageCrypto", "🔓 First 100 chars: ${encryptedData.take(100)}")
|
||||
android.util.Log.d("MessageCrypto", "🔓 Contains colon: ${encryptedData.contains(":")}")
|
||||
|
||||
val parts = encryptedData.split(":")
|
||||
android.util.Log.d("MessageCrypto", "🔓 Split parts: ${parts.size}")
|
||||
if (parts.size != 2) {
|
||||
android.util.Log.e("MessageCrypto", "❌ Invalid format: expected 2 parts, got ${parts.size}")
|
||||
return null
|
||||
}
|
||||
|
||||
val iv = android.util.Base64.decode(parts[0], android.util.Base64.DEFAULT)
|
||||
val ciphertext = android.util.Base64.decode(parts[1], android.util.Base64.DEFAULT)
|
||||
android.util.Log.d("MessageCrypto", "🔓 IV: ${iv.size} bytes, Ciphertext: ${ciphertext.size} bytes")
|
||||
|
||||
// AES-256-CBC расшифровка
|
||||
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
@@ -573,6 +614,7 @@ object MessageCrypto {
|
||||
val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
|
||||
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
||||
val decrypted = cipher.doFinal(ciphertext)
|
||||
android.util.Log.d("MessageCrypto", "🔓 AES decrypted: ${decrypted.size} bytes")
|
||||
|
||||
// Zlib декомпрессия
|
||||
val inflater = java.util.zip.Inflater()
|
||||
@@ -585,8 +627,11 @@ object MessageCrypto {
|
||||
}
|
||||
inflater.end()
|
||||
|
||||
String(outputStream.toByteArray(), Charsets.UTF_8)
|
||||
val result = String(outputStream.toByteArray(), Charsets.UTF_8)
|
||||
android.util.Log.d("MessageCrypto", "🔓 Decompressed: ${result.length} chars")
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MessageCrypto", "❌ decryptWithPBKDF2Key failed: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ data class TransportState(
|
||||
object TransportManager {
|
||||
private const val TAG = "TransportManager"
|
||||
|
||||
// Fallback transport server (CDN)
|
||||
private const val FALLBACK_TRANSPORT_SERVER = "https://cdn.rosetta-im.com"
|
||||
|
||||
private var transportServer: String? = null
|
||||
|
||||
private val _uploading = MutableStateFlow<List<TransportState>>(emptyList())
|
||||
@@ -54,9 +57,18 @@ object TransportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить адрес транспортного сервера
|
||||
* Получить адрес транспортного сервера (с fallback)
|
||||
*/
|
||||
fun getTransportServer(): String? = transportServer
|
||||
fun getTransportServer(): String = transportServer ?: FALLBACK_TRANSPORT_SERVER
|
||||
|
||||
/**
|
||||
* Получить активный сервер для скачивания/загрузки
|
||||
*/
|
||||
private fun getActiveServer(): String {
|
||||
val server = transportServer ?: FALLBACK_TRANSPORT_SERVER
|
||||
Log.d(TAG, "📡 Using transport server: $server (configured: ${transportServer != null})")
|
||||
return server
|
||||
}
|
||||
|
||||
/**
|
||||
* Запросить адрес транспортного сервера с сервера протокола
|
||||
@@ -73,10 +85,9 @@ object TransportManager {
|
||||
* @return Tag для скачивания файла
|
||||
*/
|
||||
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
|
||||
val server = transportServer
|
||||
?: throw IllegalStateException("Transport server is not set")
|
||||
val server = getActiveServer()
|
||||
|
||||
Log.d(TAG, "📤 Uploading file: $id")
|
||||
Log.d(TAG, "📤 Uploading file: $id to $server")
|
||||
|
||||
// Добавляем в список загрузок
|
||||
_uploading.value = _uploading.value + TransportState(id, 0)
|
||||
@@ -139,10 +150,9 @@ object TransportManager {
|
||||
* @return Содержимое файла
|
||||
*/
|
||||
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
|
||||
val server = transportServer
|
||||
?: throw IllegalStateException("Transport server is not set")
|
||||
val server = getActiveServer()
|
||||
|
||||
Log.d(TAG, "📥 Downloading file: $id (tag: $tag)")
|
||||
Log.d(TAG, "📥 Downloading file: $id (tag: $tag) from $server")
|
||||
|
||||
// Добавляем в список скачиваний
|
||||
_downloading.value = _downloading.value + TransportState(id, 0)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
32
app/src/main/res/raw/globalsign_ca.pem
Normal file
32
app/src/main/res/raw/globalsign_ca.pem
Normal file
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,6 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- HTTP для локального сервера -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">46.28.71.12</domain>
|
||||
</domain-config>
|
||||
|
||||
<!-- HTTPS с кастомным CA для CDN -->
|
||||
<domain-config>
|
||||
<domain includeSubdomains="true">cdn.rosetta-im.com</domain>
|
||||
<domain includeSubdomains="true">rosetta-im.com</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="@raw/globalsign_ca"/>
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
Reference in New Issue
Block a user