diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 7d0173f341..e28ebb31c6 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3383,6 +3383,12 @@ The linking wasn’t completed in the required time. The request was denied on the other device. The request failed. + A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your intent connection(s); Your device(s); + The other device is already signed in. + The other device must be signed in. + The QR code scanned is invalid. + The sign in was cancelled on the other device. + The homeserver doesn\'t support sign in with QR code. Open ${app_name} on your other device Go to Settings -> Security & Privacy -> Show All Sessions Select \'Show QR code in this device\' 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 index e467ff06e3..9ad889fca0 100644 --- 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 @@ -26,6 +26,7 @@ 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 @@ -47,10 +48,16 @@ class Rendezvous( companion object { private val TAG = LoggerTag(Rendezvous::class.java.simpleName, LoggerTag.RENDEZVOUS).value - fun buildChannelFromCode(code: String, onCancelled: (reason: RendezvousFailureReason) -> Unit): Rendezvous { - val parsed = MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code) ?: throw RuntimeException("Invalid code") + @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(onCancelled, parsed.rendezvous.transport.uri) + val transport = SimpleHttpRendezvousTransport(parsed.rendezvous.transport.uri) return Rendezvous( ECDHRendezvousChannel(transport, parsed.rendezvous.key), @@ -64,32 +71,30 @@ class Rendezvous( // not yet implemented: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE val ourIntent: RendezvousIntent = RendezvousIntent.LOGIN_ON_NEW_DEVICE - private suspend fun areIntentsIncompatible(): Boolean { + @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)) - val reason = if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) { - RendezvousFailureReason.OtherDeviceNotSignedIn + if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) { + throw RendezvousError("The other device isn't signed in", RendezvousFailureReason.OtherDeviceNotSignedIn) } else { - RendezvousFailureReason.OtherDeviceAlreadySignedIn + throw RendezvousError("The other device is already signed in", RendezvousFailureReason.OtherDeviceAlreadySignedIn) } - channel.cancel(reason) } - - return incompatible } + @Throws(RendezvousError::class) suspend fun startAfterScanningCode(): String? { val checksum = channel.connect() Timber.tag(TAG).i("Connected to secure channel with checksum: $checksum") - if (areIntentsIncompatible()) { - return null - } + checkCompatibility() // get protocols Timber.tag(TAG).i("Waiting for protocols") @@ -97,9 +102,7 @@ class Rendezvous( if (protocolsResponse?.protocols == null || !protocolsResponse.protocols.contains(Protocol.LOGIN_TOKEN)) { send(Payload(PayloadType.FINISH, outcome = Outcome.UNSUPPORTED)) - Timber.tag(TAG).i("No supported protocol") - cancel(RendezvousFailureReason.Unknown) - return null + throw RendezvousError("Unsupported protocols", RendezvousFailureReason.UnsupportedHomeserver) } send(Payload(PayloadType.PROGRESS, protocol = Protocol.LOGIN_TOKEN)) @@ -107,6 +110,7 @@ class Rendezvous( return checksum } + @Throws(RendezvousError::class) suspend fun waitForLoginOnNewDevice(authenticationService: AuthenticationService): Session? { Timber.tag(TAG).i("Waiting for login_token") @@ -115,24 +119,19 @@ class Rendezvous( if (loginToken?.type == PayloadType.FINISH) { when (loginToken.outcome) { Outcome.DECLINED -> { - Timber.tag(TAG).i("Login declined by other device") - channel.cancel(RendezvousFailureReason.UserDeclined) - return null + throw RendezvousError("Login declined by other device", RendezvousFailureReason.UserDeclined) } Outcome.UNSUPPORTED -> { - Timber.tag(TAG).i("Not supported") - channel.cancel(RendezvousFailureReason.HomeserverLacksSupport) - return null + throw RendezvousError("Homeserver lacks support", RendezvousFailureReason.UnsupportedHomeserver) } else -> { - channel.cancel(RendezvousFailureReason.Unknown) - return null + throw RendezvousError("Unknown error", RendezvousFailureReason.Unknown) } } } - val homeserver = loginToken?.homeserver ?: throw RuntimeException("No homeserver returned") - val token = loginToken.loginToken ?: throw RuntimeException("No login token returned") + 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") @@ -140,6 +139,7 @@ class Rendezvous( return authenticationService.loginUsingQrLoginToken(hsConfig, token) } + @Throws(RendezvousError::class) suspend fun completeVerificationOnNewDevice(session: Session) { val userId = session.myUserId val crypto = session.cryptoService() @@ -148,59 +148,77 @@ class Rendezvous( send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey)) // await confirmation of verification - val verificationResponse = receive() - val verifyingDeviceId = verificationResponse?.verifyingDeviceId ?: throw RuntimeException("No verifying device id returned") - 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})" - ) - throw RuntimeException("Key from verifying device doesn't match") - } + 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) + } - // set other device as verified - Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified") - crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId) + verificationResponse.masterKey?.let { masterKeyFromVerifyingDevice -> + // check master key againt what the homeserver told us + crypto.crossSigningService().getMyCrossSigningKeys()?.masterKey()?.let { localMasterKey -> + 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) - verificationResponse.masterKey ?.let { masterKeyFromVerifyingDevice -> - // set master key as trusted - crypto.crossSigningService().getMyCrossSigningKeys()?.masterKey()?.let { localMasterKey -> - if (localMasterKey.unpaddedBase64PublicKey == masterKeyFromVerifyingDevice) { Timber.tag(TAG).i("Setting master key as trusted") crypto.crossSigningService().markMyMasterKeyAsTrusted() - } else { - Timber.tag(TAG).w("Master key from verifying device doesn't match: $masterKeyFromVerifyingDevice vs $localMasterKey") - throw RuntimeException("Master key from verifying device doesn't match") - } - } ?: Timber.tag(TAG).i("No local master key") - } ?: Timber.tag(TAG).i("No master key given by verifying device") + } ?: Timber.tag(TAG).w("No local master key so not verifying") + } ?: 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) - // request secrets from the verifying device - Timber.tag(TAG).i("Requesting secrets from $verifyingDeviceId") + Timber.tag(TAG).i("No master key given by verifying device") + } - 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) + // 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 - return adapter.fromJson(data.toString(Charsets.UTF_8)) + 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 cancel(reason: RendezvousFailureReason) { - channel.cancel(reason) - } - 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 index 31014e392d..be79569164 100644 --- 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 @@ -16,8 +16,7 @@ package org.matrix.android.sdk.api.rendezvous -import org.matrix.android.sdk.api.rendezvous.model.ECDHRendezvousCode -import org.matrix.android.sdk.api.rendezvous.model.RendezvousIntent +import org.matrix.android.sdk.api.rendezvous.model.RendezvousError /** * Representation of a rendezvous channel such as that described by MSC3903. @@ -28,26 +27,25 @@ interface RendezvousChannel { /** * @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? /** - * @returns a representation of the channel that can be encoded in a QR or similar + * @returns closes the channel and cleans up */ suspend fun close() - - // In future we probably want this to be a more generic RendezvousCode but it is suffice for now - suspend fun generateCode(intent: RendezvousIntent): ECDHRendezvousCode - suspend fun cancel(reason: RendezvousFailureReason) } 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 index a607dc7f38..18e625d825 100644 --- 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 @@ -16,16 +16,17 @@ package org.matrix.android.sdk.api.rendezvous -enum class RendezvousFailureReason(val value: String, val canRetry: Boolean = true) { - UserDeclined("user_declined"), - OtherDeviceNotSignedIn("other_device_not_signed_in"), - OtherDeviceAlreadySignedIn("other_device_already_signed_in"), - Unknown("unknown"), - Expired("expired"), - UserCancelled("user_cancelled"), - InvalidCode("invalid_code"), - UnsupportedAlgorithm("unsupported_algorithm", false), - DataMismatch("data_mismatch"), - UnsupportedTransport("unsupported_transport", false), - HomeserverLacksSupport("homeserver_lacks_support", false) +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 index de0aed7efc..5daf906930 100644 --- 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 @@ -17,13 +17,16 @@ 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 - var onCancelled: ((reason: RendezvousFailureReason) -> Unit)? + @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 cancel(reason: RendezvousFailureReason) + 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 index ca7083b297..9a5c5e865a 100644 --- 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 @@ -89,13 +89,14 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu ourPublicKey = Base64.decode(olmSAS!!.publicKey, Base64.NO_WRAP) } + @Throws(RendezvousError::class) override suspend fun connect(): String { olmSAS ?.let { olmSAS -> val isInitiator = theirPublicKey == null if (isInitiator) { Timber.tag(TAG).i("Waiting for other device to send their public key") - val res = this.receiveAsPayload() ?: throw RuntimeException("No reply from other device") + val res = this.receiveAsPayload() ?: throw RendezvousError("No reply from other device", RendezvousFailureReason.ProtocolError) if (res.key == null) { throw RendezvousError( @@ -137,7 +138,7 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu override suspend fun send(data: ByteArray) { if (aesKey == null) { - throw RuntimeException("Shared secret not established") + throw IllegalStateException("Shared secret not established") } send(encrypt(data)) } @@ -150,31 +151,12 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu override suspend fun receive(): ByteArray? { if (aesKey == null) { - throw RuntimeException("Shared secret not established") + throw IllegalStateException("Shared secret not established") } val payload = receiveAsPayload() ?: return null return decrypt(payload) } - override suspend fun generateCode(intent: RendezvousIntent): ECDHRendezvousCode { - return ECDHRendezvousCode( - intent, - rendezvous = ECDHRendezvous( - transport.details() as SimpleHttpRendezvousTransportDetails, - SecureRendezvousChannelAlgorithm.ECDH_V1, - key = Base64.encodeToString(ourPublicKey, Base64.NO_WRAP) - ) - ) - } - - override suspend fun cancel(reason: RendezvousFailureReason) { - try { - transport.cancel(reason) - } finally { - close() - } - } - override suspend fun close() { olmSAS ?.let { synchronized(it) { @@ -183,6 +165,7 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu olmSAS = null } } + transport.close() } private fun encrypt(plainText: ByteArray): ECDHPayload { 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 index 2dd6e7be28..2ef24e9cb7 100644 --- 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 @@ -26,5 +26,11 @@ enum class Outcome(val value: String) { DECLINED("declined"), @Json(name = "unsupported") - UNSUPPORTED("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/RendezvousError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/model/RendezvousError.kt index fec55ffb67..c52b11a322 100644 --- 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 @@ -18,4 +18,4 @@ package org.matrix.android.sdk.api.rendezvous.model import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason -class RendezvousError(val description: String, val reason: RendezvousFailureReason) : RuntimeException(description) +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/transports/SimpleHttpRendezvousTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/transports/SimpleHttpRendezvousTransport.kt index ca2a3425cd..fa0e5d8e2a 100644 --- 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 @@ -23,6 +23,7 @@ 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 @@ -32,7 +33,7 @@ import java.util.Date /** * Implementation of the Simple HTTP transport MSC3886: https://github.com/matrix-org/matrix-spec-proposals/pull/3886 */ -class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: RendezvousFailureReason) -> Unit)?, rendezvousUri: String?) : RendezvousTransport { +class SimpleHttpRendezvousTransport(rendezvousUri: String?) : RendezvousTransport { companion object { private val TAG = LoggerTag(SimpleHttpRendezvousTransport::class.java.simpleName, LoggerTag.RENDEZVOUS).value } @@ -55,7 +56,7 @@ class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: Rendezvo override suspend fun send(contentType: MediaType, data: ByteArray) { if (cancelled) { - return + throw IllegalStateException("Rendezvous cancelled") } val method = if (uri != null) "PUT" else "POST" @@ -75,9 +76,7 @@ class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: Rendezvo val response = httpClient.newCall(request.build()).execute() if (response.code == 404) { - // we set to unknown and the cancel method will rewrite the reason to expired if applicable - cancel(RendezvousFailureReason.Unknown) - return + throw get404Error() } etag = response.header("etag") @@ -98,12 +97,12 @@ class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: Rendezvo } 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) { - if (cancelled) { - return null - } Timber.tag(TAG).i("Polling: $uri after etag $etag") val request = Request.Builder() .url(uri) @@ -118,9 +117,7 @@ class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: Rendezvo try { // expired if (response.code == 404) { - // we set to unknown and the cancel method will rewrite the reason to expired if applicable - cancel(RendezvousFailureReason.Unknown) - return null + throw get404Error() } // rely on server expiring the channel rather than checking ourselves @@ -145,31 +142,27 @@ class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: Rendezvo } } - override suspend fun cancel(reason: RendezvousFailureReason) { - var mappedReason = reason - Timber.tag(TAG).i("$expiresAt") - if (mappedReason == RendezvousFailureReason.Unknown && - expiresAt != null && Date() > expiresAt - ) { - mappedReason = RendezvousFailureReason.Expired - } + private fun get404Error(): RendezvousError { + return if (expiresAt != null && Date() > expiresAt) + RendezvousError("Expired", RendezvousFailureReason.Expired) + else + RendezvousError("Received unexpected 404", RendezvousFailureReason.Unknown) + } + override suspend fun close() { cancelled = true ready = false - onCancelled ?.let { it(mappedReason) } - if (mappedReason == RendezvousFailureReason.UserDeclined) { - uri ?.let { - try { - val httpClient = okhttp3.OkHttpClient.Builder().build() - val request = Request.Builder() - .url(it) - .delete() - .build() - httpClient.newCall(request).execute() - } catch (e: Exception) { - Timber.tag(TAG).w(e, "Failed to delete channel") - } + 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/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 0691e2367e..617d620c27 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 @@ -70,8 +70,14 @@ class QrCodeLoginStatusFragment : VectorBaseFragment 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/QrCodeLoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt index 9d4e5e9870..7f95fad485 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 @@ -30,6 +30,7 @@ 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( @@ -38,14 +39,15 @@ class QrCodeLoginViewModel @AssistedInject constructor( private val activeSessionHolder: ActiveSessionHolder, private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase ) : VectorViewModel(initialState) { - val TAG: String = QrCodeLoginViewModel::class.java.simpleName @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { 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) { @@ -71,9 +73,14 @@ class QrCodeLoginViewModel @AssistedInject constructor( private fun handleOnQrCodeScanned(action: QrCodeLoginAction.OnQrCodeScanned) { Timber.tag(TAG).d("Scanned code: ${action.qrCode}") - val rendezvous = Rendezvous.buildChannelFromCode(action.qrCode) { reason -> - Timber.tag(TAG).d("Rendezvous cancelled: $reason") - onFailed(reason) + 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 { @@ -103,9 +110,13 @@ class QrCodeLoginViewModel @AssistedInject constructor( _viewEvents.post(QrCodeLoginViewEvents.NavigateToHomeScreen) } } - } catch (failure: Throwable) { - Timber.tag(TAG).e(failure, "Error occurred during sign in") - onFailed(RendezvousFailureReason.Unknown) + } catch (t: Throwable) { + Timber.tag(TAG).e(t, "Error occurred during sign in") + if (t is RendezvousError) { + onFailed(t.reason) + } else { + onFailed(RendezvousFailureReason.Unknown) + } } } }