diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5655e474ce5..c201ce6b6db 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -188,6 +188,9 @@ dependencies {
// Development dependencies
debugImplementation(libs.leakCanary)
+ // oauth dependencies
+ implementation(libs.openIdAppOauth)
+
// Internal, dev, beta and staging only tracking & logging
devImplementation(libs.dataDog.core)
internalImplementation(libs.dataDog.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 455a27e1535..ae6166b0b40 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -144,6 +144,12 @@
+
+
+
+
+
+
@@ -199,6 +205,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
) -> Unit
+
+ operator fun invoke(context: Context, enrollmentResultHandler: (Either) -> Unit) {
+ this.enrollmentResultHandler = enrollmentResultHandler
+ scope.launch {
+ enrollE2EI.initialEnrollment().fold({
+ enrollmentResultHandler(Either.Left(it))
+ }, {
+ if (it is E2EIEnrollmentResult.Initialized) {
+ initialEnrollmentResult = it
+ OAuthUseCase(context, it.target).launch(
+ context.getActivity()!!.activityResultRegistry,
+ ::oAuthResultHandler
+ )
+ } else enrollmentResultHandler(Either.Right(it))
+ })
+ }
+ }
+
+ private fun oAuthResultHandler(oAuthResult: OAuthUseCase.OAuthResult) {
+ scope.launch {
+ when (oAuthResult) {
+ is OAuthUseCase.OAuthResult.Success -> {
+ enrollE2EI.finalizeEnrollment(
+ oAuthResult.idToken,
+ initialEnrollmentResult
+ ).map { enrollmentResultHandler(Either.Right(it)) }
+ }
+
+ is OAuthUseCase.OAuthResult.Failed -> {
+ enrollmentResultHandler(Either.Left(E2EIFailure.FailedOAuth(oAuthResult.reason)))
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt
new file mode 100644
index 00000000000..7bd95f4f25b
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt
@@ -0,0 +1,207 @@
+/*
+ * Wire
+ * Copyright (C) 2023 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.feature.e2ei
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.Base64
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultRegistry
+import androidx.activity.result.contract.ActivityResultContracts
+import com.wire.android.appLogger
+import com.wire.android.util.deeplink.DeepLinkProcessor
+import net.openid.appauth.AppAuthConfiguration
+import net.openid.appauth.AuthState
+import net.openid.appauth.AuthorizationException
+import net.openid.appauth.AuthorizationRequest
+import net.openid.appauth.AuthorizationResponse
+import net.openid.appauth.AuthorizationService
+import net.openid.appauth.AuthorizationServiceConfiguration
+import net.openid.appauth.ClientAuthentication
+import net.openid.appauth.ClientSecretBasic
+import net.openid.appauth.ResponseTypeValues
+import net.openid.appauth.browser.BrowserAllowList
+import net.openid.appauth.browser.VersionedBrowserMatcher
+import net.openid.appauth.connectivity.ConnectionBuilder
+import java.net.HttpURLConnection
+import java.net.URL
+import java.security.MessageDigest
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLContext
+import javax.net.ssl.TrustManager
+import javax.net.ssl.X509TrustManager
+
+class OAuthUseCase(context: Context, private val authUrl: String) {
+ private var authState: AuthState = AuthState()
+ private var authorizationService: AuthorizationService
+ private lateinit var authServiceConfig: AuthorizationServiceConfiguration
+
+ // todo: this is a temporary code to ignore ssl issues on the test environment, will be removed after the preparation of the environment
+ // region Ignore SSL for OAuth
+ val naiveTrustManager = object : X509TrustManager {
+ override fun getAcceptedIssuers(): Array = arrayOf()
+ override fun checkClientTrusted(certs: Array, authType: String) = Unit
+ override fun checkServerTrusted(certs: Array, authType: String) = Unit
+ }
+ val insecureSocketFactory = SSLContext.getInstance("SSL").apply {
+ val trustAllCerts = arrayOf(naiveTrustManager)
+ init(null, trustAllCerts, SecureRandom())
+ }.socketFactory
+
+ private var insecureConnection = ConnectionBuilder() { uri ->
+ val url = URL(uri.toString())
+ val connection = url.openConnection() as HttpURLConnection
+ if (connection is HttpsURLConnection) {
+ connection.hostnameVerifier = HostnameVerifier { _, _ -> true }
+ connection.sslSocketFactory = insecureSocketFactory
+ }
+ connection
+ }
+ // endregion
+
+ private var appAuthConfiguration: AppAuthConfiguration = AppAuthConfiguration.Builder()
+ .setBrowserMatcher(
+ BrowserAllowList(
+ VersionedBrowserMatcher.CHROME_CUSTOM_TAB, VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB
+ )
+ )
+ .setConnectionBuilder(insecureConnection)
+ .setSkipIssuerHttpsCheck(true)
+ .build()
+
+ init {
+ authorizationService = AuthorizationService(context, appAuthConfiguration)
+ }
+
+ private fun getAuthorizationRequestIntent(): Intent = authorizationService.getAuthorizationRequestIntent(getAuthorizationRequest())
+
+ fun launch(activityResultRegistry: ActivityResultRegistry, resultHandler: (OAuthResult) -> Unit) {
+ val resultLauncher = activityResultRegistry.register(
+ OAUTH_ACTIVITY_RESULT_KEY, ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ handleActivityResult(result, resultHandler)
+ }
+ AuthorizationServiceConfiguration.fetchFromUrl(
+ Uri.parse(authUrl.plus(IDP_CONFIGURATION_PATH)),
+ { configuration, ex ->
+ if (ex == null) {
+ authServiceConfig = configuration!!
+ resultLauncher.launch(getAuthorizationRequestIntent())
+ } else {
+ resultHandler(OAuthResult.Failed.InvalidActivityResult("Fetching the configurations failed! $ex"))
+ }
+ }, insecureConnection
+ )
+ }
+
+ private fun handleActivityResult(result: ActivityResult, resultHandler: (OAuthResult) -> Unit) {
+ if (result.resultCode == Activity.RESULT_OK) {
+ handleAuthorizationResponse(result.data!!, resultHandler)
+ } else {
+ resultHandler(OAuthResult.Failed.InvalidActivityResult(result.toString()))
+ }
+ }
+
+ private fun handleAuthorizationResponse(intent: Intent, resultHandler: (OAuthResult) -> Unit) {
+ val authorizationResponse: AuthorizationResponse? = AuthorizationResponse.fromIntent(intent)
+ val clientAuth: ClientAuthentication = ClientSecretBasic(CLIENT_SECRET)
+
+ val error = AuthorizationException.fromIntent(intent)
+
+ authState = AuthState(authorizationResponse, error)
+
+ val tokenExchangeRequest = authorizationResponse?.createTokenExchangeRequest()
+
+ tokenExchangeRequest?.let { request ->
+ authorizationService.performTokenRequest(request, clientAuth) { response, exception ->
+ if (exception != null) {
+ authState = AuthState()
+ resultHandler(OAuthResult.Failed(exception.toString()))
+ } else {
+ if (response != null) {
+ authState.update(response, exception)
+ appLogger.i("OAuth idToken: ${response.idToken}")
+ resultHandler(OAuthResult.Success(response.idToken.toString()))
+ } else {
+ resultHandler(OAuthResult.Failed.EmptyResponse)
+ }
+ }
+ }
+ } ?: resultHandler(OAuthResult.Failed.Unknown)
+ }
+
+ private fun getAuthorizationRequest() = AuthorizationRequest.Builder(
+ authServiceConfig, CLIENT_ID, ResponseTypeValues.CODE, URL_AUTH_REDIRECT
+ ).setCodeVerifier().setScopes(SCOPE_OPENID, SCOPE_EMAIL, SCOPE_PROFILE).build()
+
+ private fun AuthorizationRequest.Builder.setCodeVerifier(): AuthorizationRequest.Builder {
+ val codeVerifier = getCodeVerifier()
+ setCodeVerifier(
+ codeVerifier, getCodeChallenge(codeVerifier), CODE_VERIFIER_CHALLENGE_METHOD
+ )
+ return this
+ }
+
+ @Suppress("MagicNumber")
+ private fun getCodeVerifier(): String {
+ val secureRandom = SecureRandom()
+ val bytes = ByteArray(64)
+ secureRandom.nextBytes(bytes)
+ return Base64.encodeToString(bytes, ENCODING)
+ }
+
+ private fun getCodeChallenge(codeVerifier: String): String {
+ val hash = MESSAGE_DIGEST.digest(codeVerifier.toByteArray())
+ return Base64.encodeToString(hash, ENCODING)
+ }
+
+ sealed class OAuthResult {
+ data class Success(val idToken: String) : OAuthResult()
+ open class Failed(val reason: String) : OAuthResult() {
+ object Unknown : Failed("Unknown")
+ class InvalidActivityResult(reason: String) : Failed(reason)
+ object EmptyResponse : Failed("Empty Response")
+ }
+ }
+
+ companion object {
+ const val OAUTH_ACTIVITY_RESULT_KEY = "OAuthActivityResult"
+
+ const val SCOPE_PROFILE = "profile"
+ const val SCOPE_EMAIL = "email"
+ const val SCOPE_OPENID = "openid"
+
+ // todo: clientId and the clientSecret will be replaced with the values from the BE once the BE provides them
+ const val CLIENT_ID = "wireapp"
+ const val CLIENT_SECRET = "dUpVSGx2dVdFdGQ0dmsxWGhDalQ0SldU"
+ const val CODE_VERIFIER_CHALLENGE_METHOD = "S256"
+ const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
+ val MESSAGE_DIGEST = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
+ const val ENCODING = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
+ val URL_AUTH_REDIRECT: Uri = Uri.Builder().scheme(DeepLinkProcessor.DEEP_LINK_SCHEME)
+ .authority(DeepLinkProcessor.E2EI_DEEPLINK_HOST)
+ .appendPath(DeepLinkProcessor.E2EI_DEEPLINK_OAUTH_REDIRECT_PATH).build()
+
+ const val IDP_CONFIGURATION_PATH = "/.well-known/openid-configuration"
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
index 65f57dbeb98..9095e883ea6 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
@@ -18,6 +18,8 @@
*
*/
+@file:Suppress("MultiLineIfElse")
+
package com.wire.android.ui.common
import androidx.compose.foundation.layout.Box
diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
index 343cad91d17..61b1b98b7cd 100644
--- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt
@@ -29,6 +29,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
@@ -37,9 +38,13 @@ import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.CurrentAccount
+import com.wire.android.feature.e2ei.GetE2EICertificateUseCase
import com.wire.android.migration.failure.UserMigrationStatus
import com.wire.android.model.Clickable
import com.wire.android.ui.common.RowItemTemplate
+import com.wire.android.ui.common.WireDialog
+import com.wire.android.ui.common.WireDialogButtonProperties
+import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.common.WireSwitch
import com.wire.android.ui.common.button.WirePrimaryButton
import com.wire.android.ui.common.dimensions
@@ -51,10 +56,13 @@ import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.getDeviceIdString
import com.wire.android.util.getGitBuildId
import com.wire.android.util.ui.PreviewMultipleThemes
+import com.wire.kalium.logic.E2EIFailure
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.debug.DisableEventProcessingUseCase
+import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult
import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountResult
import com.wire.kalium.logic.feature.keypackage.MLSKeyPackageCountUseCase
+import com.wire.kalium.logic.functional.fold
import com.wire.kalium.logic.sync.periodic.UpdateApiVersionsScheduler
import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -72,7 +80,9 @@ data class DebugDataOptionsState(
val mlsErrorMessage: String = "null",
val isManualMigrationAllowed: Boolean = false,
val debugId: String = "null",
- val commitish: String = "null"
+ val commitish: String = "null",
+ val certificate: String = "null",
+ val showCertificate: Boolean = false
)
@Suppress("LongParameterList")
@@ -85,7 +95,8 @@ class DebugDataOptionsViewModel
private val updateApiVersions: UpdateApiVersionsScheduler,
private val mlsKeyPackageCountUseCase: MLSKeyPackageCountUseCase,
private val restartSlowSyncProcessForRecovery: RestartSlowSyncProcessForRecoveryUseCase,
- private val disableEventProcessingUseCase: DisableEventProcessingUseCase
+ private val disableEventProcessingUseCase: DisableEventProcessingUseCase,
+ private val e2eiCertificateUseCase: GetE2EICertificateUseCase
) : ViewModel() {
var state by mutableStateOf(
@@ -116,6 +127,28 @@ class DebugDataOptionsViewModel
}
}
+ fun enrollE2EICertificate(context: Context) {
+ e2eiCertificateUseCase(context) { result ->
+ result.fold({
+ state = state.copy(
+ certificate = (it as E2EIFailure.FailedOAuth).reason, showCertificate = true
+ )
+ }, {
+ if (it is E2EIEnrollmentResult.Finalized) {
+ state = state.copy(
+ certificate = it.certificate, showCertificate = true
+ )
+ }
+ })
+ }
+ }
+
+ fun dismissCertificateDialog() {
+ state = state.copy(
+ showCertificate = false,
+ )
+ }
+
fun forceUpdateApiVersions() {
updateApiVersions.scheduleImmediateApiVersionUpdate()
}
@@ -199,7 +232,9 @@ fun DebugDataOptions(
onRestartSlowSyncForRecovery = viewModel::restartSlowSyncForRecovery,
onForceUpdateApiVersions = viewModel::forceUpdateApiVersions,
onManualMigrationPressed = { onManualMigrationPressed(viewModel.currentAccount) },
- onDisableEventProcessingChange = viewModel::disableEventProcessing
+ onDisableEventProcessingChange = viewModel::disableEventProcessing,
+ enrollE2EICertificate = viewModel::enrollE2EICertificate,
+ dismissCertificateDialog = viewModel::dismissCertificateDialog
)
}
@@ -214,7 +249,9 @@ fun DebugDataOptionsContent(
onDisableEventProcessingChange: (Boolean) -> Unit,
onRestartSlowSyncForRecovery: () -> Unit,
onForceUpdateApiVersions: () -> Unit,
- onManualMigrationPressed: () -> Unit
+ onManualMigrationPressed: () -> Unit,
+ enrollE2EICertificate: (Context) -> Unit,
+ dismissCertificateDialog: () -> Unit
) {
Column {
@@ -249,7 +286,6 @@ fun DebugDataOptionsContent(
onClick = { onCopyText(state.commitish) }
)
)
-
if (BuildConfig.PRIVATE_BUILD) {
SettingsItem(
@@ -261,19 +297,41 @@ fun DebugDataOptionsContent(
onClick = { onCopyText(state.debugId) }
)
)
+ if (BuildConfig.DEBUG) {
+ GetE2EICertificateSwitch(
+ enrollE2EI = enrollE2EICertificate
+ )
+ if (state.showCertificate) {
+ WireDialog(
+ title = stringResource(R.string.end_to_end_identity_ceritifcate),
+ text = state.certificate,
+ onDismiss = {
+ dismissCertificateDialog()
+ },
+ optionButton1Properties = WireDialogButtonProperties(
+ onClick = {
+ dismissCertificateDialog()
+ },
+ text = stringResource(R.string.label_ok),
+ type = WireDialogButtonType.Primary,
+ )
+ )
+ }
+ }
ProteusOptions(
isEncryptedStorageEnabled = state.isEncryptedProteusStorageEnabled,
onEncryptedStorageEnabledChange = onEnableEncryptedProteusStorageChange
)
-
- MLSOptions(
- keyPackagesCount = state.keyPackagesCount,
- mlsClientId = state.mslClientId,
- mlsErrorMessage = state.mlsErrorMessage,
- restartSlowSyncForRecovery = onRestartSlowSyncForRecovery,
- onCopyText = onCopyText
- )
+ if (BuildConfig.DEBUG) {
+ MLSOptions(
+ keyPackagesCount = state.keyPackagesCount,
+ mlsClientId = state.mslClientId,
+ mlsErrorMessage = state.mlsErrorMessage,
+ restartSlowSyncForRecovery = onRestartSlowSyncForRecovery,
+ onCopyText = onCopyText
+ )
+ }
DebugToolsOptions(
isEventProcessingEnabled = state.isEventProcessingDisabled,
@@ -292,6 +350,35 @@ fun DebugDataOptionsContent(
}
}
+@Composable
+private fun GetE2EICertificateSwitch(
+ enrollE2EI: (context: Context) -> Unit
+) {
+ val context = LocalContext.current
+ Column {
+ FolderHeader(stringResource(R.string.debug_settings_e2ei_enrollment_title))
+ RowItemTemplate(modifier = Modifier.wrapContentWidth(),
+ title = {
+ Text(
+ style = MaterialTheme.wireTypography.body01,
+ color = MaterialTheme.wireColorScheme.onBackground,
+ text = stringResource(R.string.label_get_e2ei_cetificate),
+ modifier = Modifier.padding(start = dimensions().spacing8x)
+ )
+ },
+ actions = {
+ WirePrimaryButton(
+ onClick = {
+ enrollE2EI(context)
+ },
+ text = stringResource(R.string.label_get_e2ei_cetificate),
+ fillMaxWidth = false
+ )
+ }
+ )
+ }
+}
+
//region Scala Migration Options
@Composable
private fun ManualMigrationOptions(
@@ -513,6 +600,8 @@ fun PreviewOtherDebugOptions() {
onForceUpdateApiVersions = {},
onDisableEventProcessingChange = {},
onRestartSlowSyncForRecovery = {},
- onManualMigrationPressed = {}
+ onManualMigrationPressed = {},
+ enrollE2EICertificate = {},
+ dismissCertificateDialog = {},
)
}
diff --git a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt
index 4fa6d2a96f3..106909c5979 100644
--- a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt
+++ b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt
@@ -153,6 +153,8 @@ class DeepLinkProcessor @Inject constructor(
companion object {
const val DEEP_LINK_SCHEME = "wire"
+ const val E2EI_DEEPLINK_HOST = "e2ei"
+ const val E2EI_DEEPLINK_OAUTH_REDIRECT_PATH = "oauth2redirect"
const val ACCESS_DEEPLINK_HOST = "access"
const val SERVER_CONFIG_PARAM = "config"
const val SSO_LOGIN_DEEPLINK_HOST = "sso-login"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9bfe2131f3e..7c410d0c78c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -197,6 +197,8 @@
Give Feedback
Report Bug
Debug Settings
+ API VERSIONING
+ E2EI Manual Enrollment
Force API versioning update
Update
Support
@@ -986,6 +988,7 @@
Delete All Logs
Restart slow sync
Restart
+ Get E2EI Certificate
MLS Options
Enable Logging
Proteus Options
@@ -1234,6 +1237,7 @@
Certificate updated
The certificate is updated and your device is verified.
Certificate Details
+ Certificate Details
Start Recording
Recording Audio…
diff --git a/buildSrc/src/main/kotlin/scripts/variants.gradle.kts b/buildSrc/src/main/kotlin/scripts/variants.gradle.kts
index cc281e09be0..509442734b6 100644
--- a/buildSrc/src/main/kotlin/scripts/variants.gradle.kts
+++ b/buildSrc/src/main/kotlin/scripts/variants.gradle.kts
@@ -59,6 +59,7 @@ fun NamedDomainObjectContainer.createAppFlavour(
versionNameSuffix = "-${flavour.buildName}"
resValue("string", "app_name", flavour.appName)
manifestPlaceholders["sharedUserId"] = sharedUserId
+ manifestPlaceholders["appAuthRedirectScheme"] = flavorApplicationId
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bfbb234fb9e..84d15b81518 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -61,6 +61,9 @@ rss-parser = "6.0.1"
# Logging
dataDog = "1.19.3"
+#OAuth
+openIdAppAuth = "0.11.1"
+
# Other Tools
aboutLibraries = "10.8.0"
leakCanary = "2.7"
@@ -198,6 +201,9 @@ rss-parser = { module = "com.prof18.rssparser:rssparser", version.ref = "rss-par
dataDog-core = { module = "com.datadoghq:dd-sdk-android", version.ref = "dataDog" }
dataDog-compose = { module = "com.datadoghq:dd-sdk-android-compose", version.ref = "dataDog" }
+# OAuth
+openIdAppOauth = { module = "net.openid:appauth", version.ref = "openIdAppAuth" }
+
# Material
material = { module = "com.google.android.material:material", version.ref = "material" }
diff --git a/kalium b/kalium
index b0c7e3707a1..b80ce0f89bc 160000
--- a/kalium
+++ b/kalium
@@ -1 +1 @@
-Subproject commit b0c7e3707a1b696797e7529ccbb0fee032bbdbe6
+Subproject commit b80ce0f89bcef621bda6f2b234b2750b4d15d913