diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 64bc93cb6f..6b58f8520b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -342,6 +342,9 @@ licensee { ignoreDependencies("com.github.matrix-org", "matrix-analytics-events") // Ignore dependency that are not third-party licenses to us. ignoreDependencies(groupId = "io.element.android") + // The rustls-platform-verifier dependency does not provide a license file in their maven repo, but there are MIT and Apache licenses in + // the source code repo: see https://github.com/rustls/rustls-platform-verifier + ignoreDependencies(groupId = "org.rustls", artifactId = "rustls-platform-verifier") } fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) { diff --git a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt index 2a844b1331..910b2851e6 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/PlatformInitializer.kt @@ -13,6 +13,7 @@ import android.system.Os import androidx.startup.Initializer import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.x.di.AppBindings @@ -41,9 +42,18 @@ class PlatformInitializer : Initializer { sdkSentryDsn = appBindings.sentrySdkDsn()?.value?.takeIf { it.isNotBlank() }, ) bugReporter.setCurrentTracingLogLevel(logLevel.name) + + // Init the tracing service with the configuration so we have logging enabled as soon as possible platformService.init(tracingConfiguration) // Also set env variable for rust back trace Os.setenv("RUST_BACKTRACE", "1", true) + + // Init the SDK with the Android Context, so it can be used with rustls and other Android specific features + runCatchingExceptions { + org.matrix.rustsdk.Android.init(context) + }.onFailure { + Timber.e(it, "Failed to init the SDK with an Android Context. The app probably won't work properly.") + } } override fun dependencies(): List>> = mutableListOf() diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index 96add8d148..98b80d94cb 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -30,5 +30,384 @@ home lan localdomain + + + + abitabdv.crl.certum.pl + abitabev.crl.certum.pl + abitabov.crl.certum.pl + alpiro.crl.sectigo.com + anticdv.crl.certum.pl + antrustovsslg2r30ca.crl.certum.pl + autontrustprodvtlsg2r31ca.crl.certum.pl + bitcert.crl.sectigo.com + btdvtlsr35g2ca.crl.certum.pl + c.cf-b.ssl.com + c.cf-i.ssl.com + c.pki.goog + ca1.agid.gov.it + cdn.d-trust-cloudcrl.net + cdp.disig.sk + cdp.elektronicznypodpis.pl + cdp.geotrust.com + cdp.rapidssl.com + cdp.thawte.com + cdp1.disig.sk + cdp1.pca.dfn.de + cdpa.digitalcertvalidation.com + cdpb.digitalcertvalidation.com + cdpc.digitalcertvalidation.com + cdpd.digitalcertvalidation.com + cdpe.digitalcertvalidation.com + cdpf.digitalcertvalidation.com + cdpg.digitalcertvalidation.com + cdph.digitalcertvalidation.com + cdpi.digitalcertvalidation.com + certcloud.crl.trust-provider.com + certdata.crl.sectigo.com + certera.crl.sectigo.com + certificates.godaddy.com + certum-crl.wt.trustasia.com + certum.crl.sheca.com + certumdvtlsg2e39ca.crl.certum.pl + certumdvtlsg2r39ca.crl.certum.pl + certumevtlsg2e39ca.crl.certum.pl + certumevtlsg2r39ca.crl.certum.pl + certumovtlsg2e39ca.crl.certum.pl + certumovtlsg2r39ca.crl.certum.pl + cfcadveccca.crl.certum.pl + cfcadvrsaca.crl.certum.pl + cfcaeveccca.crl.certum.pl + cfcaevrsaca.crl.certum.pl + cfcaoveccca.crl.certum.pl + cfcaovrsaca.crl.certum.pl + cnssldvtlsg2r36ca.crl.certum.pl + cnsslovtlsg2r36ca.crl.certum.pl + crl-01.r1.ica.amazontrust.com + crl-01.r1.ica.amznts.eu + crl-c.emsign.com + crl-pro.litessl.com + crl.anf.es + crl.apple.com + crl.businessid.telesec.de + crl.buypass.no + crl.catrust.oemssl.cn + crl.certigna.fr + crl.certsign.ro + crl.certum.pl + crl.cfca.com.cn + crl.cntrus.oemssl.cn + crl.cnwebtrust.oemssl.cn + crl.comodoca.com + crl.crlocsp.cn + crl.cybertrust.ne.jp + crl.d-trust.net + crl.dhimyotis.com + crl.digicert.cn + crl.digicert.eu + crl.e-szigno.hu + crl.e2m01.amazontrust.com + crl.e2m01.eu.amazontrust.com + crl.e2m02.amazontrust.com + crl.e2m03.amazontrust.com + crl.e2m04.amazontrust.com + crl.e2s14.amazontrust.com + crl.e2s15.amazontrust.com + crl.e2s16.amazontrust.com + crl.e2s17.amazontrust.com + crl.e2s18.amazontrust.com + crl.e2s19.amazontrust.com + crl.e2s20.amazontrust.com + crl.e2s21.amazontrust.com + crl.e3m01.amazontrust.com + crl.e3m01.eu.amazontrust.com + crl.e3m02.amazontrust.com + crl.e3m03.amazontrust.com + crl.e3m04.amazontrust.com + crl.e3s22.amazontrust.com + crl.e3s23.amazontrust.com + crl.e3s24.amazontrust.com + crl.e3s25.amazontrust.com + crl.e3s26.amazontrust.com + crl.e3s27.amazontrust.com + crl.e3s28.amazontrust.com + crl.e3s29.amazontrust.com + crl.emsign.com + crl.ensuredca.com + crl.firmaprofesional.com + crl.gdca.com.cn + crl.global.sheca.com + crl.globalsign.com + crl.globalsign.net + crl.godaddy.com + crl.harica.gr + crl.izenpe.com + crl.litessl.com + crl.netsolssl.com + crl.oem.trustca.net + crl.pcsp.telesec.de + crl.pki.goog + crl.quovadisglobal.com + crl.r1001.amazontrust.com + crl.r2001.amazontrust.com + crl.r2m01.amazontrust.com + crl.r2m01.eu.amazontrust.com + crl.r2m02.amazontrust.com + crl.r2m03.amazontrust.com + crl.r2m04.amazontrust.com + crl.r2s18.amazontrust.com + crl.r2s19.amazontrust.com + crl.r2s20.amazontrust.com + crl.r2s21.amazontrust.com + crl.r2s22.amazontrust.com + crl.r2s23.amazontrust.com + crl.r2s24.amazontrust.com + crl.r2s25.amazontrust.com + crl.r3001.amazontrust.com + crl.r4001.amazontrust.com + crl.r4m01.amazontrust.com + crl.r4m02.amazontrust.com + crl.r4m03.amazontrust.com + crl.r4m04.amazontrust.com + crl.root-e1.certainly.com + crl.root-r1.certainly.com + crl.rootca1.amazontrust.com + crl.rootca2.amazontrust.com + crl.rootca3.amazontrust.com + crl.rootca4.amazontrust.com + crl.rztrust.oemssl.cn + crl.sbca.telesec.de + crl.sectigo.com + crl.sectigochina.com + crl.serverid.telesec.de + crl.starfieldtech.com + crl.swisssign.ch + crl.trust-provider.cn + crl.trust-provider.com + crl.tuntrust.tn + crl.usertrust.com + crl.wt.trustasia.com + crl.xinchacha.oemssl.cn + crl.zw.oemssl.cn + crl05.actalis.it + crl06.actalis.it + crl09.actalis.it + crl1.ecert.gov.hk + crl13.actalis.it + crl14.actalis.it + crl15.actalis.it + crl2.microsoft.com + crl3.digicert.com + crl4.digicert.com + crls.certainly.com + crls.ssl.com + cyberfolks2.crl.certum.pl + depo.kamusm.gov.tr + digitaltrust.crl.sectigo.com + dnencrypt.crl.sectigo.com + dvtlsca2025-crl.e-szigno.hu + dvtlsca2026-crl.e-szigno.hu + e5.c.lencr.org + e6.c.lencr.org + e7.c.lencr.org + e8.c.lencr.org + ec2ca2017-crl1.e-szigno.hu + ec2ca2017-crl2.e-szigno.hu + ec2ca2017-crl3.e-szigno.hu + ec2sslca2017-crl1.e-szigno.hu + ec2sslca2017-crl2.e-szigno.hu + ec2sslca2017-crl3.e-szigno.hu + ec3ca2017-crl1.e-szigno.hu + ec3ca2017-crl2.e-szigno.hu + ec3ca2017-crl3.e-szigno.hu + ec3sslca2017-crl1.e-szigno.hu + ec3sslca2017-crl2.e-szigno.hu + ec3sslca2017-crl3.e-szigno.hu + eca.hinet.net + edvtlsca2023-crl.e-szigno.hu + edvtlsca2025-crl.e-szigno.hu + edvtlsca2026-crl.e-szigno.hu + emudhra.crl.sectigo.com + eosslca2017-crl1.e-szigno.hu + eosslca2017-crl2.e-szigno.hu + eosslca2017-crl3.e-szigno.hu + eovtlsca2023-crl.e-szigno.hu + eovtlsca2025-crl.e-szigno.hu + eovtlsca2026-crl.e-szigno.hu + eqca2017-crl1.e-szigno.hu + eqca2017-crl2.e-szigno.hu + eqca2017-crl3.e-szigno.hu + eqcpca2017-crl1.e-szigno.hu + eqcpca2017-crl2.e-szigno.hu + eqcpca2017-crl3.e-szigno.hu + eqoca2017-crl1.e-szigno.hu + eqoca2017-crl2.e-szigno.hu + eqoca2017-crl3.e-szigno.hu + eqtlsca2018-crl1.e-szigno.hu + eqtlsca2018-crl2.e-szigno.hu + eqtlsca2018-crl3.e-szigno.hu + eqtlsca2023-crl.e-szigno.hu + eqtlsca2025-crl.e-szigno.hu + eqtlsca2026-crl.e-szigno.hu + esafer.crl.sectigo.com + eue2m1.crl.root.amznts.eu + eue3m1.crl.root.amznts.eu + eur2m1.crl.root.amznts.eu + eveccca.crl.certum.pl + evrsaca.crl.certum.pl + gdcadv.crl.certum.pl + gdcaev.crl.certum.pl + gdcaov.crl.certum.pl + gdcatrustauthdvtlsg3r31ca.crl.certum.pl + gdcatrustauthevtlsg3r31ca.crl.certum.pl + gdcatrustauthovtlsg3r31ca.crl.certum.pl + geant.crl.sectigo.com + genious.crl.sectigo.com + geossldvtlsr34g2ca.crl.certum.pl + geosslevtlsr34g2ca.crl.certum.pl + geosslovtlsr34g2ca.crl.certum.pl + globaltrust.crl.sectigo.com + globessl.crl.sectigo.com + gogetssldv.crl.certum.pl + gogetsslev.crl.certum.pl + gogetsslov.crl.certum.pl + homepldvtlsg2r35ca.crl.certum.pl + homepldvtlsg2r36ca.crl.certum.pl + homeplovtlsg2r35ca.crl.certum.pl + homeplovtlsg2r36ca.crl.certum.pl + httpcrl.trust.telia.com + ica.navercloudtrust.com + ica.navercorp.com + issauth.crl.sectigo.com + itrusdv2.crl.certum.pl + itrusdvtlsr35g2ca.crl.certum.pl + itrusov2.crl.certum.pl + itrusovtlsr35g2ca.crl.certum.pl + itso.crl.sectigo.com + joyssldvtlsg2r33ca.crl.certum.pl + joysslovtlsg2r33ca.crl.certum.pl + kingnettechdv.crl.certum.pl + kingnettechev.crl.certum.pl + kingnettechov.crl.certum.pl + kr.crl.digicert.com + lh2.crl.certum.pl + namecheap.crl.sectigo.com + nazwassldvtlsg2e29ca.crl.certum.pl + nazwassldvtlsg2r29ca.crl.certum.pl + netartcadv2.crl.certum.pl + netartssldvtlsg2e31ca.crl.certum.pl + nijimo.crl.sectigo.com + nyalabscadv.crl.certum.pl + nyatworkdv.crl.certum.pl + omitsecurity.crl.sectigo.com + onesign.crl.sectigo.com + osslca2016-crl1.e-szigno.hu + osslca2016-crl2.e-szigno.hu + osslca2016-crl3.e-szigno.hu + ovtlsca2025-crl.e-szigno.hu + ovtlsca2026-crl.e-szigno.hu + pki-crl.atos.net + pki.telesec.de + pkipro.certsign.ro + positiwise.crl.sectigo.com + psw.crl.sectigo.com + public.wisekey.com + qiduodv.crl.certum.pl + qtlsca2018-crl1.e-szigno.hu + qtlsca2018-crl2.e-szigno.hu + qtlsca2018-crl3.e-szigno.hu + qtlsca2026-crl.e-szigno.hu + r10.c.lencr.org + r11.c.lencr.org + r12.c.lencr.org + r13.c.lencr.org + rca.navercloudtrust.com + rca.navercorp.com + repo.pubcert.jprs.jp + repo1.secomtrust.net + repository.secomtrust.net + repository.tls.hinet.net + rootca.twca.com.tw + rootglobaldv.crl.certum.pl + rootnetworksdv2.crl.certum.pl + shenzhendv.crl.certum.pl + shenzhenev.crl.certum.pl + shenzhenov.crl.certum.pl + shoperdvtlsg2r34ca.crl.certum.pl + shuididv.crl.certum.pl + shuidiev.crl.certum.pl + shuidiov.crl.certum.pl + soomadv.crl.certum.pl + ssl2ca2016-crl1.e-szigno.hu + ssl2ca2016-crl2.e-szigno.hu + ssl2ca2016-crl3.e-szigno.hu + sslca2014-crl1.e-szigno.hu + sslca2014-crl2.e-szigno.hu + sslca2014-crl3.e-szigno.hu + sslcom.crl.certum.pl + ssllimiteddv.crl.certum.pl + sslserver.twca.com.tw + subca.crl.certum.pl + titrust.crl.sectigo.com + tlcdvtlsr34g2ca.crl.certum.pl + tlcevtlsr34g2ca.crl.certum.pl + tlcovtlsr34g2ca.crl.certum.pl + tlsrootca2025-crl.e-szigno.hu + trustasia.crl.certum.pl + trustasiadv.crl.certum.pl + trustasiadvtlsr35g2ca.crl.certum.pl + trustasiaev.crl.certum.pl + trustasiaevtlse35g2ca.crl.certum.pl + trustasiaevtlsr35g2ca.crl.certum.pl + trustasiaov.crl.certum.pl + trustasiaovtlse35g2ca.crl.certum.pl + trustasiaovtlsr35g2ca.crl.certum.pl + trustocean.crl.certum.pl + tstlser20.crl.telesec.de + tstlsrr23.crl.telesec.de + unitedtrustov.crl.certum.pl + validation.identrust.com + verokey.crl.sectigo.com + vtrusdvtlsg3r31ca.crl.certum.pl + vtrusovtlsg3r31ca.crl.certum.pl + webnic.crl.sectigo.com + whitessldvtlsg2e36ca.crl.certum.pl + wosign.crl.certum.pl + wotrus-dvca.crl.certum.pl + wotrus-evca.crl.certum.pl + wotrus-ovca.crl.certum.pl + wotrus.crl.sectigo.com + wtca-cafiles.itrus.com.cn + wtca-crl.itrus.com.cn + www.accv.es + www.cert.fnmt.es + www.d-trust.net + www.microsoft.com + x1.c.lencr.org + x2.c.lencr.org + xcctrustdvtlsg3r31ca.crl.certum.pl + xcctrustevtlsg3r31ca.crl.certum.pl + xcctrustovtlsg3r31ca.crl.certum.pl + xinchacha2dv.crl.certum.pl + xinchacha2ov.crl.certum.pl + xinchachatrustdvtlsg2r34ca.crl.certum.pl + xinchachatrustevtlsg2r34ca.crl.certum.pl + xinchachatrustovtlsg2r34ca.crl.certum.pl + xinnetdvtlsr34g2ca.crl.certum.pl + xinnetevtlsr34g2ca.crl.certum.pl + xinnetovtlsr34g2ca.crl.certum.pl + ye.c.lencr.org + ye1.c.lencr.org + ye2.c.lencr.org + yektadv.crl.certum.pl + yektaov.crl.certum.pl + yr.c.lencr.org + yr1.c.lencr.org + yr2.c.lencr.org + zerossl.crl.sectigo.com + ziwit.crl.sectigo.com + zycatrustdvtlsg3r31ca.crl.certum.pl + zycatrustevtlsg3r31ca.crl.certum.pl + zycatrustovtlsg3r31ca.crl.certum.pl diff --git a/app/src/main/res/xml/network_security_config_element.xml b/app/src/main/res/xml/network_security_config_element.xml new file mode 100644 index 0000000000..96add8d148 --- /dev/null +++ b/app/src/main/res/xml/network_security_config_element.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + localhost + 127.0.0.1 + + 10.0.2.2 + + onion + + + + home.arpa + local + test + + home + lan + localdomain + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb2c7bd59e..60ff0e6542 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -182,7 +182,8 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.09.26" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.09.28" +rustls_platform_verifier = "org.rustls:rustls-platform-verifier:0.2.0" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 2499b720e3..13920980a2 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { } else { debugImplementation(libs.matrix.sdk) } - implementation(projects.libraries.rustlsTls) + implementation(variantOf(libs.rustls.platform.verifier) { artifactType("aar") }) implementation(projects.appconfig) implementation(projects.features.enterprise.api) diff --git a/libraries/matrix/impl/src/main/kotlin/org/matrix/rustsdk/Android.kt b/libraries/matrix/impl/src/main/kotlin/org/matrix/rustsdk/Android.kt new file mode 100644 index 0000000000..8e4b03e1da --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/org/matrix/rustsdk/Android.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package org.matrix.rustsdk + +import android.content.Context + +/** + * This class is used to initialize the Rust SDK on Android and give it access to an Android [Context]. + */ +object Android { + init { + System.loadLibrary("matrix_sdk_ffi") + } + + @JvmStatic + external fun init(context: Context) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt index 68990560ce..d0e9592948 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt @@ -40,6 +40,8 @@ internal fun aRustEventTimelineItem( canBeRepliedTo: Boolean = true, reactions: List = emptyList(), lazyProvider: LazyTimelineItemProvider = FakeFfiLazyTimelineItemProvider(), + editSendState: EventSendState? = null, + redactionSendState: EventSendState? = null, ) = EventTimelineItem( isRemote = isRemote, eventOrTransactionId = eventOrTransactionId, @@ -59,4 +61,6 @@ internal fun aRustEventTimelineItem( canBeRepliedTo = canBeRepliedTo, reactions = reactions, lazyProvider = lazyProvider, + editSendState = editSendState, + redactionSendState = redactionSendState, ) diff --git a/libraries/rustls-tls/README.md b/libraries/rustls-tls/README.md deleted file mode 100644 index aa45fe6743..0000000000 --- a/libraries/rustls-tls/README.md +++ /dev/null @@ -1,9 +0,0 @@ -This module is a wrapper for the Android code distributed in the rustls-platform-verifier-android crate. - -To avoid the distribution mess that this library has (download a Rust crate, then search for it using Gradle and use it as local maven repo), -we previously just manually updated the AAR file instead using a script. This won't work for F-Droid because the AAR library is a black box with -no sources attached to it, so we can't use it like that. - -Instead, for the time being, we're adding the single `CertificateVerifier.kt` class this AAR had in it as part of our sources. - -When this file is updated, the [UPDATED.md](./UPDATED.md) file should be updated too with the commit SHA of the new version. diff --git a/libraries/rustls-tls/UPDATED.md b/libraries/rustls-tls/UPDATED.md deleted file mode 100644 index 10dba2dd53..0000000000 --- a/libraries/rustls-tls/UPDATED.md +++ /dev/null @@ -1,7 +0,0 @@ -Below is the commit SHA in [rustls-platform-verifier](https://github.com/rustls/rustls-platform-verifier) library used to update the code in this module: - -``` -996b1c903491641b17b3c9afb65d1352f6fc6b76 -``` - -Please update it after making manual changes. diff --git a/libraries/rustls-tls/build.gradle.kts b/libraries/rustls-tls/build.gradle.kts deleted file mode 100644 index 85f3f4c476..0000000000 --- a/libraries/rustls-tls/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import extension.buildConfigFieldBoolean - -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -plugins { - id("io.element.android-library") -} - -android { - namespace = "org.rustls.platformverifier" - - buildFeatures { - buildConfig = true - } - - defaultConfig { - buildConfigFieldBoolean("TEST", false) - } -} diff --git a/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt b/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt deleted file mode 100644 index b70413ffe5..0000000000 --- a/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt +++ /dev/null @@ -1,398 +0,0 @@ -@file:SuppressLint("LogNotTimber", "ObsoleteSdkInt") -@file:Suppress("KotlinConstantConditions") - -// IMPORTANT: this file comes from rustls-platform-verifier and should not be modified locally. - -/* - * Copyright (c) 2022 1Password - * - * SPDX-License-Identifier: MIT - */ - -package org.rustls.platformverifier - -import android.annotation.SuppressLint -import android.content.Context -import android.net.http.X509TrustManagerExtensions -import android.util.Log -import java.io.ByteArrayInputStream -import java.io.File -import java.security.KeyStore -import java.security.KeyStoreException -import java.security.MessageDigest -import java.security.PublicKey -import java.security.cert.CertificateException -import java.security.cert.CertificateExpiredException -import java.security.cert.CertificateFactory -import java.security.cert.CertificateNotYetValidException -import java.security.cert.CertificateParsingException -import java.security.cert.X509Certificate -import java.util.Date -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager -import javax.security.auth.x500.X500Principal - -// If this is updated, update the Rust definition too. -// Marked private as this is not meant to be used in Android code. -private enum class StatusCode(val value: Int) { - Ok(0), - Unavailable(1), - Expired(2), - UnknownCert(3), - Revoked(4), - InvalidEncoding(5), - InvalidExtension(6), -} - -// Marked private as this is not meant to be used in Android code. -private class VerificationResult( - status: StatusCode, - @Suppress("unused") val message: String? = null -) { - @Suppress("unused") - private val code: Int = status.value -} - -// NOTE: All TrustManager and certificate validation methods are not thread safe. These -// are all guarded by Kotlin's `Synchronized` accessors to prevent undefined behavior. - -// Only JNI and test code calls this, so unused code warnings are suppressed. -// Internal for test code - no other Kotlin code should use this object directly. -@Suppress("unused") -// We want to show a difference between Kotlin-side logs and those in Rust code -@SuppressLint("LongLogTag") -internal object CertificateVerifier { - private const val TAG = "rustls-platform-verifier-android" - - private fun createTrustManager(keystore: KeyStore?): X509TrustManagerExtensions? { - // This can never throw since the default algorithm is used. - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - - factory.init(keystore) - - val availableTrustManagers = try { - factory.trustManagers - } catch (e: RuntimeException) { - Log.w(TAG, "exception thrown creating a TrustManager: $e") - return null - } - - for (manager in availableTrustManagers) { - if (manager is X509TrustManager) { - // Kotlin ensures this can't throw at runtime since it knows that - // it must be the correct type by now. - return X509TrustManagerExtensions(manager) - } - } - - Log.e(TAG, "failed to find a usable trust manager") - return null - } - - private fun makeLazyTrustManager(keystore: KeyStore?): Lazy { - // Ensure the keystore is loaded. Since all of the trust managers are initialized in a - // `Lazy`, this will only run once. - keystore?.load(null) - - return lazy { createTrustManager(keystore) } - } - - // -- Test only -- - // Ideally, all of this will be optimized out at compile time due to not being accessed - // in release builds. - - @get:Synchronized - private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType()) - - @get:Synchronized - private var mockTrustManager: Lazy = - makeLazyTrustManager(mockKeystore) - - @JvmStatic - private fun addMockRoot(root: ByteArray) { - if (!BuildConfig.TEST) { - throw Exception("attempted to add a mock root outside a test!") - } - - val alias = "root_${mockKeystore.size()}" - // Throwing here is fine since test roots should always be well-formed - val cert = certFactory.generateCertificate(ByteArrayInputStream(root)) - mockKeystore.setCertificateEntry(alias, cert) - - reloadMockData() - } - - @JvmStatic - private fun clearMockRoots() { - // Reload to get a completely fresh internal state - mockKeystore.load(null) - reloadMockData() - } - - @JvmStatic - private fun reloadMockData() { - if (mockTrustManager.isInitialized()) { - mockTrustManager = makeLazyTrustManager(mockKeystore) - } - } - - // Get a list of the system's root CAs. - // Function is public for testing only. - @JvmStatic - fun getSystemRootCAs(): List { - val rootCAs = mutableListOf() - - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - factory.init(systemKeystore) - - val availableTrustManagers = try { - factory.trustManagers - } catch (e: RuntimeException) { - Log.w(TAG, "exception thrown creating a TrustManager: $e") - return rootCAs - } - - availableTrustManagers.forEach { trustManager -> - if (trustManager is X509TrustManager) { - rootCAs.addAll(trustManager.acceptedIssuers) - } - } - - return rootCAs - } - - // -- End testing requirements -- - - private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509") - - private var systemTrustAnchorCache = hashSetOf>() - - @get:Synchronized - private var systemCertificateDirectory: File? = System.getenv("ANDROID_ROOT")?.let { rootPath -> - File("$rootPath/etc/security/cacerts") - } - - @get:Synchronized - private val systemKeystore: KeyStore? = try { - KeyStore.getInstance("AndroidCAStore") - } catch (_: KeyStoreException) { - null - } - - @get:Synchronized - private val systemTrustManager: Lazy = - makeLazyTrustManager(systemKeystore) - - @JvmStatic - private fun verifyCertificateChain( - @Suppress("UNUSED_PARAMETER") context: Context, - serverName: String, - authMethod: String, - allowedEkus: Array, - ocspResponse: ByteArray?, - time: Long, - certChain: Array - ): VerificationResult { - // Convert the array of (supposedly) DER bytes into certificates. - val certificateChain = mutableListOf() - certChain.forEach { certBytes -> - val certificate = try { - certFactory.generateCertificate(ByteArrayInputStream(certBytes)) - } catch (e: CertificateException) { - return VerificationResult(StatusCode.InvalidEncoding) - } - certificateChain.add(certificate as X509Certificate) - } - - // Will never throw `ArrayIndexOutOfBoundsException` because `rustls`'s `ServerCertVerifier` trait - // has a mandatory `end_entity` parameter in `verify_server_cert`. - val endEntity = certificateChain[0] - - // Check that the certificate is valid at the point of time provided by `rustls`. - try { - endEntity.checkValidity(Date(time)) - } catch (e: CertificateExpiredException) { - return VerificationResult(StatusCode.Expired) - } catch (e: CertificateNotYetValidException) { - return VerificationResult(StatusCode.Expired) - } - - // Check that this certificate can be used in a TLS server. - if (!verifyCertUsage(endEntity, allowedEkus)) { - return VerificationResult(StatusCode.InvalidExtension) - } - - // Select the trust manager to use. - // - // We select them as follows: - // - If built for release, only use the system trust manager. This should let all test-related - // code be optimized out. - // - If built for tests: - // - If the mock CA store has any values, use the mock trust manager. - // - Otherwise, use the system trust manager. - val (trustManager, keystore) = if (!BuildConfig.TEST) { - val trustManager = - systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable) - Pair(trustManager, systemKeystore) - } else { - if (mockKeystore.size() != 0) { - val trustManager = mockTrustManager.value!! - Pair(trustManager, mockKeystore) - } else { - val trustManager = - systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable) - Pair(trustManager, systemKeystore) - } - } - - // Verify that the certificate chain is valid and correct, and nothing more. - // - // NOTE: This does not validate `serverName` is valid for the end-entity certificate. - // That is handled in Rust as Android/Java do not currently provide a RFC 6125 compliant - // hostname verifier. Additionally, even the RFC 2818 verifier is not available until API 24. - // - // `serverName` is only used for pinning/CT requirements. - // - // Returns the "the properly ordered chain used for verification as a list of X509Certificates.", - // meaning a list from end-entity certificate to trust-anchor. - val validChain = try { - trustManager.checkServerTrusted(certificateChain.toTypedArray(), authMethod, serverName) - } catch (e: CertificateException) { - // In test configurations we may see `checkServerTrusted` fail once vendored test - // certificates pass their expiry date. We try to avoid that by using a fixed - // verification time when calling `endEntity.checkValidity` above, however we can't - // fix the time for the `checkServerTrusted` call. - // - // To make diagnosing CI test failures easier we try to find the root cause of - // checkServerTrusted failing, returning a different `StatusCode` as appropriate. - if (BuildConfig.TEST) { - var rootCause: Throwable? = e - while (rootCause?.cause != null && rootCause.cause != rootCause) { - rootCause = rootCause.cause - } - return when (rootCause) { - is CertificateExpiredException, is CertificateNotYetValidException -> VerificationResult( - StatusCode.Expired, - rootCause.toString() - ) - - else -> VerificationResult(StatusCode.UnknownCert, rootCause.toString()) - } - } - // In non-test configurations we should have caught expiry errors earlier and - // can simply return an unknown cert error without digging through the exception - // cause chain. - return VerificationResult(StatusCode.UnknownCert, e.toString()) - } - - // TEST ONLY: Mock test suite cannot attempt to check revocation status if no OSCP data has been stapled, - // because Android requires certificates to an specify OCSP responder for network fetch in this case. - // If in testing w/o OCSP stapled, short-circuit here - only prior checks apply. - if (BuildConfig.TEST && (mockKeystore.size() != 0) && (ocspResponse == null)) { - return VerificationResult(StatusCode.Ok) - } - - // ================================================================================================= - // IMPORTANT! DIFF WITH THE ORIGINAL SOURCES HERE: - // - // We removed the cert revocation checks because Android won't do them by default - // and supporting them would be quite difficult given CRLs won't work out of the box. - // - // See https://github.com/rustls/rustls-platform-verifier/issues/221 for more info. - // ================================================================================================= - - return VerificationResult(StatusCode.Ok) - } - - private fun verifyCertUsage(certificate: X509Certificate, allowedEkus: Array): Boolean { - val ekus = try { - certificate.extendedKeyUsage - } - // This should be unreachable, but could happen. - catch (_: CertificateParsingException) { - return false - } catch (_: NullPointerException) { - // According to Chromium's implementation, this can crash when the EKU data is malformed. - Log.w(TAG, "exception handling certificate EKU") - return false - } ?: return true // If the list is empty, we have nothing to do. - - return ekus.any { allowedEkus.contains(it) } - } - - // Android hashes a principal using the first four bytes of its MD5 digest, encoded in - // lowercase hex and reversed. - // - // Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=339 - private fun hashPrincipal(principal: X500Principal): String { - val hexDigits = "0123456789abcdef".toCharArray() - val digest = MessageDigest.getInstance("MD5").digest(principal.encoded) - val hexChars = CharArray(8) - - for (i in 0..3) { - // Kotlin doesn't support bitwise operators for bytes, only Int and Long. - val digestByte = digest[3 - i].toInt() - hexChars[2 * i] = hexDigits[(digestByte shr 4) and 0xf] - hexChars[2 * i + 1] = hexDigits[digestByte and 0xf] - } - - return String(hexChars) - } - - // Check if CA root is known or not. - // Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise/user. - // - // Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351 - fun isKnownRoot(root: X509Certificate): Boolean { - // System keystore and cert directory must be non-null to perform checking - systemKeystore?.let { loadedSystemKeystore -> - systemCertificateDirectory?.let { loadedSystemCertificateDirectory -> - - // Check the in-memory cache first - val key = Pair(root.subjectX500Principal, root.publicKey) - if (systemTrustAnchorCache.contains(key)) { - return true - } - - // System trust anchors are stored under a hash of the principal. - // In case of collisions, append number. - val hash = hashPrincipal(root.subjectX500Principal) - var i = 0 - while (true) { - val alias = "$hash.$i" - - if (!File(loadedSystemCertificateDirectory, alias).exists()) { - break - } - - val anchor = loadedSystemKeystore.getCertificate("system:$alias") - - // It's possible for `anchor` to be `null` if the user deleted a trust anchor. - // Continue iterating as there may be further collisions after the deleted anchor. - if (anchor == null) { - continue - // This should never happen - } else if (anchor !is X509Certificate) { - // SAFETY: This logs a unique identifier (hash value) only in cases where a file within the - // system's root trust store is not a valid X509 certificate (extremely unlikely error). - // The hash doesn't tell us any sensitive information about the invalid cert or reveal any of - // its contents - it just lets us ID the bad file if a user is having TLS failure issues. - Log.e(TAG, "anchor is not a certificate, alias: $alias") - continue - // If subject and public key match, it's a system root. - } else { - if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) { - systemTrustAnchorCache.add(key) - return true - } - } - - i += 1 - } - } - } - - // Not found in cache or store: non-public - return false - } -} diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 51b06af7a7..f23e600677 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -43,7 +43,6 @@ val excludedKoverSubProjects = listOf( ":libraries:core", ":libraries:coroutines", ":libraries:di", - ":libraries:rustls-tls", ":tests:detekt-rules", ":tests:konsist", ":tests:testutils", diff --git a/settings.gradle.kts b/settings.gradle.kts index 8bdd4632c0..e46d80f4f2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,16 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + exclusiveContent { + forRepository { + maven { + url = uri("https://github.com/rustls/rustls-platform-verifier/raw/maven-archive/android-release-support/maven/") + } + } + filter { + includeModule("org.rustls", "rustls-platform-verifier") + } + } maven { url = uri("https://www.jitpack.io") content { diff --git a/tools/dependencies/update_rustls_platform_verifier.py b/tools/dependencies/update_rustls_platform_verifier.py new file mode 100755 index 0000000000..9d17f63164 --- /dev/null +++ b/tools/dependencies/update_rustls_platform_verifier.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2026 Element Creations Ltd. +# +# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +# Please see LICENSE files in the repository root for full details. + +""" +Update the rustls-platform-verifier dependency to the given version and regenerate +app/src/main/res/xml/network_security_config.xml. + +The rustls-platform-verifier AAR ships a network_security_config.xml allowing cleartext traffic +to the domains used by certificate authorities to publish their CRLs. Since the app has its own +network security config, those domains are merged into a copy of network_security_config_element.xml. + +Usage: ./tools/dependencies/update_rustls_platform_verifier.py +""" + +import io +import os +import re +import sys +import urllib.request +import xml.etree.ElementTree as ET +import zipfile + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +LIBS_VERSIONS_TOML = os.path.join(ROOT_DIR, "gradle", "libs.versions.toml") +XML_DIR = os.path.join(ROOT_DIR, "app", "src", "main", "res", "xml") +SOURCE_CONFIG = os.path.join(XML_DIR, "network_security_config_element.xml") +TARGET_CONFIG = os.path.join(XML_DIR, "network_security_config.xml") + +AAR_URL = ( + "https://raw.githubusercontent.com/rustls/rustls-platform-verifier/maven-archive/android-release-support/" + "maven/org/rustls/rustls-platform-verifier/{version}/rustls-platform-verifier-{version}.aar" +) +AAR_CONFIG_PATH = "res/xml/network_security_config.xml" + +CLEARTEXT_DOMAIN_CONFIG_REGEX = re.compile( + r'(.*?)(\n?[ \t]*)', + re.DOTALL, +) + + +def update_libs_versions(version): + with open(LIBS_VERSIONS_TOML, "r") as f: + content = f.read() + new_content, count = re.subn( + r'^(rustls_platform_verifier\s*=\s*"org\.rustls:rustls-platform-verifier:)[^"]+(")', + rf"\g<1>{version}\g<2>", + content, + flags=re.MULTILINE, + ) + if count != 1: + sys.exit(f"Error: could not find the rustls_platform_verifier dependency in {LIBS_VERSIONS_TOML}") + with open(LIBS_VERSIONS_TOML, "w") as f: + f.write(new_content) + print(f"Updated rustls_platform_verifier to {version} in {os.path.relpath(LIBS_VERSIONS_TOML, ROOT_DIR)}") + + +def download_aar(version): + url = AAR_URL.format(version=version) + print(f"Downloading {url}") + try: + with urllib.request.urlopen(url) as response: + return response.read() + except urllib.error.HTTPError as e: + sys.exit(f"Error: could not download the AAR for version {version} ({e.code} {e.reason})") + + +def extract_cleartext_domains(aar_bytes): + with zipfile.ZipFile(io.BytesIO(aar_bytes)) as aar: + try: + config = aar.read(AAR_CONFIG_PATH) + except KeyError: + sys.exit(f"Error: {AAR_CONFIG_PATH} not found in the AAR") + root = ET.fromstring(config) + domains = [] + for domain_config in root.iter("domain-config"): + if domain_config.get("cleartextTrafficPermitted") != "true": + continue + for domain in domain_config.findall("domain"): + domains.append((domain.get("includeSubdomains"), domain.text.strip())) + if not domains: + sys.exit("Error: no cleartext domains found in the AAR network_security_config.xml") + return domains + + +def write_network_security_config(domains, version): + with open(SOURCE_CONFIG, "r") as f: + content = f.read() + + lines = [ + "", + "", + f" ", + " ", + ] + for include_subdomains, name in domains: + attributes = f' includeSubdomains="{include_subdomains}"' if include_subdomains is not None else "" + lines.append(f" {name}") + extra = "\n".join(lines) + + new_content, count = CLEARTEXT_DOMAIN_CONFIG_REGEX.subn(lambda m: m.group(1) + extra + m.group(2), content, count=1) + if count != 1: + sys.exit(f'Error: could not find in {SOURCE_CONFIG}') + + with open(TARGET_CONFIG, "w") as f: + f.write(new_content) + print(f"Wrote {os.path.relpath(TARGET_CONFIG, ROOT_DIR)} with {len(domains)} extra domains") + + +def main(): + if len(sys.argv) != 2: + sys.exit(f"Usage: {sys.argv[0]} ") + version = sys.argv[1].strip() + if not re.fullmatch(r"[0-9A-Za-z.\-]+", version): + sys.exit(f"Error: invalid version '{version}'") + + # Download first, so nothing is modified if the version does not exist + aar_bytes = download_aar(version) + domains = extract_cleartext_domains(aar_bytes) + update_libs_versions(version) + write_network_security_config(domains, version) + + +if __name__ == "__main__": + main() diff --git a/tools/lint/lint.xml b/tools/lint/lint.xml index c4f045272c..038ed9a292 100644 --- a/tools/lint/lint.xml +++ b/tools/lint/lint.xml @@ -42,6 +42,8 @@ + +