Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/8212.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Updates to protocol used for Sign in with QR code
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,53 @@ import org.junit.Test
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.rendezvous.channels.ECDHRendezvousChannel
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.api.rendezvous.model.RendezvousFlow
import org.matrix.android.sdk.common.CommonTestHelper

class RendezvousTest : InstrumentedTest {

@Test
fun shouldSuccessfullyBuildChannels() = CommonTestHelper.runCryptoTest(context()) { _, _ ->
fun shouldSuccessfullyBuildMSC3906V1Channels() = CommonTestHelper.runCryptoTest(context()) { _, _ ->
val cases = listOf(
// v1:
// MSC3903 v1 + MSC3906 v1:
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"login.reciprocate\"}",
// v2:
// MSC3903 v2 + MSC3906 v1:
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"login.reciprocate\"}",
"\"intent\":\"login.reciprocate\"}"
)

cases.forEach { input ->
val rz = Rendezvous.buildChannelFromCode(input)
rz.channel shouldBeInstanceOf ECDHRendezvousChannel::class
rz.flow shouldBeEqualTo RendezvousFlow.SETUP_ADDITIONAL_DEVICE_V1
}
}

fun shouldSuccessfullyBuildMSC3906V2Channels() = CommonTestHelper.runCryptoTest(context()) { _, _ ->
val cases = listOf(
// MSC3903 v1:
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"flow\":\"org.matrix.msc3906.setup.additional_device.v2\"," +
"\"intent\":\"login.reciprocate\"}",
// MSC3903 v2:
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"flow\":\"org.matrix.msc3906.setup.additional_device.v2\"," +
"\"intent\":\"login.reciprocate\"}"
)

cases.forEach { input ->
Rendezvous.buildChannelFromCode(input).channel shouldBeInstanceOf ECDHRendezvousChannel::class
val rz = Rendezvous.buildChannelFromCode(input)
rz.channel shouldBeInstanceOf ECDHRendezvousChannel::class
rz.flow shouldBeEqualTo RendezvousFlow.SETUP_ADDITIONAL_DEVICE_V2
}
}

Expand All @@ -56,6 +82,7 @@ class RendezvousTest : InstrumentedTest {
"{\"rendezvous\":{\"algorithm\":\"bad algo\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"flow\":\"org.matrix.msc3906.setup.additional_device.v2\"," +
"\"intent\":\"login.reciprocate\"}"
)
} shouldThrow RendezvousError::class with {
Expand All @@ -70,6 +97,7 @@ class RendezvousTest : InstrumentedTest {
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"bad transport\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"flow\":\"org.matrix.msc3906.setup.additional_device.v2\"," +
"\"intent\":\"login.reciprocate\"}"
)
} shouldThrow RendezvousError::class with {
Expand All @@ -84,6 +112,7 @@ class RendezvousTest : InstrumentedTest {
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"flow\":\"org.matrix.msc3906.setup.additional_device.v2\"," +
"\"intent\":\"foo\"}"
)
} shouldThrow RendezvousError::class with {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ 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.FailureReason
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.RendezvousCode
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.api.rendezvous.model.RendezvousFlow
import org.matrix.android.sdk.api.rendezvous.model.RendezvousIntent
import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportType
import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm
Expand All @@ -42,11 +44,14 @@ 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.
* Implementation of MSC3906 to sign in + E2EE set up using a QR code: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
*
* @alpha This is an experimental API, and is subject to change until MSC3906 is stabilised and accepted.
*/
class Rendezvous(
val channel: RendezvousChannel,
val theirIntent: RendezvousIntent,
val flow: RendezvousFlow,
) {
companion object {
private val TAG = LoggerTag(Rendezvous::class.java.simpleName, LoggerTag.RENDEZVOUS).value
Expand All @@ -61,6 +66,11 @@ class Rendezvous(
throw RendezvousError("Malformed code", RendezvousFailureReason.InvalidCode)
} ?: throw RendezvousError("Code is null", RendezvousFailureReason.InvalidCode)

// then we check that flow is supported
if (genericParsed.flow != null && !RendezvousFlow.values().map { it.value }.contains(genericParsed.flow)) {
throw RendezvousError("Unsupported flow", RendezvousFailureReason.UnsupportedAlgorithm)
}

// then we check that algorithm is supported
if (!SecureRendezvousChannelAlgorithm.values().map { it.value }.contains(genericParsed.rendezvous.algorithm)) {
throw RendezvousError("Unsupported algorithm", RendezvousFailureReason.UnsupportedAlgorithm)
Expand All @@ -83,25 +93,33 @@ class Rendezvous(

return Rendezvous(
ECDHRendezvousChannel(transport, supportedParsed.rendezvous.algorithm, supportedParsed.rendezvous.key),
supportedParsed.intent
supportedParsed.intent,
// default to v1 if not specified:
supportedParsed.flow ?: RendezvousFlow.SETUP_ADDITIONAL_DEVICE_V1
)
}
}

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
fun isUsingV1(): Boolean = flow == RendezvousFlow.SETUP_ADDITIONAL_DEVICE_V1

@Throws(RendezvousError::class)
private suspend fun checkCompatibility() {
// not yet implemented: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
val ourIntent: RendezvousIntent = RendezvousIntent.LOGIN_ON_NEW_DEVICE

val incompatible = theirIntent == ourIntent

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

if (incompatible) {
// inform the other side
send(Payload(PayloadType.FINISH, intent = ourIntent))
if (isUsingV1()) {
send(Payload(PayloadType.FINISH, intent = ourIntent))
} else {
send(Payload(PayloadType.FAILURE, intent = ourIntent))
}
if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) {
throw RendezvousError("The other device isn't signed in", RendezvousFailureReason.OtherDeviceNotSignedIn)
} else {
Expand All @@ -123,11 +141,19 @@ class Rendezvous(
val protocolsResponse = receive()

if (protocolsResponse?.protocols == null || !protocolsResponse.protocols.contains(Protocol.LOGIN_TOKEN)) {
send(Payload(PayloadType.FINISH, outcome = Outcome.UNSUPPORTED))
if (isUsingV1()) {
send(Payload(PayloadType.FINISH, outcome = Outcome.UNSUPPORTED))
} else {
send(Payload(PayloadType.FAILURE, reason = FailureReason.UNSUPPORTED))
}
throw RendezvousError("Unsupported protocols", RendezvousFailureReason.UnsupportedHomeserver)
}

send(Payload(PayloadType.PROGRESS, protocol = Protocol.LOGIN_TOKEN))
if (isUsingV1()) {
send(Payload(PayloadType.PROGRESS, protocol = Protocol.LOGIN_TOKEN))
} else {
send(Payload(PayloadType.PROTOCOL, protocol = Protocol.LOGIN_TOKEN))
}

return checksum
}
Expand All @@ -138,6 +164,7 @@ class Rendezvous(

val loginToken = receive()

// v1:
if (loginToken?.type == PayloadType.FINISH) {
when (loginToken.outcome) {
Outcome.DECLINED -> {
Expand All @@ -152,6 +179,28 @@ class Rendezvous(
}
}

// v2:
if (loginToken?.type == PayloadType.DECLINED) {
throw RendezvousError("Login declined by other device", RendezvousFailureReason.UserDeclined)
}
if (loginToken?.type == PayloadType.FAILURE) {
when (loginToken.reason) {
FailureReason.UNSUPPORTED -> {
throw RendezvousError("Homeserver lacks support", RendezvousFailureReason.UnsupportedHomeserver)
}
FailureReason.CANCELLED -> {
throw RendezvousError("Login cancelled by other device", RendezvousFailureReason.UserDeclined)
}
FailureReason.E2EE_SECURITY_ERROR -> {
throw RendezvousError("E2EE security error", RendezvousFailureReason.E2EESecurityIssue)
}
// incompatible intent shouldn't be received at this stage
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)

Expand All @@ -167,8 +216,11 @@ class Rendezvous(
val crypto = session.cryptoService()
val deviceId = crypto.getMyCryptoDevice().deviceId
val deviceKey = crypto.getMyCryptoDevice().fingerprint()
send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))

if (isUsingV1()) {
send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
} else {
send(Payload(PayloadType.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
}
try {
// explicitly download keys for ourself rather than racing with initial sync which might not complete in time
crypto.downloadKeysIfNeeded(listOf(userId), false)
Expand All @@ -179,60 +231,75 @@ class Rendezvous(

// await confirmation of verification
val verificationResponse = receive()
if (verificationResponse?.outcome == Outcome.VERIFIED) {
if (verificationResponse?.outcome == Outcome.VERIFIED || verificationResponse?.type == PayloadType.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
handleVerification(session, verifyingDeviceId, verificationResponse.verifyingDeviceKey, verificationResponse.masterKey)
} else {
Timber.tag(TAG).i("Not doing verification")
}
}

@Throws(RendezvousError::class)
private suspend fun handleVerification(session: Session, verifyingDeviceId: String, verifyingDeviceKey: String?, masterKey: String?) {
var crypto = session.cryptoService()
var userId = session.myUserId
val verifyingDeviceFromServer = crypto.getCryptoDeviceInfo(userId, verifyingDeviceId)
if (verifyingDeviceFromServer?.fingerprint() != verifyingDeviceKey) {
Timber.tag(TAG).w(
"Verifying device $verifyingDeviceId key doesn't match: ${
verifyingDeviceFromServer?.fingerprint()
} vs $verifyingDeviceKey)"
)
// inform the other side
if (isUsingV1()) {
send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
throw RendezvousError("Key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
} else {
send(Payload(PayloadType.FAILURE, reason = FailureReason.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
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()
// 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
// 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
if (isUsingV1()) {
send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
throw RendezvousError("Master key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
} else {
send(Payload(PayloadType.FAILURE, reason = FailureReason.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)
// 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("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")
}
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")
// 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")
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)
}
}

Expand Down
Loading