mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-31 11:24:58 +01:00
Merge pull request #8123 from vector-im/hughns/msc3903-v2
Support for v2 of MSC3903
This commit is contained in:
commit
e8ea5388b9
1
changelog.d/8123.feature
Normal file
1
changelog.d/8123.feature
Normal file
@ -0,0 +1 @@
|
||||
Updates to protocol used for Sign in with QR code
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -26,8 +26,11 @@ 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
|
||||
@ -53,18 +56,37 @@ class Rendezvous(
|
||||
|
||||
@Throws(RendezvousError::class)
|
||||
fun buildChannelFromCode(code: String): Rendezvous {
|
||||
val parsed = try {
|
||||
// we rely on moshi validating the code and throwing exception if invalid JSON or doesn't
|
||||
// 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("Invalid code", RendezvousFailureReason.InvalidCode)
|
||||
} ?: throw RendezvousError("Invalid code", RendezvousFailureReason.InvalidCode)
|
||||
throw RendezvousError("Malformed ECDH rendezvous code", RendezvousFailureReason.InvalidCode)
|
||||
} ?: throw RendezvousError("ECDH rendezvous code is null", RendezvousFailureReason.InvalidCode)
|
||||
|
||||
val transport = SimpleHttpRendezvousTransport(parsed.rendezvous.transport.uri)
|
||||
val transport = SimpleHttpRendezvousTransport(supportedParsed.rendezvous.transport.uri)
|
||||
|
||||
return Rendezvous(
|
||||
ECDHRendezvousChannel(transport, parsed.rendezvous.key),
|
||||
parsed.intent
|
||||
ECDHRendezvousChannel(transport, supportedParsed.rendezvous.algorithm, supportedParsed.rendezvous.key),
|
||||
supportedParsed.intent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,11 @@ 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, theirPublicKeyBase64: String?) : RendezvousChannel {
|
||||
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"
|
||||
@ -53,7 +57,7 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu
|
||||
val algorithm: SecureRendezvousChannelAlgorithm? = null,
|
||||
val key: String? = null,
|
||||
val ciphertext: String? = null,
|
||||
val iv: String? = null
|
||||
val iv: String? = null,
|
||||
)
|
||||
|
||||
private val olmSASMutex = Mutex()
|
||||
@ -65,10 +69,22 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu
|
||||
|
||||
init {
|
||||
theirPublicKeyBase64?.let {
|
||||
theirPublicKey = Base64.decode(it, Base64.NO_WRAP)
|
||||
theirPublicKey = decodeBase64(it)
|
||||
}
|
||||
olmSAS = OlmSAS()
|
||||
ourPublicKey = Base64.decode(olmSAS!!.publicKey, Base64.NO_WRAP)
|
||||
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)
|
||||
@ -86,25 +102,25 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu
|
||||
RendezvousFailureReason.UnsupportedAlgorithm,
|
||||
)
|
||||
}
|
||||
theirPublicKey = Base64.decode(res.key, Base64.NO_WRAP)
|
||||
theirPublicKey = decodeBase64(res.key)
|
||||
} 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)
|
||||
algorithm = algorithm,
|
||||
key = encodeBase64(ourPublicKey)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
olmSASMutex.withLock {
|
||||
sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
|
||||
sas.setTheirPublicKey(Base64.encodeToString(theirPublicKey, Base64.NO_WRAP))
|
||||
sas.setTheirPublicKey(encodeBase64(theirPublicKey))
|
||||
sas.setTheirPublicKey(encodeBase64(theirPublicKey))
|
||||
|
||||
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"
|
||||
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)
|
||||
|
||||
@ -162,20 +178,20 @@ class ECDHRendezvousChannel(override var transport: RendezvousTransport, theirPu
|
||||
cipherText.addAll(encryptCipher.doFinal().toList())
|
||||
|
||||
return ECDHPayload(
|
||||
ciphertext = Base64.encodeToString(cipherText.toByteArray(), Base64.NO_WRAP),
|
||||
iv = Base64.encodeToString(iv, Base64.NO_WRAP)
|
||||
ciphertext = encodeBase64(cipherText.toByteArray()),
|
||||
iv = encodeBase64(iv)
|
||||
)
|
||||
}
|
||||
|
||||
private fun decrypt(payload: ECDHPayload): ByteArray {
|
||||
val iv = Base64.decode(payload.iv, Base64.NO_WRAP)
|
||||
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(Base64.decode(payload.ciphertext, Base64.NO_WRAP)).toList())
|
||||
plainText.addAll(encryptCipher.update(decodeBase64(payload.ciphertext)).toList())
|
||||
plainText.addAll(encryptCipher.doFinal().toList())
|
||||
|
||||
return plainText.toByteArray()
|
||||
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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,
|
||||
)
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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
|
||||
)
|
@ -20,5 +20,5 @@ import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
open class RendezvousTransportDetails(
|
||||
val type: RendezvousTransportType
|
||||
val type: String
|
||||
)
|
||||
|
@ -22,5 +22,7 @@ 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")
|
||||
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")
|
||||
}
|
||||
|
@ -21,4 +21,4 @@ import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SimpleHttpRendezvousTransportDetails(
|
||||
val uri: String
|
||||
) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1)
|
||||
) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1.name)
|
||||
|
Loading…
x
Reference in New Issue
Block a user