You've already forked element-android
mirror of
https://github.com/vector-im/element-android.git
synced 2025-07-31 07:04:23 +03:00
Remove login with QR code feature.
This commit is contained in:
@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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.amshove.kluent.invoking
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeInstanceOf
|
||||
import org.amshove.kluent.shouldThrow
|
||||
import org.amshove.kluent.with
|
||||
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.common.CommonTestHelper
|
||||
|
||||
class RendezvousTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun shouldSuccessfullyBuildChannels() = CommonTestHelper.runCryptoTest(context()) { _, _ ->
|
||||
val cases = listOf(
|
||||
// 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:
|
||||
"{\"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\"}",
|
||||
)
|
||||
|
||||
cases.forEach { input ->
|
||||
Rendezvous.buildChannelFromCode(input).channel shouldBeInstanceOf ECDHRendezvousChannel::class
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldFailToBuildChannelAsUnsupportedAlgorithm() {
|
||||
invoking {
|
||||
Rendezvous.buildChannelFromCode(
|
||||
"{\"rendezvous\":{\"algorithm\":\"bad algo\"," +
|
||||
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
|
||||
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
|
||||
"\"intent\":\"login.reciprocate\"}"
|
||||
)
|
||||
} shouldThrow RendezvousError::class with {
|
||||
this.reason shouldBeEqualTo RendezvousFailureReason.UnsupportedAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldFailToBuildChannelAsUnsupportedTransport() {
|
||||
invoking {
|
||||
Rendezvous.buildChannelFromCode(
|
||||
"{\"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\"}}," +
|
||||
"\"intent\":\"login.reciprocate\"}"
|
||||
)
|
||||
} shouldThrow RendezvousError::class with {
|
||||
this.reason shouldBeEqualTo RendezvousFailureReason.UnsupportedTransport
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldFailToBuildChannelWithInvalidIntent() {
|
||||
invoking {
|
||||
Rendezvous.buildChannelFromCode(
|
||||
"{\"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\":\"foo\"}"
|
||||
)
|
||||
} shouldThrow RendezvousError::class with {
|
||||
this.reason shouldBeEqualTo RendezvousFailureReason.InvalidCode
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldFailToBuildChannelAsInvalidCode() {
|
||||
val cases = listOf(
|
||||
"{}",
|
||||
"rubbish",
|
||||
""
|
||||
)
|
||||
|
||||
cases.forEach { input ->
|
||||
invoking {
|
||||
Rendezvous.buildChannelFromCode(input)
|
||||
} shouldThrow RendezvousError::class with {
|
||||
this.reason shouldBeEqualTo RendezvousFailureReason.InvalidCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
/*
|
||||
* 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.RendezvousCode
|
||||
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.model.RendezvousTransportType
|
||||
import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm
|
||||
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.util.MatrixJsonParser
|
||||
import timber.log.Timber
|
||||
|
||||
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
|
||||
// However, we want to keep this implementation around for some time.
|
||||
// TODO define an end-of-life date for this implementation.
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// we first check that the code is valid JSON and has right high-level structure
|
||||
val genericParsed = try {
|
||||
// we rely on moshi validating the code and throwing exception if invalid JSON or algorithm doesn't match
|
||||
MatrixJsonParser.getMoshi().adapter(RendezvousCode::class.java).fromJson(code)
|
||||
} catch (a: Throwable) {
|
||||
throw RendezvousError("Malformed code", RendezvousFailureReason.InvalidCode)
|
||||
} ?: throw RendezvousError("Code is null", RendezvousFailureReason.InvalidCode)
|
||||
|
||||
// then we check that algorithm is supported
|
||||
if (!SecureRendezvousChannelAlgorithm.values().map { it.value }.contains(genericParsed.rendezvous.algorithm)) {
|
||||
throw RendezvousError("Unsupported algorithm", RendezvousFailureReason.UnsupportedAlgorithm)
|
||||
}
|
||||
|
||||
// and, that the transport is supported
|
||||
if (!RendezvousTransportType.values().map { it.value }.contains(genericParsed.rendezvous.transport.type)) {
|
||||
throw RendezvousError("Unsupported transport", RendezvousFailureReason.UnsupportedTransport)
|
||||
}
|
||||
|
||||
// now that we know the overall structure looks sensible, we rely on moshi validating the code and
|
||||
// throwing exception if other parts are invalid
|
||||
val supportedParsed = try {
|
||||
MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code)
|
||||
} catch (a: Throwable) {
|
||||
throw RendezvousError("Malformed ECDH rendezvous code", RendezvousFailureReason.InvalidCode)
|
||||
} ?: throw RendezvousError("ECDH rendezvous code is null", RendezvousFailureReason.InvalidCode)
|
||||
|
||||
val transport = SimpleHttpRendezvousTransport(supportedParsed.rendezvous.transport.uri)
|
||||
|
||||
return Rendezvous(
|
||||
ECDHRendezvousChannel(transport, supportedParsed.rendezvous.algorithm, supportedParsed.rendezvous.key),
|
||||
supportedParsed.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.getMyCryptoDevice().deviceId
|
||||
val deviceKey = crypto.getMyCryptoDevice().fingerprint()
|
||||
send(Payload(PayloadType.PROGRESS, outcome = Outcome.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)
|
||||
} catch (e: Throwable) {
|
||||
// log as warning and continue as initial sync might still complete
|
||||
Timber.tag(TAG).w(e, "Failed to download keys for self")
|
||||
}
|
||||
|
||||
// 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 other sessions.
|
||||
Timber.tag(TAG).i("Requesting secrets from other sessions")
|
||||
|
||||
session.sharedSecretStorageService().requestMissingSecrets()
|
||||
} 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()
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
/*
|
||||
* 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.getDecimalCodeRepresentation
|
||||
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,
|
||||
private val algorithm: SecureRendezvousChannelAlgorithm,
|
||||
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 = decodeBase64(it)
|
||||
}
|
||||
olmSAS = OlmSAS()
|
||||
ourPublicKey = decodeBase64(olmSAS!!.publicKey)
|
||||
}
|
||||
|
||||
fun encodeBase64(input: ByteArray?): String? {
|
||||
if (algorithm == SecureRendezvousChannelAlgorithm.ECDH_V2) {
|
||||
return Base64.encodeToString(input, Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
}
|
||||
return Base64.encodeToString(input, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun decodeBase64(input: String?): ByteArray {
|
||||
// for decoding we aren't concerned about padding
|
||||
return Base64.decode(input, 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 = decodeBase64(res.key)
|
||||
} else {
|
||||
// send our public key unencrypted
|
||||
Timber.tag(TAG).i("Sending public key")
|
||||
send(
|
||||
ECDHPayload(
|
||||
algorithm = algorithm,
|
||||
key = encodeBase64(ourPublicKey)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
olmSASMutex.withLock {
|
||||
sas.setTheirPublicKey(encodeBase64(theirPublicKey))
|
||||
sas.setTheirPublicKey(encodeBase64(theirPublicKey))
|
||||
|
||||
val initiatorKey = encodeBase64(if (isInitiator) ourPublicKey else theirPublicKey)
|
||||
val recipientKey = encodeBase64(if (isInitiator) theirPublicKey else ourPublicKey)
|
||||
val aesInfo = "${algorithm.value}|$initiatorKey|$recipientKey"
|
||||
|
||||
aesKey = sas.generateShortCode(aesInfo, 32)
|
||||
|
||||
val rawChecksum = sas.generateShortCode(aesInfo, 5)
|
||||
return rawChecksum.getDecimalCodeRepresentation(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<Byte>()
|
||||
|
||||
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 = encodeBase64(cipherText.toByteArray()),
|
||||
iv = encodeBase64(iv)
|
||||
)
|
||||
}
|
||||
|
||||
private fun decrypt(payload: ECDHPayload): ByteArray {
|
||||
val iv = decodeBase64(payload.iv)
|
||||
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<Byte>()
|
||||
plainText.addAll(encryptCipher.update(decodeBase64(payload.ciphertext)).toList())
|
||||
plainText.addAll(encryptCipher.doFinal().toList())
|
||||
|
||||
return plainText.toByteArray()
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
)
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
)
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* 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<Protocol>? = 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
|
||||
)
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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)
|
||||
open class Rendezvous(
|
||||
val transport: RendezvousTransportDetails,
|
||||
val algorithm: String,
|
||||
)
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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)
|
||||
open class RendezvousCode(
|
||||
open val intent: RendezvousIntent,
|
||||
open val rendezvous: Rendezvous
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* 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)
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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)
|
||||
open class RendezvousTransportDetails(
|
||||
val type: String
|
||||
)
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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"),
|
||||
@Json(name = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256")
|
||||
ECDH_V2("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256")
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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.name)
|
@ -1,173 +0,0 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user