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
|
* Расшифровка MESSAGES attachment blob
|
||||||
* Формат: ivBase64:ciphertextBase64
|
* Формат: ivBase64:ciphertextBase64
|
||||||
* Использует PBKDF2 + AES-256-CBC + zlib decompression
|
* Использует 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(
|
fun decryptAttachmentBlob(
|
||||||
encryptedData: String,
|
encryptedData: String,
|
||||||
@@ -520,20 +558,16 @@ object MessageCrypto {
|
|||||||
myPrivateKey: String
|
myPrivateKey: String
|
||||||
): String? {
|
): String? {
|
||||||
return try {
|
return try {
|
||||||
|
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlob: data length=${encryptedData.length}, key length=${encryptedKey.length}")
|
||||||
|
|
||||||
// 1. Расшифровываем ChaCha ключ (как для сообщений)
|
// 1. Расшифровываем ChaCha ключ (как для сообщений)
|
||||||
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes")
|
||||||
|
|
||||||
// 2. Конвертируем key+nonce в строку используя bytesToJsUtf8String
|
// 2. Используем новую функцию
|
||||||
// чтобы совпадало с JS Buffer.toString('utf-8') который заменяет
|
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
|
||||||
// невалидные 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)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,13 +593,20 @@ object MessageCrypto {
|
|||||||
*/
|
*/
|
||||||
private fun decryptWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
|
private fun decryptWithPBKDF2Key(encryptedData: String, pbkdf2Key: ByteArray): String? {
|
||||||
return try {
|
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(":")
|
val parts = encryptedData.split(":")
|
||||||
|
android.util.Log.d("MessageCrypto", "🔓 Split parts: ${parts.size}")
|
||||||
if (parts.size != 2) {
|
if (parts.size != 2) {
|
||||||
|
android.util.Log.e("MessageCrypto", "❌ Invalid format: expected 2 parts, got ${parts.size}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val iv = android.util.Base64.decode(parts[0], android.util.Base64.DEFAULT)
|
val iv = android.util.Base64.decode(parts[0], android.util.Base64.DEFAULT)
|
||||||
val ciphertext = android.util.Base64.decode(parts[1], 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 расшифровка
|
// AES-256-CBC расшифровка
|
||||||
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
|
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
@@ -573,6 +614,7 @@ object MessageCrypto {
|
|||||||
val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
|
val ivSpec = javax.crypto.spec.IvParameterSpec(iv)
|
||||||
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
||||||
val decrypted = cipher.doFinal(ciphertext)
|
val decrypted = cipher.doFinal(ciphertext)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔓 AES decrypted: ${decrypted.size} bytes")
|
||||||
|
|
||||||
// Zlib декомпрессия
|
// Zlib декомпрессия
|
||||||
val inflater = java.util.zip.Inflater()
|
val inflater = java.util.zip.Inflater()
|
||||||
@@ -585,8 +627,11 @@ object MessageCrypto {
|
|||||||
}
|
}
|
||||||
inflater.end()
|
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) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("MessageCrypto", "❌ decryptWithPBKDF2Key failed: ${e.message}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ data class TransportState(
|
|||||||
object TransportManager {
|
object TransportManager {
|
||||||
private const val TAG = "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 var transportServer: String? = null
|
||||||
|
|
||||||
private val _uploading = MutableStateFlow<List<TransportState>>(emptyList())
|
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 для скачивания файла
|
* @return Tag для скачивания файла
|
||||||
*/
|
*/
|
||||||
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
|
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
|
||||||
val server = transportServer
|
val server = getActiveServer()
|
||||||
?: throw IllegalStateException("Transport server is not set")
|
|
||||||
|
|
||||||
Log.d(TAG, "📤 Uploading file: $id")
|
Log.d(TAG, "📤 Uploading file: $id to $server")
|
||||||
|
|
||||||
// Добавляем в список загрузок
|
// Добавляем в список загрузок
|
||||||
_uploading.value = _uploading.value + TransportState(id, 0)
|
_uploading.value = _uploading.value + TransportState(id, 0)
|
||||||
@@ -139,10 +150,9 @@ object TransportManager {
|
|||||||
* @return Содержимое файла
|
* @return Содержимое файла
|
||||||
*/
|
*/
|
||||||
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
|
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
|
||||||
val server = transportServer
|
val server = getActiveServer()
|
||||||
?: throw IllegalStateException("Transport server is not set")
|
|
||||||
|
|
||||||
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)
|
_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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
|
<!-- HTTP для локального сервера -->
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
<domain includeSubdomains="true">46.28.71.12</domain>
|
<domain includeSubdomains="true">46.28.71.12</domain>
|
||||||
</domain-config>
|
</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>
|
</network-security-config>
|
||||||
|
|||||||
Reference in New Issue
Block a user