From 2cb72d8033cc96d3872bb03bda408d85749829b3 Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Tue, 4 Aug 2026 16:51:23 +0300 Subject: [PATCH 1/4] fix: restore API TLS verification --- .../sdk/internal/di/module/NetworkModule.kt | 15 ----------- .../internal/di/module/NetworkModuleTest.kt | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt diff --git a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt index ddf1d75e1..aa4e1ac0a 100644 --- a/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt +++ b/sdk/src/main/java/com/qonversion/android/sdk/internal/di/module/NetworkModule.kt @@ -32,12 +32,7 @@ import okhttp3.Cache import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory -import java.security.SecureRandom -import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager @Module internal class NetworkModule { @@ -86,21 +81,11 @@ internal class NetworkModule { context: Application, interceptor: NetworkInterceptor ): OkHttpClient { - val trustAllCerts = arrayOf(object : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) {} - override fun checkServerTrusted(chain: Array, authType: String) {} - override fun getAcceptedIssuers(): Array = arrayOf() - }) - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustAllCerts, SecureRandom()) - return OkHttpClient.Builder() .cache(Cache(context.cacheDir, CACHE_SIZE)) .readTimeout(TIMEOUT, TimeUnit.SECONDS) .connectTimeout(TIMEOUT, TimeUnit.SECONDS) .addInterceptor(interceptor) - .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) - .hostnameVerifier { _, _ -> true } .build() } diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt new file mode 100644 index 000000000..81674a730 --- /dev/null +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt @@ -0,0 +1,25 @@ +package com.qonversion.android.sdk.internal.di.module + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import com.qonversion.android.sdk.internal.api.NetworkInterceptor +import io.mockk.mockk +import org.junit.Assert.assertFalse +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.net.ssl.SSLSession + +@RunWith(RobolectricTestRunner::class) +internal class NetworkModuleTest { + @Test + fun `api client rejects a hostname that does not match the certificate`() { + val application = ApplicationProvider.getApplicationContext() + val interceptor = mockk(relaxed = true) + val sslSession = mockk(relaxed = true) + + val client = NetworkModule().provideOkHttpClient(application, interceptor) + + assertFalse(client.hostnameVerifier.verify("attacker.invalid", sslSession)) + } +} From 5b9ffff954982da66f72a7796937d7dd7d1141a2 Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Tue, 4 Aug 2026 16:59:39 +0300 Subject: [PATCH 2/4] test: use OkHttp hostname verifier accessor --- .../android/sdk/internal/di/module/NetworkModuleTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt index 81674a730..51b540a86 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt @@ -20,6 +20,6 @@ internal class NetworkModuleTest { val client = NetworkModule().provideOkHttpClient(application, interceptor) - assertFalse(client.hostnameVerifier.verify("attacker.invalid", sslSession)) + assertFalse(client.hostnameVerifier().verify("attacker.invalid", sslSession)) } } From fa673c0fc372a0779b62715032e057f676c315ae Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Tue, 4 Aug 2026 17:07:46 +0300 Subject: [PATCH 3/4] test: avoid mocking JDK TLS session --- .../sdk/internal/di/module/NetworkModuleTest.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt b/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt index 51b540a86..1331ac0ff 100644 --- a/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt +++ b/sdk/src/test/java/com/qonversion/android/sdk/internal/di/module/NetworkModuleTest.kt @@ -8,6 +8,8 @@ import org.junit.Assert.assertFalse import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.lang.reflect.Proxy +import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSession @RunWith(RobolectricTestRunner::class) @@ -16,7 +18,15 @@ internal class NetworkModuleTest { fun `api client rejects a hostname that does not match the certificate`() { val application = ApplicationProvider.getApplicationContext() val interceptor = mockk(relaxed = true) - val sslSession = mockk(relaxed = true) + val sslSession = Proxy.newProxyInstance( + NetworkModuleTest::class.java.classLoader, + arrayOf(SSLSession::class.java), + ) { _, method, _ -> + if (method.name == "getPeerCertificates") { + throw SSLPeerUnverifiedException("no certificate for mismatched host") + } + null + } as SSLSession val client = NetworkModule().provideOkHttpClient(application, interceptor) From 907c3dc7fecde3672d67bde372b7777c34b7987b Mon Sep 17 00:00:00 2001 From: Daniil Fadeev Date: Wed, 5 Aug 2026 12:56:06 +0300 Subject: [PATCH 4/4] fix: enforce TLS verification in NoCodes --- nocodes/build.gradle | 4 +- .../networkClient/NetworkClientImpl.kt | 19 -------- .../networkClient/NetworkClientImplTest.kt | 47 +++++++++++++++++++ 3 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 nocodes/src/test/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImplTest.kt diff --git a/nocodes/build.gradle b/nocodes/build.gradle index 626d2985a..3b262cccb 100644 --- a/nocodes/build.gradle +++ b/nocodes/build.gradle @@ -67,10 +67,12 @@ dependencies { api project(':sdk') + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:core:1.5.0' androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation "androidx.test:rules:1.5.0" androidTestImplementation 'androidx.test.ext:junit:1.1.5' } -apply from: "../scripts/maven-release.gradle" \ No newline at end of file +apply from: "../scripts/maven-release.gradle" diff --git a/nocodes/src/main/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImpl.kt b/nocodes/src/main/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImpl.kt index fb4ee5280..61d359678 100644 --- a/nocodes/src/main/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImpl.kt +++ b/nocodes/src/main/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImpl.kt @@ -20,12 +20,6 @@ import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.net.MalformedURLException import java.net.URL -import java.security.SecureRandom -import java.security.cert.X509Certificate -import javax.net.ssl.HttpsURLConnection -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager private const val NETWORK_ENCODING = "utf-8" @@ -66,19 +60,6 @@ internal class NetworkClientImpl( return try { val connection = url.openConnection() as HttpURLConnection - // Trust all certificates for staging - if (connection is HttpsURLConnection) { - val trustAllCerts = arrayOf(object : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) {} - override fun checkServerTrusted(chain: Array, authType: String) {} - override fun getAcceptedIssuers(): Array = arrayOf() - }) - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustAllCerts, SecureRandom()) - connection.sslSocketFactory = sslContext.socketFactory - connection.setHostnameVerifier { _, _ -> true } - } - // Set smart timeout based on fallback availability val timeout = if (isFallbackAvailable) { TimeoutConstants.FALLBACK_AVAILABLE_TIMEOUT diff --git a/nocodes/src/test/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImplTest.kt b/nocodes/src/test/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImplTest.kt new file mode 100644 index 000000000..eda788bb2 --- /dev/null +++ b/nocodes/src/test/java/io/qonversion/nocodes/internal/networkLayer/networkClient/NetworkClientImplTest.kt @@ -0,0 +1,47 @@ +package io.qonversion.nocodes.internal.networkLayer.networkClient + +import io.qonversion.nocodes.internal.common.serializers.Serializer +import org.junit.Assert.assertSame +import org.junit.Test +import java.net.URL +import java.net.URLConnection +import java.net.URLStreamHandler +import java.security.Principal +import java.security.cert.Certificate +import javax.net.ssl.HttpsURLConnection + +internal class NetworkClientImplTest { + @Test + fun `https connections retain platform TLS verification`() { + lateinit var platformConnection: TestHttpsURLConnection + val url = URL(null, "https://api.qonversion.io", object : URLStreamHandler() { + override fun openConnection(url: URL): URLConnection { + return TestHttpsURLConnection(url).also { platformConnection = it } + } + }) + val platformSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory() + val platformHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier() + + val connection = NetworkClientImpl(UnusedSerializer()).connect(url) as HttpsURLConnection + + assertSame(platformConnection, connection) + assertSame(platformSocketFactory, connection.sslSocketFactory) + assertSame(platformHostnameVerifier, connection.hostnameVerifier) + } + + private class UnusedSerializer : Serializer { + override fun serialize(data: Map): String = error("not used") + override fun deserialize(payload: String): Any = error("not used") + } + + private class TestHttpsURLConnection(url: URL) : HttpsURLConnection(url) { + override fun connect() = Unit + override fun disconnect() = Unit + override fun usingProxy(): Boolean = false + override fun getCipherSuite(): String = "" + override fun getLocalCertificates(): Array? = null + override fun getServerCertificates(): Array = emptyArray() + override fun getPeerPrincipal(): Principal? = null + override fun getLocalPrincipal(): Principal? = null + } +}