diff --git a/app/build.gradle b/app/build.gradle index 17f56adf57..3e88bb05e4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -303,7 +303,6 @@ dependencies { implementation 'androidx.media3:media3-exoplayer:1.4.0' implementation 'androidx.media3:media3-ui:1.4.0' implementation 'org.conscrypt:conscrypt-android:2.5.2' - implementation 'org.signal:aesgcmprovider:0.0.3' implementation 'io.github.webrtc-sdk:android:125.6422.06.1' implementation "me.leolin:ShortcutBadger:1.1.16" implementation 'se.emilsjolander:stickylistheaders:2.7.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff8031044f..25dbdcf341 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,7 +85,6 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:largeHeap="true" - android:networkSecurityConfig="@xml/network_security_configuration" android:supportsRtl="true" android:theme="@style/Theme.Session.DayNight" tools:replace="android:allowBackup,android:label" > diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 99debd596c..9bc710a7bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -64,7 +64,6 @@ import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.ThreadUtils; -import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.configs.ConfigUploader; import org.thoughtcrime.securesms.database.EmojiSearchDatabase; @@ -119,6 +118,8 @@ import dagger.hilt.android.HiltAndroidApp; import kotlin.Deprecated; import kotlin.Unit; +import kotlinx.coroutines.Dispatchers; +import kotlinx.coroutines.DispatchersKt; import network.loki.messenger.BuildConfig; import network.loki.messenger.R; @@ -394,22 +395,22 @@ public boolean isAppVisible() { // Loki private void initializeSecurityProvider() { - try { - Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Failed to find AesGcmCipher class"); - throw new ProviderInitializationException(); - } - - int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1); - Log.i(TAG, "Installed AesGcmProvider: " + aesPosition); - - if (aesPosition < 0) { - Log.e(TAG, "Failed to install AesGcmProvider()"); - throw new ProviderInitializationException(); - } - - int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 2); +// try { +// Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); +// } catch (ClassNotFoundException e) { +// Log.e(TAG, "Failed to find AesGcmCipher class"); +// throw new ProviderInitializationException(); +// } +// +// int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1); +// Log.i(TAG, "Installed AesGcmProvider: " + aesPosition); +// +// if (aesPosition < 0) { +// Log.e(TAG, "Failed to install AesGcmProvider()"); +// throw new ProviderInitializationException(); +// } + + int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 1); Log.i(TAG, "Installed Conscrypt provider: " + conscryptPosition); if (conscryptPosition < 0) { @@ -549,4 +550,7 @@ public void wakeUpDeviceAndDismissKeyguardIfRequired() { } } + static { + System.setProperty(DispatchersKt.IO_PARALLELISM_PROPERTY_NAME, "10"); + } } diff --git a/app/src/main/res/xml/network_security_configuration.xml b/app/src/main/res/xml/network_security_configuration.xml deleted file mode 100644 index b0392ade39..0000000000 --- a/app/src/main/res/xml/network_security_configuration.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - 127.0.0.1 - - - seed1.getsession.org - - - - - - seed2.getsession.org - - - - - - seed3.getsession.org - - - - - - public.loki.foundation - - \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3d92c73210..7b97feabfa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,6 +19,7 @@ org.gradle.jvmargs=-Xmx3096M -Dkotlin.daemon.jvm.options\="-Xmx3096M" gradlePluginVersion=8.5.2 googleServicesVersion=4.3.12 kotlinVersion=2.0.0 +ktorVersion=3.0.3 kspVersion=2.0.0-1.0.23 navVersion=2.8.0-beta05 android.useAndroidX=true diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index f1634d8e2e..905cbf9c90 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select +import kotlinx.coroutines.selects.onTimeout import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all import nl.komponents.kovenant.functional.bind @@ -81,7 +82,7 @@ object SnodeAPI { private const val minimumSnodePoolCount = 12 private const val minimumSwarmSnodeCount = 3 // Use port 4433 to enforce pinned certificates - private val seedNodePort = 4443 + private val seedNodePort = 443 private val seedNodePool = if (SnodeModule.shared.useTestNet) setOf( "http://public.loki.foundation:38157" diff --git a/libsession/src/main/java/org/session/libsession/utilities/Util.kt b/libsession/src/main/java/org/session/libsession/utilities/Util.kt index e0c47c34c7..493dce835f 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Util.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -45,7 +45,7 @@ object Util { @JvmStatic @Throws(IOException::class) fun copy(`in`: InputStream, out: OutputStream?): Long { - val buffer = ByteArray(8192) + val buffer = ByteArray(24560) var read: Int var total: Long = 0 while (`in`.read(buffer).also { read = it } != -1) { diff --git a/libsignal/build.gradle b/libsignal/build.gradle index 9fac9541d9..1c2e589294 100644 --- a/libsignal/build.gradle +++ b/libsignal/build.gradle @@ -30,6 +30,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" + + implementation("io.ktor:ktor-client-cio:$ktorVersion") + testImplementation "junit:junit:$junitVersion" testImplementation "org.assertj:assertj-core:3.11.1" testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0" diff --git a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java index 19996c17cb..a20f4c17f3 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java @@ -19,6 +19,7 @@ import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; +//TODO: Rewrite this class so that it doesn't rely on specific cipher length assumptions public class ProfileCipherInputStream extends FilterInputStream { private final Cipher cipher; @@ -35,12 +36,9 @@ public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException { Util.readFully(in, nonce); this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce)); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (NoSuchPaddingException e) { - throw new AssertionError(e); - } catch (InvalidAlgorithmParameterException e) { - throw new AssertionError(e); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | + InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); } catch (InvalidKeyException e) { throw new IOException(e); } @@ -67,21 +65,21 @@ public int read(byte[] output, int outputOffset, int outputLength) throws IOExce synchronized (CIPHER_LOCK) { if (read == -1) { if (cipher.getOutputSize(0) > outputLength) { - throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength); + throw new RuntimeException("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength); } finished = true; return cipher.doFinal(output, outputOffset); } else { if (cipher.getOutputSize(read) > outputLength) { - throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength); + throw new RuntimeException("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength); } return cipher.update(ciphertext, 0, read, output, outputOffset); } } } catch (IllegalBlockSizeException | ShortBufferException e) { - throw new AssertionError(e); + throw new RuntimeException(e); } catch (BadPaddingException e) { throw new IOException(e); } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt index fc29d07916..a459fa2921 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt @@ -1,72 +1,50 @@ package org.session.libsignal.utilities +import android.annotation.SuppressLint +import android.net.http.X509TrustManagerExtensions import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import okhttp3.Call -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response -import org.session.libsignal.utilities.Util.SECURE_RANDOM +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.timeout +import io.ktor.client.request.headers +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsBytes +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager object HTTP { var isConnectedToNetwork: (() -> Boolean) = { false } - private val seedNodeConnection by lazy { - - OkHttpClient().newBuilder() - .callTimeout(timeout, TimeUnit.SECONDS) - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .build() + private val seedNodeClient: HttpClient by lazy { + HttpClient(CIO) } - private val defaultConnection by lazy { - // Snode to snode communication uses self-signed certificates but clients can safely ignore this - val trustManager = object : X509TrustManager { + private val serviceNodeClient: HttpClient by lazy { + HttpClient(CIO) { + engine { + https { + @SuppressLint("CustomX509TrustManager") + trustManager = object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array?, authorizationType: String?) { } - override fun checkClientTrusted(chain: Array?, authorizationType: String?) { } - override fun checkServerTrusted(chain: Array?, authorizationType: String?) { } - override fun getAcceptedIssuers(): Array { return arrayOf() } - } - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM) - OkHttpClient().newBuilder() - .sslSocketFactory(sslContext.socketFactory, trustManager) - .hostnameVerifier { _, _ -> true } - .callTimeout(timeout, TimeUnit.SECONDS) - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .build() - } + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted(chain: Array?, authorizationType: String?) { } - private fun getDefaultConnection(timeout: Long): OkHttpClient { - // Snode to snode communication uses self-signed certificates but clients can safely ignore this - val trustManager = object : X509TrustManager { + override fun getAcceptedIssuers(): Array { return arrayOf() } + } + } - override fun checkClientTrusted(chain: Array?, authorizationType: String?) { } - override fun checkServerTrusted(chain: Array?, authorizationType: String?) { } - override fun getAcceptedIssuers(): Array { return arrayOf() } + requestTimeout = TimeUnit.SECONDS.toMillis(timeout) + } } - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM) - return OkHttpClient().newBuilder() - .sslSocketFactory(sslContext.socketFactory, trustManager) - .hostnameVerifier { _, _ -> true } - .callTimeout(timeout, TimeUnit.SECONDS) - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .build() } private const val timeout: Long = 120 @@ -105,41 +83,41 @@ object HTTP { * Sync. Don't call from the main thread. */ suspend fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray { - val request = Request.Builder().url(url) - .removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value - .removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // Set a fake value - when (verb) { - Verb.GET -> request.get() - Verb.PUT, Verb.POST -> { - if (body == null) { throw Exception("Invalid request body.") } - val contentType = "application/json; charset=utf-8".toMediaType() - @Suppress("NAME_SHADOWING") val body = RequestBody.create(contentType, body) - if (verb == Verb.PUT) request.put(body) else request.post(body) - } - Verb.DELETE -> request.delete() - } - return try { - when { - // Custom timeout - timeout != HTTP.timeout -> { - if (useSeedNodeConnection) { - throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.") - } - getDefaultConnection(timeout) + val client = if (useSeedNodeConnection) seedNodeClient else serviceNodeClient + + try { + val response = client.request(url) { + method = HttpMethod.parse(verb.rawValue) + + headers { + remove("User-Agent") + remove("Accept-Language") + + append("User-Agent", "WhatsApp") + append("Accept-Language", "en-us") } - useSeedNodeConnection -> seedNodeConnection - else -> defaultConnection - }.newCall(request.build()).await().use { response -> - when (val statusCode = response.code) { - 200 -> response.body!!.bytes() - else -> { - Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.") - throw HTTPRequestFailedException(statusCode, null) - } + + if (verb == Verb.PUT || verb == Verb.POST) { + check(body != null) { "Invalid request body." } + contentType(ContentType.Application.Json) + setBody(body) + } + + timeout { + requestTimeoutMillis = TimeUnit.SECONDS.toMillis(timeout) + } + } + + when (val code = response.status) { + HttpStatusCode.OK -> return response.bodyAsBytes() + else -> { + Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $code.") + throw HTTPRequestFailedException(code.value, null) } } + } catch (exception: Exception) { - Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.") + Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.", exception) if (!isConnectedToNetwork()) { throw HTTPNoNetworkException() } @@ -147,13 +125,4 @@ object HTTP { throw HTTPRequestFailedException(0, null, "HTTP request failed due to: ${exception.message}") } } - - @Suppress("OPT_IN_USAGE") - private val httpCallDispatcher = Dispatchers.IO.limitedParallelism(15) - - private suspend fun Call.await(): Response { - return withContext(httpCallDispatcher) { - execute() - } - } }