Sign in with QR code (#2793)

* Add QR code login.
* Add FF to disable it in release mode.
* Force portrait orientation on the login flow.
* Create `NumberedList` UI components.
* Improve camera permission dialog.
* Make nodes in qrcode feature use `QrCodeLoginScope` instead of `AppScope`
* Bump SDK version.
* Fix maestro tests

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-05-31 14:38:27 +02:00 committed by GitHub
parent e29f919abc
commit c8bd04ceb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
253 changed files with 4421 additions and 326 deletions

View file

@ -46,25 +46,11 @@ class RustMatrixClientFactory @Inject constructor(
private val utdTracker: UtdTracker,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = ClientBuilder()
.basePath(baseDirectory.absolutePath)
val client = getBaseClientBuilder()
.homeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.passphrase(sessionData.passphrase)
.userAgent(userAgentProvider.provide())
.let {
// Sadly ClientBuilder.proxy() does not accept null :/
// Tracked by https://github.com/matrix-org/matrix-rust-sdk/issues/3159
val proxy = proxyProvider.provides()
if (proxy != null) {
it.proxy(proxy)
} else {
it
}
}
.addRootCertificates(userCertificatesProvider.provides())
// FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
.use { it.build() }
client.restoreSession(sessionData.toSession())
@ -84,6 +70,24 @@ class RustMatrixClientFactory @Inject constructor(
clock = clock,
)
}
internal fun getBaseClientBuilder(): ClientBuilder {
return ClientBuilder()
.basePath(baseDirectory.absolutePath)
.userAgent(userAgentProvider.provide())
.addRootCertificates(userCertificatesProvider.provides())
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
.let {
// Sadly ClientBuilder.proxy() does not accept null :/
// Tracked by https://github.com/matrix-org/matrix-rust-sdk/issues/3159
val proxy = proxyProvider.provides()
if (proxy != null) {
it.proxy(proxy)
} else {
it
}
}
}
}
private fun SessionData.toSession() = Session(

View file

@ -25,8 +25,13 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
@ -36,11 +41,16 @@ import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.HumanQrLoginException
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
import org.matrix.rustcomponents.sdk.QrLoginProgress
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
@ -197,4 +207,43 @@ class RustMatrixAuthenticationService @Inject constructor(
}
}
}
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder()
.passphrase(pendingPassphrase)
.buildWithQrCode(
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
oidcConfiguration = oidcConfiguration,
progressListener = object : QrLoginProgressListener {
override fun onUpdate(state: QrLoginProgress) {
Timber.d("QR Code login progress: $state")
progress(state.toStep())
}
}
)
client.use { rustClient ->
val sessionData = rustClient.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
}
}.mapFailure {
when (it) {
is QrCodeDecodeException -> QrErrorMapper.map(it)
is HumanQrLoginException -> QrErrorMapper.map(it)
else -> it
}
}.onFailure { throwable ->
if (throwable is CancellationException) {
throw throwable
}
Timber.e(throwable, "Failed to login with QR code")
}
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
import org.matrix.rustcomponents.sdk.HumanQrLoginException as RustHumanQrLoginException
import org.matrix.rustcomponents.sdk.QrCodeDecodeException as RustQrCodeDecodeException
object QrErrorMapper {
fun map(qrCodeDecodeException: RustQrCodeDecodeException): QrCodeDecodeException = when (qrCodeDecodeException) {
is RustQrCodeDecodeException.Crypto -> {
// We plan to restore it in the future when UniFFi can process them
// val reason = when (qrCodeDecodeException.error) {
// LoginQrCodeDecodeError.NOT_ENOUGH_DATA -> QrCodeDecodeException.Crypto.Reason.NOT_ENOUGH_DATA
// LoginQrCodeDecodeError.NOT_UTF8 -> QrCodeDecodeException.Crypto.Reason.NOT_UTF8
// LoginQrCodeDecodeError.URL_PARSE -> QrCodeDecodeException.Crypto.Reason.URL_PARSE
// LoginQrCodeDecodeError.INVALID_MODE -> QrCodeDecodeException.Crypto.Reason.INVALID_MODE
// LoginQrCodeDecodeError.INVALID_VERSION -> QrCodeDecodeException.Crypto.Reason.INVALID_VERSION
// LoginQrCodeDecodeError.BASE64 -> QrCodeDecodeException.Crypto.Reason.BASE64
// LoginQrCodeDecodeError.INVALID_PREFIX -> QrCodeDecodeException.Crypto.Reason.INVALID_PREFIX
// }
QrCodeDecodeException.Crypto(
qrCodeDecodeException.message.orEmpty(),
// reason
)
}
}
fun map(humanQrLoginError: RustHumanQrLoginException): QrLoginException = when (humanQrLoginError) {
is RustHumanQrLoginException.Cancelled -> QrLoginException.Cancelled
is RustHumanQrLoginException.ConnectionInsecure -> QrLoginException.ConnectionInsecure
is RustHumanQrLoginException.Declined -> QrLoginException.Declined
is RustHumanQrLoginException.Expired -> QrLoginException.Expired
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import org.matrix.rustcomponents.sdk.QrLoginProgress
fun QrLoginProgress.toStep(): QrCodeLoginStep {
return when (this) {
is QrLoginProgress.EstablishingSecureChannel -> QrCodeLoginStep.EstablishingSecureChannel(checkCodeString)
is QrLoginProgress.Starting -> QrCodeLoginStep.Starting
is QrLoginProgress.WaitingForToken -> QrCodeLoginStep.WaitingForToken(userCode)
is QrLoginProgress.Done -> QrCodeLoginStep.Finished
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory
import org.matrix.rustcomponents.sdk.QrCodeData
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class RustQrCodeLoginDataFactory @Inject constructor() : MatrixQrCodeLoginDataFactory {
override fun parseQrCodeData(data: ByteArray): Result<MatrixQrCodeLoginData> {
return runCatching { SdkQrCodeLoginData(QrCodeData.fromBytes(data)) }
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.auth.qrlogin
import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData
import org.matrix.rustcomponents.sdk.QrCodeData as RustQrCodeData
class SdkQrCodeLoginData(
internal val rustQrCodeData: RustQrCodeData,
) : MatrixQrCodeLoginData