Implementations of MSC3886 and MSC3903
This commit is contained in:
parent
87956e9438
commit
1235db7895
|
@ -27,6 +27,7 @@ open class LoggerTag(name: String, parentTag: LoggerTag? = null) {
|
||||||
object SYNC : LoggerTag("SYNC")
|
object SYNC : LoggerTag("SYNC")
|
||||||
object VOIP : LoggerTag("VOIP")
|
object VOIP : LoggerTag("VOIP")
|
||||||
object CRYPTO : LoggerTag("CRYPTO")
|
object CRYPTO : LoggerTag("CRYPTO")
|
||||||
|
object RENDEZVOUS : LoggerTag("RZ")
|
||||||
|
|
||||||
val value: String = if (parentTag == null) {
|
val value: String = if (parentTag == null) {
|
||||||
name
|
name
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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 okhttp3.MediaType
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousTransportDetails
|
||||||
|
|
||||||
|
interface RendezvousTransport {
|
||||||
|
var ready: Boolean;
|
||||||
|
var onCancelled: ((reason: RendezvousFailureReason) -> Unit)?;
|
||||||
|
suspend fun details(): RendezvousTransportDetails;
|
||||||
|
suspend fun send(contentType: MediaType, data: ByteArray);
|
||||||
|
suspend fun receive(): ByteArray?;
|
||||||
|
suspend fun cancel(reason: RendezvousFailureReason);
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* 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.channels
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixJsonParser
|
||||||
|
import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.RendezvousFailureReason
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.RendezvousChannel
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.RendezvousTransport
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.ECDHRendezvous
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.ECDHRendezvousCode
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousError
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousIntent
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.SecureRendezvousChannelAlgorithm
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.transports.SimpleHttpRendezvousTransportDetails
|
||||||
|
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
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ECDHPayload(
|
||||||
|
@Json val algorithm: SecureRendezvousChannelAlgorithm? = null,
|
||||||
|
@Json val key: String? = null,
|
||||||
|
@Json val ciphertext: String? = null,
|
||||||
|
@Json val iv: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private val TAG = LoggerTag(ECDHRendezvousChannel::class.java.simpleName, LoggerTag.RENDEZVOUS).value
|
||||||
|
|
||||||
|
fun getDecimalCodeRepresentation(byteArray: ByteArray): String {
|
||||||
|
val b0 = byteArray[0].toUnsignedInt() // need unsigned byte
|
||||||
|
val b1 = byteArray[1].toUnsignedInt() // need unsigned byte
|
||||||
|
val b2 = byteArray[2].toUnsignedInt() // need unsigned byte
|
||||||
|
val b3 = byteArray[3].toUnsignedInt() // need unsigned byte
|
||||||
|
val b4 = byteArray[4].toUnsignedInt() // need unsigned byte
|
||||||
|
// (B0 << 5 | B1 >> 3) + 1000
|
||||||
|
val first = (b0.shl(5) or b1.shr(3)) + 1000
|
||||||
|
// ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000
|
||||||
|
val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000
|
||||||
|
// ((B3 & 0x3f) << 7 | B4 >> 1) + 1000
|
||||||
|
val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000
|
||||||
|
return "$first-$second-$third"
|
||||||
|
}
|
||||||
|
|
||||||
|
const val ALGORITHM_SPEC = "AES/GCM/NoPadding"
|
||||||
|
const val KEY_SPEC = "AES"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, theirPublicKeyBase64: String?): RendezvousChannel {
|
||||||
|
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 = Base64.decode(it, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
olmSAS = OlmSAS()
|
||||||
|
ourPublicKey = Base64.decode(olmSAS!!.publicKey, Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun connect(): String {
|
||||||
|
if (olmSAS == null) {
|
||||||
|
throw RuntimeException("Channel closed")
|
||||||
|
}
|
||||||
|
val isInitiator = theirPublicKey == null
|
||||||
|
|
||||||
|
if (isInitiator) {
|
||||||
|
Timber.tag(TAG).i("Waiting for other device to send their public key")
|
||||||
|
val res = this.receiveAsPayload() ?: throw RuntimeException("No reply from other device")
|
||||||
|
|
||||||
|
if (res.key == null) {
|
||||||
|
throw RendezvousError(
|
||||||
|
"Unsupported algorithm: ${res.algorithm}",
|
||||||
|
RendezvousFailureReason.UnsupportedAlgorithm,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
theirPublicKey = Base64.decode(res.key, Base64.NO_WRAP)
|
||||||
|
} else {
|
||||||
|
// send our public key unencrypted
|
||||||
|
Timber.tag(TAG).i("Sending public key")
|
||||||
|
send(ECDHPayload(
|
||||||
|
algorithm = SecureRendezvousChannelAlgorithm.ECDH_V1,
|
||||||
|
key = Base64.encodeToString(ourPublicKey, Base64.NO_WRAP)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
olmSAS!!.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
val initiatorKey = Base64.encodeToString(if (isInitiator) ourPublicKey else theirPublicKey, Base64.NO_WRAP)
|
||||||
|
val recipientKey = Base64.encodeToString(if (isInitiator) theirPublicKey else ourPublicKey, Base64.NO_WRAP)
|
||||||
|
val aesInfo = "${SecureRendezvousChannelAlgorithm.ECDH_V1.value}|$initiatorKey|$recipientKey"
|
||||||
|
|
||||||
|
aesKey = olmSAS!!.generateShortCode(aesInfo, 32)
|
||||||
|
|
||||||
|
Timber.tag(TAG).i("Our public key: ${Base64.encodeToString(ourPublicKey, Base64.NO_WRAP)}")
|
||||||
|
Timber.tag(TAG).i("Their public key: ${Base64.encodeToString(theirPublicKey, Base64.NO_WRAP)}")
|
||||||
|
Timber.tag(TAG).i("AES info: $aesInfo")
|
||||||
|
Timber.tag(TAG).i("AES key: ${Base64.encodeToString(aesKey, Base64.NO_WRAP)}")
|
||||||
|
|
||||||
|
val rawChecksum = olmSAS!!.generateShortCode(aesInfo, 5)
|
||||||
|
return getDecimalCodeRepresentation(rawChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 RuntimeException("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 RuntimeException("Shared secret not established")
|
||||||
|
}
|
||||||
|
val payload = receiveAsPayload() ?: return null
|
||||||
|
return decrypt(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun generateCode(intent: RendezvousIntent): ECDHRendezvousCode {
|
||||||
|
return ECDHRendezvousCode(
|
||||||
|
intent,
|
||||||
|
rendezvous = ECDHRendezvous(
|
||||||
|
transport.details() as SimpleHttpRendezvousTransportDetails,
|
||||||
|
SecureRendezvousChannelAlgorithm.ECDH_V1,
|
||||||
|
key = Base64.encodeToString(ourPublicKey, Base64.NO_WRAP)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun cancel(reason: RendezvousFailureReason) {
|
||||||
|
try {
|
||||||
|
transport.cancel(reason)
|
||||||
|
} finally {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun close() {
|
||||||
|
olmSAS?.releaseSas()
|
||||||
|
olmSAS = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encrypt(plainText: ByteArray): ECDHPayload {
|
||||||
|
Timber.tag(TAG).i("Encrypting: ${plainText.toString(Charsets.UTF_8)}")
|
||||||
|
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 = Base64.encodeToString(cipherText.toByteArray(), Base64.NO_WRAP),
|
||||||
|
iv = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decrypt(payload: ECDHPayload): ByteArray {
|
||||||
|
val iv = Base64.decode(payload.iv, Base64.NO_WRAP)
|
||||||
|
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(Base64.decode(payload.ciphertext, Base64.NO_WRAP)).toList())
|
||||||
|
plainText.addAll(encryptCipher.doFinal().toList())
|
||||||
|
|
||||||
|
val plainTextBytes = plainText.toByteArray()
|
||||||
|
|
||||||
|
Timber.tag(TAG).i("Decrypted: ${plainTextBytes.toString(Charsets.UTF_8)}")
|
||||||
|
return plainTextBytes
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.transports.SimpleHttpRendezvousTransportDetails
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ECDHRendezvous(
|
||||||
|
@Json val transport: SimpleHttpRendezvousTransportDetails,
|
||||||
|
@Json val algorithm: SecureRendezvousChannelAlgorithm,
|
||||||
|
@Json val key: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ECDHRendezvousCode(
|
||||||
|
@Json val intent: RendezvousIntent,
|
||||||
|
@Json val rendezvous: ECDHRendezvous
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
open class EmbeddedRendezvous(
|
||||||
|
@Json(name = "transport") val transport: RendezvousTransportDetails,
|
||||||
|
@Json(name = "algorithm") val algorithm: SecureRendezvousChannelAlgorithm
|
||||||
|
)
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.RendezvousFailureReason
|
||||||
|
|
||||||
|
class RendezvousError(val description: String, val reason: RendezvousFailureReason): RuntimeException(description) {
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
enum class RendezvousIntent {
|
||||||
|
@Json(name = "login.start") LOGIN_ON_NEW_DEVICE,
|
||||||
|
@Json(name = "login.reciprocate") RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
open class RendezvousTransportDetails(
|
||||||
|
@Json val type: RendezvousTransportType
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
enum class RendezvousTransportType(val value: String) {
|
||||||
|
@Json(name = "http.v1") MSC3886_SIMPLE_HTTP_V1("http.v1")
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
enum class SecureRendezvousChannelAlgorithm(val value: String) {
|
||||||
|
@Json(name = "m.rendezvous.v1.curve25519-aes-sha256") ECDH_V1("m.rendezvous.v1.curve25519-aes-sha256")
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* 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.transports
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
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.internal.rendezvous.RendezvousFailureReason
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.RendezvousTransport
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousTransportDetails
|
||||||
|
import org.matrix.android.sdk.internal.rendezvous.model.RendezvousTransportType
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
private val TAG = LoggerTag(SimpleHttpRendezvousTransport::class.java.simpleName, LoggerTag.RENDEZVOUS).value
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class SimpleHttpRendezvousTransportDetails(
|
||||||
|
@Json val uri: String
|
||||||
|
): RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the Simple HTTP transport MSC3886: https://github.com/matrix-org/matrix-spec-proposals/pull/3886
|
||||||
|
*/
|
||||||
|
class SimpleHttpRendezvousTransport(override var onCancelled: ((reason: RendezvousFailureReason) -> Unit)?, rendezvousUri: String?) : RendezvousTransport {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun send(contentType: MediaType, data: ByteArray) {
|
||||||
|
if (cancelled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val method = if (uri != null) "PUT" else "POST"
|
||||||
|
// TODO: properly determine endpoint
|
||||||
|
val uri = if (uri != null) uri!! else "https://rendezvous.lab.element.dev"
|
||||||
|
|
||||||
|
Timber.tag(TAG).i("Sending data: ${data.toString(Charsets.UTF_8)} to $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) {
|
||||||
|
cancel(RendezvousFailureReason.Unknown)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
expiresAt = format.parse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve location header which could be relative or absolute
|
||||||
|
this.uri = response.request.url.toUri().resolve(location).toString()
|
||||||
|
ready = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun receive(): ByteArray? {
|
||||||
|
val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
|
||||||
|
var done = false
|
||||||
|
val httpClient = okhttp3.OkHttpClient.Builder().build()
|
||||||
|
while (!done) {
|
||||||
|
if (cancelled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
|
||||||
|
Timber.tag(TAG).i("Received polling response: ${response.code} from $uri")
|
||||||
|
|
||||||
|
if (response.code == 404) {
|
||||||
|
cancel(RendezvousFailureReason.Unknown)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
val data = response.body?.bytes()
|
||||||
|
Timber.tag(TAG).i("Received data: ${data?.toString(Charsets.UTF_8)} from $uri with etag $etag")
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
done = false
|
||||||
|
delay(1000)
|
||||||
|
} finally {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun cancel(reason: RendezvousFailureReason) {
|
||||||
|
var mappedReason = reason
|
||||||
|
if (mappedReason == RendezvousFailureReason.Unknown &&
|
||||||
|
expiresAt != null && Date() > expiresAt) {
|
||||||
|
mappedReason = RendezvousFailureReason.Expired
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled = true
|
||||||
|
ready = false
|
||||||
|
onCancelled ?.let { it(mappedReason) }
|
||||||
|
|
||||||
|
if (mappedReason == RendezvousFailureReason.UserDeclined) {
|
||||||
|
uri ?.let {
|
||||||
|
try {
|
||||||
|
val httpClient = okhttp3.OkHttpClient.Builder().build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(it)
|
||||||
|
.delete()
|
||||||
|
.build()
|
||||||
|
httpClient.newCall(request).execute()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag(TAG).w(e, "Failed to delete channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue