Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QR Code Login UI #7338

Merged
merged 40 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
06c0d61
Create base classes.
Oct 3, 2022
6fbdd87
Create custom view for header section.
Oct 4, 2022
4fdb4e8
Create custom view for instructions section.
Oct 5, 2022
9859dab
Complete qr code login instructions screen.
Oct 5, 2022
5f6c8ee
Navigate to the instructions screen.
Oct 6, 2022
5dfaa25
Remove unused session parameter.
Oct 6, 2022
9b7f6c9
Navigate to qr code scanner activity.
Oct 6, 2022
945fa0a
Create qr code login status view layout.
Oct 6, 2022
a66b183
Add connection status to the view state.
Oct 6, 2022
a00afa7
Simulate qr login states.
Oct 6, 2022
1932eda
Fix instructions view visibility.
Oct 6, 2022
04fb316
Implement show qr code screen.
Oct 7, 2022
2527cab
Fix cancel actions.
Oct 7, 2022
2b452d6
Implement qr code login failed states.
Oct 7, 2022
236b303
Fix ui test case.
Oct 7, 2022
ad208a0
Refactor layout.
Oct 10, 2022
aacf2ba
Refactor layout.
Oct 11, 2022
5566300
Add qr code options to layout.
Oct 11, 2022
f272e56
Implement link a device flow.
Oct 11, 2022
d8ea9c8
Add flag for qr code login.
Oct 11, 2022
87956e9
Retry scanning if not a QR code
hughns Oct 11, 2022
1235db7
Implementations of MSC3886 and MSC3903
hughns Oct 11, 2022
4b14ee4
Partial implementation of QR login logic
hughns Oct 11, 2022
1e1affb
Merge branch 'develop' into feature/ons/qr_code_login_ui
Oct 12, 2022
6e58f2f
Only do completeOnNewDevice if we received a confirmation code
hughns Oct 12, 2022
fb2776d
Cherry pick previous commits.
Oct 13, 2022
e554b43
Merge branch 'feature/ons/qr_code_login_ui' of https://github.com/vec…
hughns Oct 13, 2022
90fa5d5
Revert "Only do completeOnNewDevice if we received a confirmation code"
hughns Oct 13, 2022
e305478
Revert "Partial implementation of QR login logic"
hughns Oct 13, 2022
489dfd7
Revert "Implementations of MSC3886 and MSC3903"
hughns Oct 13, 2022
9429a4f
Revert "Retry scanning if not a QR code"
hughns Oct 13, 2022
343cf74
Add flag to allow QR login on all servers + split flag for showing in…
hughns Oct 14, 2022
4c7c861
Fix logic for showing confirm button
hughns Oct 14, 2022
5953346
Merge branch 'develop' into feature/ons/qr_code_login_ui
Oct 14, 2022
b04ad49
Add changelog.
Oct 14, 2022
e83bdc3
Use correct homeserver url to check qr code login support.
Oct 14, 2022
6c10a9b
Code review fixes.
Oct 14, 2022
8547fee
Enable qr code login by default.
Oct 17, 2022
91bb86d
Code review fixes.
Oct 17, 2022
d3a24fe
Lint fix.
Oct 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2178,6 +2178,7 @@
<string name="login_signin_matrix_id_password_notice">If you don’t know your password, go back to reset it.</string>
<string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string>
<string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string>
<string name="login_scan_qr_code">Scan QR code</string>

<string name="seen_by">Seen by</string>

Expand Down Expand Up @@ -3317,6 +3318,9 @@
<string name="device_manager_session_rename_edit_hint">Session name</string>
<string name="device_manager_session_rename_description">Custom session names can help you recognize your devices more easily.</string>
<string name="device_manager_session_rename_warning">Please be aware that session names are also visible to people you communicate with.</string>
<string name="device_manager_sessions_sign_in_with_qr_code_title">SIGN IN WITH QR CODE</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess strings should be lower case. The upper case style should be done using text appearance instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Actually it is more consistent with lowercase.

<string name="device_manager_sessions_sign_in_with_qr_code_description">You can use this device to sign in a mobile or web device with a QR code. There are two ways to do this:</string>

<string name="device_manager_learn_more_sessions_inactive_title">Inactive sessions</string>
<string name="device_manager_learn_more_sessions_inactive">Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.</string>
<string name="device_manager_learn_more_sessions_unverified_title">Unverified sessions</string>
Expand Down Expand Up @@ -3347,4 +3351,39 @@
<string name="onboarding_new_app_layout_feedback_message">Tap top right to see the option to feedback.</string>
<string name="onboarding_new_app_layout_button_try">Try it out</string>

<string name="one">1</string>
<string name="two">2</string>
<string name="three">3</string>
<string name="four">4</string>

