Reduce logging

This commit is contained in:
Hugh Nimmo-Smith 2022-10-13 21:26:22 +01:00
parent dd47297dfd
commit 7bc0bd3b57
2 changed files with 403 additions and 0 deletions

View File

@ -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).d("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).d("Decrypted: ${plainTextBytes.toString(Charsets.UTF_8)}")
return plainTextBytes
}
}

View File

@ -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).d("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).d("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
Timber.tag(TAG).i("$expiresAt")
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")
}
}
}
}
}