diff --git a/changelog.d/7369.feature b/changelog.d/7369.feature
new file mode 100644
index 0000000000..240fac3516
--- /dev/null
+++ b/changelog.d/7369.feature
@@ -0,0 +1 @@
+Add logic for sign in with QR code
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index fac36ffa52..e6714005a1 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3386,9 +3386,16 @@
Linking with this device is not supported.
The linking wasn’t completed in the required time.
The request was denied on the other device.
- Open ${app_name} on your other device
- Go to Settings -> Security & Privacy -> Show All Sessions
- Select \'Show QR code in this device\'
+ The request failed.
+ A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your internet connection(s); Your device(s);
+ The other device is already signed in.
+ The other device must be signed in.
+ That QR code is invalid.
+ The sign in was cancelled on the other device.
+ The homeserver doesn\'t support sign in with QR code.
+ Open the app on your other device
+ Go to Settings -> Security & Privacy
+ Select \'Show QR code\'
Start at the sign in screen
Select \'Sign in with QR code\'
Start at the sign in screen
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
index ae65963f37..22af8cebbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
@@ -27,6 +27,7 @@ open class LoggerTag(name: String, parentTag: LoggerTag? = null) {
object SYNC : LoggerTag("SYNC")
object VOIP : LoggerTag("VOIP")
object CRYPTO : LoggerTag("CRYPTO")
+ object RENDEZVOUS : LoggerTag("RZ")
val value: String = if (parentTag == null) {
name
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
new file mode 100644
index 0000000000..f724ac4b62
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+import android.net.Uri
+import org.matrix.android.sdk.api.auth.AuthenticationService
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.rendezvous.channels.ECDHRendezvousChannel
+import org.matrix.android.sdk.api.rendezvous.model.ECDHRendezvousCode
+import org.matrix.android.sdk.api.rendezvous.model.Outcome
+import org.matrix.android.sdk.api.rendezvous.model.Payload
+import org.matrix.android.sdk.api.rendezvous.model.PayloadType
+import org.matrix.android.sdk.api.rendezvous.model.Protocol
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousIntent
+import org.matrix.android.sdk.api.rendezvous.transports.SimpleHttpRendezvousTransport
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.util.MatrixJsonParser
+import timber.log.Timber
+
+/**
+ * Implementation of MSC3906 to sign in + E2EE set up using a QR code.
+ */
+class Rendezvous(
+ val channel: RendezvousChannel,
+ val theirIntent: RendezvousIntent,
+) {
+ companion object {
+ private val TAG = LoggerTag(Rendezvous::class.java.simpleName, LoggerTag.RENDEZVOUS).value
+
+ @Throws(RendezvousError::class)
+ fun buildChannelFromCode(code: String): Rendezvous {
+ val parsed = try {
+ // we rely on moshi validating the code and throwing exception if invalid JSON or doesn't
+ MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code)
+ } catch (a: Throwable) {
+ throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode)
+ } ?: throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode)
+
+ val transport = SimpleHttpRendezvousTransport(parsed.rendezvous.transport.uri)
+
+ return Rendezvous(
+ ECDHRendezvousChannel(transport, parsed.rendezvous.key),
+ parsed.intent
+ )
+ }
+ }
+
+ private val adapter = MatrixJsonParser.getMoshi().adapter(Payload::class.java)
+
+ // not yet implemented: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
+ val ourIntent: RendezvousIntent = RendezvousIntent.LOGIN_ON_NEW_DEVICE
+
+ @Throws(RendezvousError::class)
+ private suspend fun checkCompatibility() {
+ val incompatible = theirIntent == ourIntent
+
+ Timber.tag(TAG).d("ourIntent: $ourIntent, theirIntent: $theirIntent, incompatible: $incompatible")
+
+ if (incompatible) {
+ // inform the other side
+ send(Payload(PayloadType.FINISH, intent = ourIntent))
+ if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) {
+ throw RendezvousError("The other device isn't signed in", RendezvousFailureReason.OtherDeviceNotSignedIn)
+ } else {
+ throw RendezvousError("The other device is already signed in", RendezvousFailureReason.OtherDeviceAlreadySignedIn)
+ }
+ }
+ }
+
+ @Throws(RendezvousError::class)
+ suspend fun startAfterScanningCode(): String {
+ val checksum = channel.connect()
+
+ Timber.tag(TAG).i("Connected to secure channel with checksum: $checksum")
+
+ checkCompatibility()
+
+ // get protocols
+ Timber.tag(TAG).i("Waiting for protocols")
+ val protocolsResponse = receive()
+
+ if (protocolsResponse?.protocols == null || !protocolsResponse.protocols.contains(Protocol.LOGIN_TOKEN)) {
+ send(Payload(PayloadType.FINISH, outcome = Outcome.UNSUPPORTED))
+ throw RendezvousError("Unsupported protocols", RendezvousFailureReason.UnsupportedHomeserver)
+ }
+
+ send(Payload(PayloadType.PROGRESS, protocol = Protocol.LOGIN_TOKEN))
+
+ return checksum
+ }
+
+ @Throws(RendezvousError::class)
+ suspend fun waitForLoginOnNewDevice(authenticationService: AuthenticationService): Session {
+ Timber.tag(TAG).i("Waiting for login_token")
+
+ val loginToken = receive()
+
+ if (loginToken?.type == PayloadType.FINISH) {
+ when (loginToken.outcome) {
+ Outcome.DECLINED -> {
+ throw RendezvousError("Login declined by other device", RendezvousFailureReason.UserDeclined)
+ }
+ Outcome.UNSUPPORTED -> {
+ throw RendezvousError("Homeserver lacks support", RendezvousFailureReason.UnsupportedHomeserver)
+ }
+ else -> {
+ throw RendezvousError("Unknown error", RendezvousFailureReason.Unknown)
+ }
+ }
+ }
+
+ val homeserver = loginToken?.homeserver ?: throw RendezvousError("No homeserver returned", RendezvousFailureReason.ProtocolError)
+ val token = loginToken.loginToken ?: throw RendezvousError("No login token returned", RendezvousFailureReason.ProtocolError)
+
+ Timber.tag(TAG).i("Got login_token now attempting to sign in with $homeserver")
+
+ val hsConfig = HomeServerConnectionConfig(homeServerUri = Uri.parse(homeserver))
+ return authenticationService.loginUsingQrLoginToken(hsConfig, token)
+ }
+
+ @Throws(RendezvousError::class)
+ suspend fun completeVerificationOnNewDevice(session: Session) {
+ val userId = session.myUserId
+ val crypto = session.cryptoService()
+ val deviceId = crypto.getMyDevice().deviceId
+ val deviceKey = crypto.getMyDevice().fingerprint()
+ send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
+
+ // await confirmation of verification
+ val verificationResponse = receive()
+ if (verificationResponse?.outcome == Outcome.VERIFIED) {
+ val verifyingDeviceId = verificationResponse.verifyingDeviceId
+ ?: throw RendezvousError("No verifying device id returned", RendezvousFailureReason.ProtocolError)
+ val verifyingDeviceFromServer = crypto.getCryptoDeviceInfo(userId, verifyingDeviceId)
+ if (verifyingDeviceFromServer?.fingerprint() != verificationResponse.verifyingDeviceKey) {
+ Timber.tag(TAG).w(
+ "Verifying device $verifyingDeviceId key doesn't match: ${
+ verifyingDeviceFromServer?.fingerprint()
+ } vs ${verificationResponse.verifyingDeviceKey})"
+ )
+ // inform the other side
+ send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
+ throw RendezvousError("Key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
+ }
+
+ verificationResponse.masterKey?.let { masterKeyFromVerifyingDevice ->
+ // verifying device provided us with a master key, so use it to check integrity
+
+ // see what the homeserver told us
+ val localMasterKey = crypto.crossSigningService().getMyCrossSigningKeys()?.masterKey()
+
+ // n.b. if no local master key this is a problem, as well as it not matching
+ if (localMasterKey?.unpaddedBase64PublicKey != masterKeyFromVerifyingDevice) {
+ Timber.tag(TAG).w("Master key from verifying device doesn't match: $masterKeyFromVerifyingDevice vs $localMasterKey")
+ // inform the other side
+ send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
+ throw RendezvousError("Master key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
+ }
+
+ // set other device as verified
+ Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified")
+ crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)
+
+ Timber.tag(TAG).i("Setting master key as trusted")
+ crypto.crossSigningService().markMyMasterKeyAsTrusted()
+ } ?: run {
+ // set other device as verified anyway
+ Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified")
+ crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)
+
+ Timber.tag(TAG).i("No master key given by verifying device")
+ }
+
+ // request secrets from the verifying device
+ Timber.tag(TAG).i("Requesting secrets from $verifyingDeviceId")
+
+ session.sharedSecretStorageService().let {
+ it.requestSecret(MASTER_KEY_SSSS_NAME, verifyingDeviceId)
+ it.requestSecret(SELF_SIGNING_KEY_SSSS_NAME, verifyingDeviceId)
+ it.requestSecret(USER_SIGNING_KEY_SSSS_NAME, verifyingDeviceId)
+ it.requestSecret(KEYBACKUP_SECRET_SSSS_NAME, verifyingDeviceId)
+ }
+ } else {
+ Timber.tag(TAG).i("Not doing verification")
+ }
+ }
+
+ @Throws(RendezvousError::class)
+ private suspend fun receive(): Payload? {
+ val data = channel.receive() ?: return null
+ val payload = try {
+ adapter.fromJson(data.toString(Charsets.UTF_8))
+ } catch (e: Exception) {
+ Timber.tag(TAG).w(e, "Failed to parse payload")
+ throw RendezvousError("Invalid payload received", RendezvousFailureReason.Unknown)
+ }
+
+ return payload
+ }
+
+ private suspend fun send(payload: Payload) {
+ channel.send(adapter.toJson(payload).toByteArray(Charsets.UTF_8))
+ }
+
+ suspend fun close() {
+ channel.close()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt
new file mode 100644
index 0000000000..0956a5b0a0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+
+/**
+ * Representation of a rendezvous channel such as that described by MSC3903.
+ */
+interface RendezvousChannel {
+ val transport: RendezvousTransport
+
+ /**
+ * @returns the checksum/confirmation digits to be shown to the user
+ */
+ @Throws(RendezvousError::class)
+ suspend fun connect(): String
+
+ /**
+ * Send a payload via the channel.
+ * @param data payload to send
+ */
+ @Throws(RendezvousError::class)
+ suspend fun send(data: ByteArray)
+
+ /**
+ * Receive a payload from the channel.
+ * @returns the received payload
+ */
+ @Throws(RendezvousError::class)
+ suspend fun receive(): ByteArray?
+
+ /**
+ * Closes the channel and cleans up.
+ */
+ suspend fun close()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt
new file mode 100644
index 0000000000..18e625d825
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+enum class RendezvousFailureReason(val canRetry: Boolean = true) {
+ UserDeclined,
+ OtherDeviceNotSignedIn,
+ OtherDeviceAlreadySignedIn,
+ Unknown,
+ Expired,
+ UserCancelled,
+ InvalidCode,
+ UnsupportedAlgorithm(false),
+ UnsupportedTransport(false),
+ UnsupportedHomeserver(false),
+ ProtocolError,
+ E2EESecurityIssue(false)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt
new file mode 100644
index 0000000000..81632e951a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous
+
+import okhttp3.MediaType
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportDetails
+
+interface RendezvousTransport {
+ var ready: Boolean
+
+ @Throws(RendezvousError::class)
+ suspend fun details(): RendezvousTransportDetails
+
+ @Throws(RendezvousError::class)
+ suspend fun send(contentType: MediaType, data: ByteArray)
+
+ @Throws(RendezvousError::class)
+ suspend fun receive(): ByteArray?
+
+ suspend fun close()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt
new file mode 100644
index 0000000000..c1d6b1b70e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/channels/ECDHRendezvousChannel.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.channels
+
+import android.util.Base64
+import com.squareup.moshi.JsonClass
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import okhttp3.MediaType.Companion.toMediaType
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.rendezvous.RendezvousChannel
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+import org.matrix.android.sdk.api.rendezvous.RendezvousTransport
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm
+import org.matrix.android.sdk.api.util.MatrixJsonParser
+import org.matrix.android.sdk.internal.crypto.verification.SASDefaultVerificationTransaction
+import org.matrix.olm.OlmSAS
+import timber.log.Timber
+import java.security.SecureRandom
+import java.util.LinkedList
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ * Implements X25519 ECDH key agreement and AES-256-GCM encryption channel as per MSC3903:
+ * https://github.com/matrix-org/matrix-spec-proposals/pull/3903
+ */
+class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPublicKeyBase64: String?) : RendezvousChannel {
+ companion object {
+ private const val ALGORITHM_SPEC = "AES/GCM/NoPadding"
+ private const val KEY_SPEC = "AES"
+ private val TAG = LoggerTag(ECDHRendezvousChannel::class.java.simpleName, LoggerTag.RENDEZVOUS).value
+ }
+
+ @JsonClass(generateAdapter = true)
+ internal data class ECDHPayload(
+ val algorithm: SecureRendezvousChannelAlgorithm? = null,
+ val key: String? = null,
+ val ciphertext: String? = null,
+ val iv: String? = null
+ )
+
+ private val olmSASMutex = Mutex()
+ private var olmSAS: OlmSAS?
+ private val ourPublicKey: ByteArray
+ private val ecdhAdapter = MatrixJsonParser.getMoshi().adapter(ECDHPayload::class.java)
+ private var theirPublicKey: ByteArray? = null
+ private var aesKey: ByteArray? = null
+
+ init {
+ theirPublicKeyBase64?.let {
+ theirPublicKey = Base64.decode(it, Base64.NO_WRAP)
+ }
+ olmSAS = OlmSAS()
+ ourPublicKey = Base64.decode(olmSAS!!.publicKey, Base64.NO_WRAP)
+ }
+
+ @Throws(RendezvousError::class)
+ override suspend fun connect(): String {
+ val sas = olmSAS ?: throw RendezvousError("Channel closed", RendezvousFailureReason.Unknown)
+ val isInitiator = theirPublicKey == null
+
+ if (isInitiator) {
+ Timber.tag(TAG).i("Waiting for other device to send their public key")
+ val res = this.receiveAsPayload() ?: throw RendezvousError("No reply from other device", RendezvousFailureReason.ProtocolError)
+
+ if (res.key == null) {
+ throw RendezvousError(
+ "Unsupported algorithm: ${res.algorithm}",
+ RendezvousFailureReason.UnsupportedAlgorithm,
+ )
+ }
+ theirPublicKey = Base64.decode(res.key, Base64.NO_WRAP)
+ } else {
+ // send our public key unencrypted
+ Timber.tag(TAG).i("Sending public key")
+ send(
+ ECDHPayload(
+ algorithm = SecureRendezvousChannelAlgorithm.ECDH_V1,
+ key = Base64.encodeToString(ourPublicKey, Base64.NO_WRAP)
+ )
+ )
+ }
+
+ olmSASMutex.withLock {
+ sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
+ sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
+
+ val initiatorKey = Base64.encodeToString(if (isInitiator) ourPublicKey else theirPublicKey, Base64.NO_WRAP)
+ val recipientKey = Base64.encodeToString(if (isInitiator) theirPublicKey else ourPublicKey, Base64.NO_WRAP)
+ val aesInfo = "${SecureRendezvousChannelAlgorithm.ECDH_V1.value}|$initiatorKey|$recipientKey"
+
+ aesKey = sas.generateShortCode(aesInfo, 32)
+
+ val rawChecksum = sas.generateShortCode(aesInfo, 5)
+ return SASDefaultVerificationTransaction.getDecimalCodeRepresentation(rawChecksum, separator = "-")
+ }
+ }
+
+ private suspend fun send(payload: ECDHPayload) {
+ transport.send("application/json".toMediaType(), ecdhAdapter.toJson(payload).toByteArray(Charsets.UTF_8))
+ }
+
+ override suspend fun send(data: ByteArray) {
+ if (aesKey == null) {
+ throw IllegalStateException("Shared secret not established")
+ }
+ send(encrypt(data))
+ }
+
+ private suspend fun receiveAsPayload(): ECDHPayload? {
+ transport.receive()?.toString(Charsets.UTF_8)?.let {
+ return ecdhAdapter.fromJson(it)
+ } ?: return null
+ }
+
+ override suspend fun receive(): ByteArray? {
+ if (aesKey == null) {
+ throw IllegalStateException("Shared secret not established")
+ }
+ val payload = receiveAsPayload() ?: return null
+ return decrypt(payload)
+ }
+
+ override suspend fun close() {
+ val sas = olmSAS ?: throw IllegalStateException("Channel already closed")
+ olmSASMutex.withLock {
+ // this does a double release check already so we don't re-check ourselves
+ sas.releaseSas()
+ olmSAS = null
+ }
+ transport.close()
+ }
+
+ private fun encrypt(plainText: ByteArray): ECDHPayload {
+ val iv = ByteArray(16)
+ SecureRandom().nextBytes(iv)
+
+ val cipherText = LinkedList()
+
+ val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC)
+ val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC)
+ val ivParameterSpec = IvParameterSpec(iv)
+ encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
+ cipherText.addAll(encryptCipher.update(plainText).toList())
+ cipherText.addAll(encryptCipher.doFinal().toList())
+
+ return ECDHPayload(
+ ciphertext = Base64.encodeToString(cipherText.toByteArray(), Base64.NO_WRAP),
+ iv = Base64.encodeToString(iv, Base64.NO_WRAP)
+ )
+ }
+
+ private fun decrypt(payload: ECDHPayload): ByteArray {
+ val iv = Base64.decode(payload.iv, Base64.NO_WRAP)
+ val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC)
+ val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC)
+ val ivParameterSpec = IvParameterSpec(iv)
+ encryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
+
+ val plainText = LinkedList()
+ plainText.addAll(encryptCipher.update(Base64.decode(payload.ciphertext, Base64.NO_WRAP)).toList())
+ plainText.addAll(encryptCipher.doFinal().toList())
+
+ return plainText.toByteArray()
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvous.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvous.kt
new file mode 100644
index 0000000000..55bac6397e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvous.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ECDHRendezvous(
+ val transport: SimpleHttpRendezvousTransportDetails,
+ val algorithm: SecureRendezvousChannelAlgorithm,
+ val key: String
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvousCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvousCode.kt
new file mode 100644
index 0000000000..575b5d4bfd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/ECDHRendezvousCode.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ECDHRendezvousCode(
+ val intent: RendezvousIntent,
+ val rendezvous: ECDHRendezvous
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Outcome.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Outcome.kt
new file mode 100644
index 0000000000..0ebd1f88b3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Outcome.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class Outcome(val value: String) {
+ @Json(name = "success")
+ SUCCESS("success"),
+
+ @Json(name = "declined")
+ DECLINED("declined"),
+
+ @Json(name = "unsupported")
+ UNSUPPORTED("unsupported"),
+
+ @Json(name = "verified")
+ VERIFIED("verified"),
+
+ @Json(name = "e2ee_security_error")
+ E2EE_SECURITY_ERROR("e2ee_security_error")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Payload.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Payload.kt
new file mode 100644
index 0000000000..04631ce959
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Payload.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class Payload(
+ val type: PayloadType,
+ val intent: RendezvousIntent? = null,
+ val outcome: Outcome? = null,
+ val protocols: List? = null,
+ val protocol: Protocol? = null,
+ val homeserver: String? = null,
+ @Json(name = "login_token") val loginToken: String? = null,
+ @Json(name = "device_id") val deviceId: String? = null,
+ @Json(name = "device_key") val deviceKey: String? = null,
+ @Json(name = "verifying_device_id") val verifyingDeviceId: String? = null,
+ @Json(name = "verifying_device_key") val verifyingDeviceKey: String? = null,
+ @Json(name = "master_key") val masterKey: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/PayloadType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/PayloadType.kt
new file mode 100644
index 0000000000..33beb1f525
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/PayloadType.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+internal enum class PayloadType(val value: String) {
+ @Json(name = "m.login.start")
+ START("m.login.start"),
+
+ @Json(name = "m.login.finish")
+ FINISH("m.login.finish"),
+
+ @Json(name = "m.login.progress")
+ PROGRESS("m.login.progress")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Protocol.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Protocol.kt
new file mode 100644
index 0000000000..6fce2fa11c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/Protocol.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class Protocol(val value: String) {
+ @Json(name = "org.matrix.msc3906.login_token")
+ LOGIN_TOKEN("org.matrix.msc3906.login_token")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt
new file mode 100644
index 0000000000..c52b11a322
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+
+class RendezvousError(val description: String, val reason: RendezvousFailureReason) : Exception(description)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousIntent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousIntent.kt
new file mode 100644
index 0000000000..65037e1252
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousIntent.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class RendezvousIntent {
+ @Json(name = "login.start") LOGIN_ON_NEW_DEVICE,
+ @Json(name = "login.reciprocate") RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
+}
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginErrorType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt
similarity index 69%
rename from vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginErrorType.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt
index 9a6cc13de0..1bde43ab7e 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginErrorType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportDetails.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 New Vector Ltd
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package im.vector.app.features.login.qr
+package org.matrix.android.sdk.api.rendezvous.model
-enum class QrCodeLoginErrorType {
- DEVICE_IS_NOT_SUPPORTED,
- TIMEOUT,
- REQUEST_WAS_DENIED,
-}
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+open class RendezvousTransportDetails(
+ val type: RendezvousTransportType
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportType.kt
new file mode 100644
index 0000000000..6fca7efa71
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousTransportType.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class RendezvousTransportType(val value: String) {
+ @Json(name = "org.matrix.msc3886.http.v1")
+ MSC3886_SIMPLE_HTTP_V1("org.matrix.msc3886.http.v1")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt
new file mode 100644
index 0000000000..75f0024fda
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SecureRendezvousChannelAlgorithm.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class SecureRendezvousChannelAlgorithm(val value: String) {
+ @Json(name = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256")
+ ECDH_V1("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256")
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt
new file mode 100644
index 0000000000..049aa8b756
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/SimpleHttpRendezvousTransportDetails.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.model
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class SimpleHttpRendezvousTransportDetails(
+ val uri: String
+) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt
new file mode 100644
index 0000000000..620b599e3d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.api.rendezvous.transports
+
+import kotlinx.coroutines.delay
+import okhttp3.MediaType
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+import org.matrix.android.sdk.api.rendezvous.RendezvousTransport
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportDetails
+import org.matrix.android.sdk.api.rendezvous.model.SimpleHttpRendezvousTransportDetails
+import timber.log.Timber
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Implementation of the Simple HTTP transport MSC3886: https://github.com/matrix-org/matrix-spec-proposals/pull/3886
+ */
+class SimpleHttpRendezvousTransport(rendezvousUri: String?) : RendezvousTransport {
+ companion object {
+ private val TAG = LoggerTag(SimpleHttpRendezvousTransport::class.java.simpleName, LoggerTag.RENDEZVOUS).value
+ }
+
+ override var ready = false
+ private var cancelled = false
+ private var uri: String?
+ private var etag: String? = null
+ private var expiresAt: Date? = null
+
+ init {
+ uri = rendezvousUri
+ }
+
+ override suspend fun details(): RendezvousTransportDetails {
+ val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
+
+ return SimpleHttpRendezvousTransportDetails(uri)
+ }
+
+ @Throws(RendezvousError::class)
+ override suspend fun send(contentType: MediaType, data: ByteArray) {
+ if (cancelled) {
+ throw IllegalStateException("Rendezvous cancelled")
+ }
+
+ val method = if (uri != null) "PUT" else "POST"
+ val uri = this.uri ?: throw RuntimeException("No rendezvous URI")
+
+ val httpClient = okhttp3.OkHttpClient.Builder().build()
+
+ val request = Request.Builder()
+ .url(uri)
+ .method(method, data.toRequestBody())
+ .header("content-type", contentType.toString())
+
+ etag?.let {
+ request.header("if-match", it)
+ }
+
+ val response = httpClient.newCall(request.build()).execute()
+
+ if (response.code == 404) {
+ throw get404Error()
+ }
+ etag = response.header("etag")
+
+ Timber.tag(TAG).i("Sent data to $uri new etag $etag")
+
+ if (method == "POST") {
+ val location = response.header("location") ?: throw RuntimeException("No rendezvous URI found in response")
+
+ response.header("expires")?.let {
+ val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
+ expiresAt = format.parse(it)
+ }
+
+ // resolve location header which could be relative or absolute
+ this.uri = response.request.url.toUri().resolve(location).toString()
+ ready = true
+ }
+ }
+
+ @Throws(RendezvousError::class)
+ override suspend fun receive(): ByteArray? {
+ if (cancelled) {
+ throw IllegalStateException("Rendezvous cancelled")
+ }
+ val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
+ val httpClient = okhttp3.OkHttpClient.Builder().build()
+ while (true) {
+ Timber.tag(TAG).i("Polling: $uri after etag $etag")
+ val request = Request.Builder()
+ .url(uri)
+ .get()
+
+ etag?.let {
+ request.header("if-none-match", it)
+ }
+
+ val response = httpClient.newCall(request.build()).execute()
+
+ try {
+ // expired
+ if (response.code == 404) {
+ throw get404Error()
+ }
+
+ // rely on server expiring the channel rather than checking ourselves
+
+ if (response.header("content-type") != "application/json") {
+ response.header("etag")?.let {
+ etag = it
+ }
+ } else if (response.code == 200) {
+ response.header("etag")?.let {
+ etag = it
+ }
+ return response.body?.bytes()
+ }
+
+ // sleep for a second before polling again
+ // we rely on the server expiring the channel rather than checking it ourselves
+ delay(1000)
+ } finally {
+ response.close()
+ }
+ }
+ }
+
+ private fun get404Error(): RendezvousError {
+ if (expiresAt != null && Date() > expiresAt) {
+ return RendezvousError("Expired", RendezvousFailureReason.Expired)
+ }
+
+ return RendezvousError("Received unexpected 404", RendezvousFailureReason.Unknown)
+ }
+
+ override suspend fun close() {
+ cancelled = true
+ ready = false
+
+ uri?.let {
+ try {
+ val httpClient = okhttp3.OkHttpClient.Builder().build()
+ val request = Request.Builder()
+ .url(it)
+ .delete()
+ .build()
+ httpClient.newCall(request).execute()
+ } catch (e: Throwable) {
+ Timber.tag(TAG).w(e, "Failed to delete channel")
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
index 1cbaff059a..29b416bb82 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
@@ -82,6 +82,33 @@ internal abstract class SASDefaultVerificationTransaction(
// older devices have limited support of emoji but SDK offers images for the 64 verification emojis
// so always send that we support EMOJI
val KNOWN_SHORT_CODES = listOf(SasMode.EMOJI, SasMode.DECIMAL)
+
+ /**
+ * decimal: generate five bytes by using HKDF.
+ * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive),
+ * and add 1000 (resulting in a number between 1000 and 9191 inclusive).
+ * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers.
+ * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000,
+ * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000.
+ * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic,
+ * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.)
+ * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers,
+ * or with the three numbers on separate lines.
+ */
+ fun getDecimalCodeRepresentation(byteArray: ByteArray, separator: String = " "): String {
+ val b0 = byteArray[0].toUnsignedInt() // need unsigned byte
+ val b1 = byteArray[1].toUnsignedInt() // need unsigned byte
+ val b2 = byteArray[2].toUnsignedInt() // need unsigned byte
+ val b3 = byteArray[3].toUnsignedInt() // need unsigned byte
+ val b4 = byteArray[4].toUnsignedInt() // need unsigned byte
+ // (B0 << 5 | B1 >> 3) + 1000
+ val first = (b0.shl(5) or b1.shr(3)) + 1000
+ // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000
+ val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000
+ // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000
+ val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000
+ return "$first$separator$second$separator$third"
+ }
}
override var state: VerificationTxState = VerificationTxState.None
@@ -371,33 +398,6 @@ internal abstract class SASDefaultVerificationTransaction(
return getDecimalCodeRepresentation(shortCodeBytes!!)
}
- /**
- * decimal: generate five bytes by using HKDF.
- * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive),
- * and add 1000 (resulting in a number between 1000 and 9191 inclusive).
- * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers.
- * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000,
- * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000.
- * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic,
- * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.)
- * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers,
- * or with the three numbers on separate lines.
- */
- fun getDecimalCodeRepresentation(byteArray: ByteArray): String {
- val b0 = byteArray[0].toUnsignedInt() // need unsigned byte
- val b1 = byteArray[1].toUnsignedInt() // need unsigned byte
- val b2 = byteArray[2].toUnsignedInt() // need unsigned byte
- val b3 = byteArray[3].toUnsignedInt() // need unsigned byte
- val b4 = byteArray[4].toUnsignedInt() // need unsigned byte
- // (B0 << 5 | B1 >> 3) + 1000
- val first = (b0.shl(5) or b1.shr(3)) + 1000
- // ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000
- val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000
- // ((B3 & 0x3f) << 7 | B4 >> 1) + 1000
- val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000
- return "$first $second $third"
- }
-
override fun getEmojiCodeRepresentation(): List {
return getEmojiCodeRepresentation(shortCodeBytes!!)
}
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt
index 8854d0720f..5ea46d3dcd 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt
@@ -22,4 +22,5 @@ sealed class QrCodeLoginAction : VectorViewModelAction {
data class OnQrCodeScanned(val qrCode: String) : QrCodeLoginAction()
object GenerateQrCode : QrCodeLoginAction()
object ShowQrCode : QrCodeLoginAction()
+ object TryAgain : QrCodeLoginAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt
index f5fd17c0c8..a0c113224d 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt
@@ -24,7 +24,9 @@ import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.SimpleFragmentActivity
+import im.vector.app.features.home.HomeActivity
import im.vector.lib.core.utils.compat.getParcelableCompat
import timber.log.Timber
@@ -37,32 +39,35 @@ class QrCodeLoginActivity : SimpleFragmentActivity() {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
- val qrCodeLoginArgs: QrCodeLoginArgs? = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG)
if (isFirstCreation()) {
- when (qrCodeLoginArgs?.loginType) {
- QrCodeLoginType.LOGIN -> {
- showInstructionsFragment(qrCodeLoginArgs)
- }
- QrCodeLoginType.LINK_A_DEVICE -> {
- if (qrCodeLoginArgs.showQrCodeImmediately) {
- handleNavigateToShowQrCodeScreen()
- } else {
- showInstructionsFragment(qrCodeLoginArgs)
- }
- }
- null -> {
- Timber.i("QrCodeLoginArgs is null. This is not expected.")
- finish()
- return
- }
- }
+ navigateToInitialFragment()
}
observeViewEvents()
}
+ private fun navigateToInitialFragment() {
+ val qrCodeLoginArgs: QrCodeLoginArgs? = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG)
+ when (qrCodeLoginArgs?.loginType) {
+ QrCodeLoginType.LOGIN -> {
+ showInstructionsFragment(qrCodeLoginArgs)
+ }
+ QrCodeLoginType.LINK_A_DEVICE -> {
+ if (qrCodeLoginArgs.showQrCodeImmediately) {
+ handleNavigateToShowQrCodeScreen()
+ } else {
+ showInstructionsFragment(qrCodeLoginArgs)
+ }
+ }
+ null -> {
+ Timber.i("QrCodeLoginArgs is null. This is not expected.")
+ finish()
+ }
+ }
+ }
+
private fun showInstructionsFragment(qrCodeLoginArgs: QrCodeLoginArgs) {
- addFragment(
+ replaceFragment(
views.container,
QrCodeLoginInstructionsFragment::class.java,
qrCodeLoginArgs,
@@ -75,10 +80,16 @@ class QrCodeLoginActivity : SimpleFragmentActivity() {
when (it) {
QrCodeLoginViewEvents.NavigateToStatusScreen -> handleNavigateToStatusScreen()
QrCodeLoginViewEvents.NavigateToShowQrCodeScreen -> handleNavigateToShowQrCodeScreen()
+ QrCodeLoginViewEvents.NavigateToHomeScreen -> handleNavigateToHomeScreen()
+ QrCodeLoginViewEvents.NavigateToInitialScreen -> handleNavigateToInitialScreen()
}
}
}
+ private fun handleNavigateToInitialScreen() {
+ navigateToInitialFragment()
+ }
+
private fun handleNavigateToShowQrCodeScreen() {
addFragment(
views.container,
@@ -95,6 +106,11 @@ class QrCodeLoginActivity : SimpleFragmentActivity() {
)
}
+ private fun handleNavigateToHomeScreen() {
+ val intent = HomeActivity.newIntent(this, firstStartMainActivity = false, existingSession = true)
+ startActivity(intent)
+ }
+
companion object {
private const val FRAGMENT_QR_CODE_INSTRUCTIONS_TAG = "FRAGMENT_QR_CODE_INSTRUCTIONS_TAG"
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt
index 330562b874..4bef41b6c1 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt
@@ -16,9 +16,11 @@
package im.vector.app.features.login.qr
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+
sealed class QrCodeLoginConnectionStatus {
object ConnectingToDevice : QrCodeLoginConnectionStatus()
data class Connected(val securityCode: String, val canConfirmSecurityCode: Boolean) : QrCodeLoginConnectionStatus()
object SigningIn : QrCodeLoginConnectionStatus()
- data class Failed(val errorType: QrCodeLoginErrorType, val canTryAgain: Boolean) : QrCodeLoginConnectionStatus()
+ data class Failed(val errorType: RendezvousFailureReason, val canTryAgain: Boolean) : QrCodeLoginConnectionStatus()
}
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt
index efd23f2530..40fcbbbb85 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt
@@ -63,6 +63,7 @@ class QrCodeLoginInstructionsFragment : VectorBaseFragment
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt
index a9c589e469..6ef261e6d9 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt
@@ -28,6 +28,7 @@ import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginStatusBinding
import im.vector.app.features.themes.ThemeUtils
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
@AndroidEntryPoint
class QrCodeLoginStatusFragment : VectorBaseFragment() {
@@ -41,6 +42,13 @@ class QrCodeLoginStatusFragment : VectorBaseFragment getString(R.string.qr_code_login_header_failed_device_is_not_supported_description)
- QrCodeLoginErrorType.TIMEOUT -> getString(R.string.qr_code_login_header_failed_timeout_description)
- QrCodeLoginErrorType.REQUEST_WAS_DENIED -> getString(R.string.qr_code_login_header_failed_denied_description)
+ private fun getErrorDescription(reason: RendezvousFailureReason): String {
+ return when (reason) {
+ RendezvousFailureReason.UnsupportedAlgorithm,
+ RendezvousFailureReason.UnsupportedTransport -> getString(R.string.qr_code_login_header_failed_device_is_not_supported_description)
+ RendezvousFailureReason.UnsupportedHomeserver -> getString(R.string.qr_code_login_header_failed_homeserver_is_not_supported_description)
+ RendezvousFailureReason.Expired -> getString(R.string.qr_code_login_header_failed_timeout_description)
+ RendezvousFailureReason.UserDeclined -> getString(R.string.qr_code_login_header_failed_denied_description)
+ RendezvousFailureReason.E2EESecurityIssue -> getString(R.string.qr_code_login_header_failed_e2ee_security_issue_description)
+ RendezvousFailureReason.OtherDeviceAlreadySignedIn -> getString(R.string.qr_code_login_header_failed_other_device_already_signed_in_description)
+ RendezvousFailureReason.OtherDeviceNotSignedIn -> getString(R.string.qr_code_login_header_failed_other_device_not_signed_in_description)
+ RendezvousFailureReason.InvalidCode -> getString(R.string.qr_code_login_header_failed_invalid_qr_code_description)
+ RendezvousFailureReason.UserCancelled -> getString(R.string.qr_code_login_header_failed_user_cancelled_description)
+ else -> getString(R.string.qr_code_login_header_failed_other_description)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt
index dc258408e7..e20ea6b2e8 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt
@@ -21,4 +21,6 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class QrCodeLoginViewEvents : VectorViewEvents {
object NavigateToStatusScreen : QrCodeLoginViewEvents()
object NavigateToShowQrCodeScreen : QrCodeLoginViewEvents()
+ object NavigateToHomeScreen : QrCodeLoginViewEvents()
+ object NavigateToInitialScreen : QrCodeLoginViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt
index e979ffa63c..97cca9d791 100644
--- a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt
@@ -20,13 +20,24 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.core.session.ConfigureAndStartSessionUseCase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.auth.AuthenticationService
+import org.matrix.android.sdk.api.rendezvous.Rendezvous
+import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
+import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import timber.log.Timber
class QrCodeLoginViewModel @AssistedInject constructor(
@Assisted private val initialState: QrCodeLoginViewState,
+ private val authenticationService: AuthenticationService,
+ private val activeSessionHolder: ActiveSessionHolder,
+ private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) : VectorViewModel(initialState) {
@AssistedFactory
@@ -34,16 +45,28 @@ class QrCodeLoginViewModel @AssistedInject constructor(
override fun create(initialState: QrCodeLoginViewState): QrCodeLoginViewModel
}
- companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() {
+ val TAG: String = QrCodeLoginViewModel::class.java.simpleName
+ }
override fun handle(action: QrCodeLoginAction) {
when (action) {
is QrCodeLoginAction.OnQrCodeScanned -> handleOnQrCodeScanned(action)
QrCodeLoginAction.GenerateQrCode -> handleQrCodeViewStarted()
QrCodeLoginAction.ShowQrCode -> handleShowQrCode()
+ QrCodeLoginAction.TryAgain -> handleTryAgain()
}
}
+ private fun handleTryAgain() {
+ setState {
+ copy(
+ connectionStatus = null
+ )
+ }
+ _viewEvents.post(QrCodeLoginViewEvents.NavigateToInitialScreen)
+ }
+
private fun handleShowQrCode() {
_viewEvents.post(QrCodeLoginViewEvents.NavigateToShowQrCodeScreen)
}
@@ -58,20 +81,60 @@ class QrCodeLoginViewModel @AssistedInject constructor(
}
private fun handleOnQrCodeScanned(action: QrCodeLoginAction.OnQrCodeScanned) {
- if (isValidQrCode(action.qrCode)) {
- setState {
- copy(
- connectionStatus = QrCodeLoginConnectionStatus.ConnectingToDevice
- )
+ Timber.tag(TAG).d("Scanned code of length ${action.qrCode.length}")
+
+ val rendezvous = try { Rendezvous.buildChannelFromCode(action.qrCode) } catch (t: Throwable) {
+ Timber.tag(TAG).e(t, "Error occurred during sign in")
+ if (t is RendezvousError) {
+ onFailed(t.reason)
+ } else {
+ onFailed(RendezvousFailureReason.Unknown)
+ }
+ return
+ }
+
+ setState {
+ copy(
+ connectionStatus = QrCodeLoginConnectionStatus.ConnectingToDevice
+ )
+ }
+
+ _viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
+
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ val confirmationCode = rendezvous.startAfterScanningCode()
+ Timber.tag(TAG).i("Established secure channel with checksum: $confirmationCode")
+
+ onConnectionEstablished(confirmationCode)
+
+ val session = rendezvous.waitForLoginOnNewDevice(authenticationService)
+ onSigningIn()
+
+ activeSessionHolder.setActiveSession(session)
+ authenticationService.reset()
+ configureAndStartSessionUseCase.execute(session)
+
+ rendezvous.completeVerificationOnNewDevice(session)
+
+ _viewEvents.post(QrCodeLoginViewEvents.NavigateToHomeScreen)
+ } catch (t: Throwable) {
+ Timber.tag(TAG).e(t, "Error occurred during sign in")
+ if (t is RendezvousError) {
+ onFailed(t.reason)
+ } else {
+ onFailed(RendezvousFailureReason.Unknown)
+ }
}
- _viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
}
}
- private fun onFailed(errorType: QrCodeLoginErrorType, canTryAgain: Boolean) {
+ private fun onFailed(reason: RendezvousFailureReason) {
+ _viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
+
setState {
copy(
- connectionStatus = QrCodeLoginConnectionStatus.Failed(errorType, canTryAgain)
+ connectionStatus = QrCodeLoginConnectionStatus.Failed(reason, reason.canRetry)
)
}
}
@@ -93,14 +156,11 @@ class QrCodeLoginViewModel @AssistedInject constructor(
}
}
- // TODO. Implement in the logic related PR.
- private fun isValidQrCode(qrCode: String): Boolean {
- Timber.d("isValidQrCode: $qrCode")
- return false
- }
-
- // TODO. Implement in the logic related PR.
+ /**
+ * QR code generation is not currently supported and this is a placeholder for future
+ * functionality.
+ */
private fun generateQrCodeData(): String {
- return "TODO"
+ return "NOT SUPPORTED"
}
}