<!-- QR Code Login -->
<string name="qr_code_login_header_scan_qr_code_title">Scan QR code</string>
<string name="qr_code_login_header_scan_qr_code_description">Use the camera on this device to scan the QR code shown on your other device:</string>
<string name="qr_code_login_header_show_qr_code_title">Sign in with QR code</string>
<string name="qr_code_login_header_show_qr_code_new_device_description">Use your signed in device to scan the QR code below:</string>
<string name="qr_code_login_header_show_qr_code_link_a_device_description">Scan the QR code below with your device that’s signed out.</string>
<string name="qr_code_login_header_connected_title">Secure connection established</string>
<string name="qr_code_login_header_connected_description">Check your signed in device, the code below should be displayed. Confirm that the code below matches with that device:</string>
<string name="qr_code_login_header_failed_title">Unsuccessful connection</string>
<string name="qr_code_login_header_failed_device_is_not_supported_description">Linking with this device is not supported.</string>
<string name="qr_code_login_header_failed_timeout_description">The linking wasn’t completed in the required time.</string>
<string name="qr_code_login_header_failed_denied_description">The request was denied on the other device.</string>
<string name="qr_code_login_header_failed_other_description">The request failed.</string>
<string name="qr_code_login_new_device_instruction_1">Open ${app_name} on your other device</string>
<string name="qr_code_login_new_device_instruction_2">Go to Settings -> Security &amp; Privacy -> Show All Sessions</string>
<string name="qr_code_login_new_device_instruction_3">Select \'Show QR code\'</string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to avoid translation mistake, change to

Suggested change
<string name="qr_code_login_new_device_instruction_3">Select \'Show QR code\'</string>
<string name="qr_code_login_new_device_instruction_3">Select \'%s\'</string>

and inject at runtime the wording of the button.
Which is the key qr_code_login_show_qr_code_button IIUC which contains the text... Show QR code in this device.

<string name="qr_code_login_link_a_device_scan_qr_code_instruction_1">Start at the sign in screen</string>
<string name="qr_code_login_link_a_device_scan_qr_code_instruction_2">Select \'Sign in with QR code\'</string>
<string name="qr_code_login_link_a_device_show_qr_code_instruction_1">Start at the sign in screen</string>
<string name="qr_code_login_link_a_device_show_qr_code_instruction_2">Select \'Scan QR code\'</string>
<string name="qr_code_login_show_qr_code_button">Show QR code in this device</string>
<string name="qr_code_login_signing_in_a_mobile_device">Signing in a mobile device?</string>
<string name="qr_code_login_scan_qr_code_button">Scan QR code</string>
<string name="qr_code_login_connecting_to_device">Connecting to device</string>
<string name="qr_code_login_signing_in">Signing you in</string>
<string name="qr_code_login_status_no_match">No match?</string>
<string name="qr_code_login_try_again">Try again</string>
<string name="qr_code_login_confirm_security_code">Confirm</string>
<string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>

</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="QrCodeLoginInstructionsView">
<attr name="qrCodeLoginInstruction1" format="string" />
<attr name="qrCodeLoginInstruction2" format="string" />
<attr name="qrCodeLoginInstruction3" format="string" />
<attr name="qrCodeLoginInstruction4" format="string" />
</declare-styleable>

</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="QrCodeLoginHeaderView">
<attr name="qrCodeLoginHeaderTitle" format="string" />
<attr name="qrCodeLoginHeaderDescription" format="string" />
<attr name="qrCodeLoginHeaderImageResource" format="reference" />
<attr name="qrCodeLoginHeaderImageBackgroundTint" format="color" />
</declare-styleable>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.matrix.android.sdk.internal.rendezvous

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.logger.LoggerTag
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 org.matrix.android.sdk.internal.rendezvous.channels.ECDHRendezvousChannel
import org.matrix.android.sdk.internal.rendezvous.model.ECDHRendezvousCode
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousIntent
import org.matrix.android.sdk.internal.rendezvous.transports.SimpleHttpRendezvousTransport
import timber.log.Timber

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")
}

@JsonClass(generateAdapter = true)
internal data class Payload(
@Json val type: PayloadType,
@Json val intent: RendezvousIntent? = null,
@Json val outcome: String? = null,
@Json val protocols: List<String>? = null,
@Json val protocol: String? = null,
@Json val homeserver: String? = null,
@Json val login_token: String? = null,
@Json val device_id: String? = null,
@Json val device_key: String? = null,
@Json val verifying_device_id: String? = null,
@Json val verifying_device_key: String? = null,
@Json val master_key: String? = null
)

private val TAG = LoggerTag(Rendezvous::class.java.simpleName, LoggerTag.RENDEZVOUS).value

