-
Notifications
You must be signed in to change notification settings - Fork 731
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7369 from vector-im/feature/hughns/qr_code_login
Implement logic for sign in with QR
- Loading branch information
Showing
29 changed files
with
1,202 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add logic for sign in with QR code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
229 changes: 229 additions & 0 deletions
229
matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/Rendezvous.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousChannel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
32 changes: 32 additions & 0 deletions
32
...dk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousFailureReason.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
36 changes: 36 additions & 0 deletions
36
...ix-sdk-android/src/main/java/org/matrix/android/sdk/api/rendezvous/RendezvousTransport.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
Oops, something went wrong.