feat: Add fallback transport server and enhance file download handling with blurhash support

This commit is contained in:
k1ngsterr1
2026-01-24 16:04:23 +05:00
parent d5083e60a5
commit 8c87b12c5f
5 changed files with 593 additions and 314 deletions

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFjTCCA3WgAwIBAgIRAIN9TriekS/nLK07x2kt3CAwDQYJKoZIhvcNAQELBQAw
TDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkds
b2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMjUwNTIxMDIzNjUyWhcN
MjcwNTIxMDAwMDAwWjBVMQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2ln
biBudi1zYTErMCkGA1UEAxMiR2xvYmFsU2lnbiBHQ0MgUjYgQWxwaGFTU0wgQ0Eg
MjAyNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/oiu0Bviq52UUE
ADbFWmgu3rC7KDSMoorLN1Wd03McG3Z1aP71DlPCE33838r72Dfuj5M9LXfiQLJp
Au6MwNExmKOzothw4x0zGf5oBYyrCMGm3fBpLPafwYQ3MchBOWMTbf83rKUPLH48
KCJ0MnU8GUl8oA/J81wIvbbKPuNrFf6hvJDccjzc4NyxLz3A89zjV2g5whCg5O0u
9YX4Zxk9JHuc/LvllOJO4waAYLjbWBJkz3rV3ts1SmSYnJqmyRTIjXwQgRvhEYqt
DbRskt0W7M6cPwCze3GTBN2UHNpHkMs3YmVxku68I0aOQn5+uz//fDROP3z1Z/7I
APteRtECAwEAAaOCAV8wggFbMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr
BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
xbSTj28r3B5Iv7cQMIXO0bK7SC0wHwYDVR0jBBgwFoAUrmwFo5MT4qLn4tcc1sfw
f8hnU6AwewYIKwYBBQUHAQEEbzBtMC4GCCsGAQUFBzABhiJodHRwOi8vb2NzcDIu
Z2xvYmFsc2lnbi5jb20vcm9vdHI2MDsGCCsGAQUFBzAChi9odHRwOi8vc2VjdXJl
Lmdsb2JhbHNpZ24uY29tL2NhY2VydC9yb290LXI2LmNydDA2BgNVHR8ELzAtMCug
KaAnhiVodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29tL3Jvb3QtcjYuY3JsMCEGA1Ud
IAQaMBgwCAYGZ4EMAQIBMAwGCisGAQQBoDIKAQMwDQYJKoZIhvcNAQELBQADggIB
AB/uvBuZf4CiuSahwiXn4geF52roAH+6jxsEPTXTfb7bbeMDXsYgRRsOTNA70ruZ
Tnz5DfFMuBhNoFhIFb0qR1izdy6VkdKOqFPNF2dOFI1EcnY9l2ory9mrzHqVbrL4
vzUd17FLUVyjTVU7PAv4nxyhnO1GTeT83YlrdRF31NyR6bvZVTEERHmpbWSgeveJ
LRtaMzlGWiLZ8IwkH7o6GH3jp/KPtDW4Npu8w64HrRZdN2pqQhi7+YKwfHM7H+2U
dM1BGN0sjOWMVbMSB9MtCsleS2Mb7TRZEbOHxECJLLIluQypZr7Pol3+hAqrhyKI
k+6y+Da0NeDuWxW59Ku4NvClqW1UFX1SpfNGhzVfp/CH+vPM1tySomx2jE0EnYZu
GwVucXPBsp5nUWqUV9+143glVuS7GTg9hFPjNBInn17HbCoIIQIOzj5Vd9bK3A9U
GxXNpwenDHEalCsD/4eQYDHPhFE7sNe0D/OXu+FAM02VZkARx37Jp4bDdujvgL9P
vZPR3wThvDN1CTU8Bc3xea3yKFAraKcPZLkhReQUAm2VpR+HSJRPlUpYizlF9WkL
h3KcAVCBJWvnOkVwxyU5QJMcnwW95JlOtx+9100GL99jHE5rs3gXp7F4bg8H01QT
9jVOhBBmQ7nQoXuwI0tqal2QUqZz3eeu62CU7xBwtfYR
-----END CERTIFICATE-----

View File

@@ -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>