data class Rendezvous(
val channel: RendezvousChannel,
val theirIntent: RendezvousIntent
) {
companion object {
fun buildChannelFromCode(code: String, onCancelled: (reason: RendezvousFailureReason) -> Unit): Rendezvous {
val parsed = MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code) ?: throw RuntimeException("Invalid code")

val transport = SimpleHttpRendezvousTransport(onCancelled, 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

private suspend fun areIntentsIncompatible(): Boolean {
val incompatible = theirIntent == ourIntent

Timber.tag(TAG).d("ourIntent: $ourIntent, theirIntent: $theirIntent, incompatible: $incompatible")

if (incompatible) {
send(Payload(PayloadType.Finish, intent = ourIntent))
val reason = if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) RendezvousFailureReason.OtherDeviceNotSignedIn else RendezvousFailureReason.OtherDeviceAlreadySignedIn
channel.cancel(reason)
}

return incompatible
}

suspend fun startAfterScanningCode(): String? {
val checksum = channel.connect();

Timber.tag(TAG).i("Connected to secure channel with checksum: $checksum")

if (areIntentsIncompatible()) {
return null
}

// get protocols
Timber.tag(TAG).i("Waiting for protocols");
val protocolsResponse = receive()

if (protocolsResponse?.protocols == null || !protocolsResponse.protocols.contains("login_token")) {
send(Payload(PayloadType.Finish, outcome = "unsupported"))
Timber.tag(TAG).i("No supported protocol")
cancel(RendezvousFailureReason.Unknown)
return null
}

send(Payload(PayloadType.Progress, protocol = "login_token"))

return checksum
}

suspend fun completeOnNewDevice(): Session? {
Timber.tag(TAG).i("Waiting for login_token");

val loginToken = receive()

if (loginToken?.type == PayloadType.Finish) {
when (loginToken.outcome) {
"declined" -> {
Timber.tag(TAG).i("Login declined by other device")
channel.cancel(RendezvousFailureReason.UserDeclined)
return null
}
"unsupported" -> {
Timber.tag(TAG).i("Not supported")
channel.cancel(RendezvousFailureReason.HomeserverLacksSupport)
return null
}
}
channel.cancel(RendezvousFailureReason.Unknown)
return null
}

val homeserver = loginToken?.homeserver ?: throw RuntimeException("No homeserver returned")
val login_token = loginToken.login_token ?: throw RuntimeException("No login token returned")

Timber.tag(TAG).i("Got login_token: $login_token for $homeserver");

// TODO: set view to be state logging in?

// use token to login
// const login = await sendLoginRequest(homeserver, undefined, "m.login.token", { token: login_token });
//
// await setLoggedIn(login);
//
// const { deviceId, userId } = login;
//
// const client = MatrixClientPeg.get();
//

val newSession: Session? = null

newSession ?.let {
session ->
val userId = session.myUserId
val crypto = session.cryptoService()
val deviceId = crypto.getMyDevice().deviceId
val deviceKey = crypto.getMyDevice().fingerprint()
send(Payload(PayloadType.Progress, outcome = "success", device_id = deviceId, device_key = deviceKey))

// await confirmation of verification

val verificationResponse = receive()
val verifyingDeviceId = verificationResponse?.verifying_device_id ?: throw RuntimeException("No verifying device id returned")
val verifyingDeviceFromServer = crypto.getCryptoDeviceInfo(userId, verifyingDeviceId)
if (verifyingDeviceFromServer?.fingerprint() == verificationResponse.verifying_device_key) {
// set other device as verified
Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified");
crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)

verificationResponse.master_key ?.let {
// set master key as trusted
crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, it)

}

// request secrets from the verifying device
Timber.tag(TAG).i("Requesting secrets from $verifyingDeviceId")

session.sharedSecretStorageService() .let {
it.requestSecret(verifyingDeviceId, MASTER_KEY_SSSS_NAME)
it.requestSecret(verifyingDeviceId, SELF_SIGNING_KEY_SSSS_NAME)
it.requestSecret(verifyingDeviceId, USER_SIGNING_KEY_SSSS_NAME)
it.requestSecret(verifyingDeviceId, KEYBACKUP_SECRET_SSSS_NAME)
}
} else {
Timber.tag(TAG).i("Verifying device $verifyingDeviceId doesn't match: $verifyingDeviceFromServer")
}
}

return newSession
}

private suspend fun receive(): Payload? {
val data = channel.receive()?: return null
return adapter.fromJson(data.toString(Charsets.UTF_8))
}

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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.matrix.android.sdk.internal.rendezvous

import org.matrix.android.sdk.internal.rendezvous.model.ECDHRendezvousCode
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousIntent

interface RendezvousChannel {
var transport: RendezvousTransport;
/**
* @returns the checksum/confirmation digits to be shown to the user
*/
suspend fun connect(): String
/**
* Send a payload via the channel.
* @param data payload to send
*/
suspend fun send(data: ByteArray)
/**
* Receive a payload from the channel.
* @returns the received payload
*/
suspend fun receive(): ByteArray?
/**
* @returns a representation of the channel that can be encoded in a QR or similar
*/
suspend fun close()
// TODO: this should be transport independent in the future
suspend fun generateCode(intent: RendezvousIntent): ECDHRendezvousCode
suspend fun cancel(reason: RendezvousFailureReason)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.matrix.android.sdk.internal.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)
}
Loading