Support verification using room transport

This commit is contained in:
Valere 2019-12-01 12:44:29 +01:00
parent 79ef055bfb
commit 26b4b6e194
57 changed files with 1939 additions and 288 deletions

View file

@ -33,6 +33,7 @@ Changes in RiotX 0.9.0 (2019-12-05)
Features ✨: Features ✨:
- Account creation. It's now possible to create account on any homeserver with RiotX (#34) - Account creation. It's now possible to create account on any homeserver with RiotX (#34)
- Iteration of the login flow (#613) - Iteration of the login flow (#613)
- [SDK] MSC2241 / verification in DMs (#707)
Improvements 🙌: Improvements 🙌:
- Send mention Pills from composer - Send mention Pills from composer

View file

@ -16,19 +16,42 @@
package im.vector.matrix.android.api.session.crypto.sas package im.vector.matrix.android.api.session.crypto.sas
import im.vector.matrix.android.api.MatrixCallback
/**
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
*
* Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors.
* SAS verification is a user-friendly key verification process.
* SAS verification is intended to be a highly interactive process for users,
* and as such exposes verification methods which are easier for users to use.
*/
interface SasVerificationService { interface SasVerificationService {
fun addListener(listener: SasVerificationListener) fun addListener(listener: SasVerificationListener)
fun removeListener(listener: SasVerificationListener) fun removeListener(listener: SasVerificationListener)
/**
* Mark this device as verified manually
*/
fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) fun markedLocallyAsManuallyVerified(userId: String, deviceID: String)
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
/**
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
* @see beginKeyVerification
*/
fun beginKeyVerificationSAS(userId: String, deviceID: String): String? fun beginKeyVerificationSAS(userId: String, deviceID: String): String?
/**
* Request a key verification from another user using toDevice events.
*/
fun beginKeyVerification(method: String, userId: String, deviceID: String): String? fun beginKeyVerification(method: String, userId: String, deviceID: String): String?
fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
// fun transactionUpdated(tx: SasVerificationTransaction) // fun transactionUpdated(tx: SasVerificationTransaction)
interface SasVerificationListener { interface SasVerificationListener {

View file

@ -17,7 +17,7 @@
package im.vector.matrix.android.api.session.crypto.sas package im.vector.matrix.android.api.session.crypto.sas
interface SasVerificationTransaction { interface SasVerificationTransaction {
val state: SasVerificationTxState var state: SasVerificationTxState
val cancelledReason: CancelCode? val cancelledReason: CancelCode?

View file

@ -72,6 +72,7 @@ object EventType {
const val KEY_VERIFICATION_KEY = "m.key.verification.key" const val KEY_VERIFICATION_KEY = "m.key.verification.key"
const val KEY_VERIFICATION_MAC = "m.key.verification.mac" const val KEY_VERIFICATION_MAC = "m.key.verification.mac"
const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel"
const val KEY_VERIFICATION_DONE = "m.key.verification.done"
// Relation Events // Relation Events
const val REACTION = "m.reaction" const val REACTION = "m.reaction"

View file

@ -0,0 +1,26 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
@JsonClass(generateAdapter = true)
internal data class MessageRelationContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
)

View file

@ -26,6 +26,7 @@ object MessageType {
const val MSGTYPE_VIDEO = "m.video" const val MSGTYPE_VIDEO = "m.video"
const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_LOCATION = "m.location"
const val MSGTYPE_FILE = "m.file" const val MSGTYPE_FILE = "m.file"
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
// Add, in local, a fake message type in order to StickerMessage can inherit Message class // Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field // Because sticker isn't a message type but a event type without msgtype field

View file

@ -0,0 +1,76 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.AcceptVerifInfoFactory
import im.vector.matrix.android.internal.crypto.verification.VerifInfoAccept
import timber.log.Timber
@JsonClass(generateAdapter = true)
internal data class MessageVerificationAcceptContent(
@Json(name = "hash") override val hash: String?,
@Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?,
@Json(name = "message_authentication_code") override val messageAuthenticationCode: String?,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
@Json(name = "commitment") override var commitment: String? = null
) : VerifInfoAccept {
override val transactionID: String?
get() = relatesTo?.eventId
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| keyAgreementProtocol.isNullOrBlank()
|| hash.isNullOrBlank()
|| commitment.isNullOrBlank()
|| messageAuthenticationCode.isNullOrBlank()
|| shortAuthenticationStrings.isNullOrEmpty()) {
Timber.e("## received invalid verification request")
return false
}
return true
}
override fun toEventContent() = this.toContent()
companion object : AcceptVerifInfoFactory {
override fun create(tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerifInfoAccept {
return MessageVerificationAcceptContent(
hash,
keyAgreementProtocol,
messageAuthenticationCode,
shortAuthenticationStrings,
RelationDefaultContent(
RelationType.REFERENCE,
tid
),
commitment
)
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.VerifInfoCancel
@JsonClass(generateAdapter = true)
internal data class MessageVerificationCancelContent(
@Json(name = "code") override val code: String? = null,
@Json(name = "reason") override val reason: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerifInfoCancel {
override val transactionID: String?
get() = relatesTo?.eventId
override fun toEventContent() = this.toContent()
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
return false
}
return true
}
companion object {
fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent {
return MessageVerificationCancelContent(
reason.value,
reason.humanReadable,
RelationDefaultContent(
RelationType.REFERENCE,
transactionId
)
)
}
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
@JsonClass(generateAdapter = true)
internal data class MessageVerificationDoneContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfo {
override fun isValid() = true
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.VerifInfoKey
import im.vector.matrix.android.internal.crypto.verification.KeyVerifInfoFactory
import timber.log.Timber
@JsonClass(generateAdapter = true)
internal data class MessageVerificationKeyContent(
/**
* The devices ephemeral public key, as an unpadded base64 string
*/
@Json(name = "key") override val key: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerifInfoKey {
override val transactionID: String?
get() = relatesTo?.eventId
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || key.isNullOrBlank()) {
Timber.e("## received invalid verification request")
return false
}
return true
}
override fun toEventContent() = this.toContent()
companion object : KeyVerifInfoFactory {
override fun create(tid: String, pubKey: String): VerifInfoKey {
return MessageVerificationKeyContent(
pubKey,
RelationDefaultContent(
RelationType.REFERENCE,
tid
)
)
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.VerifInfoMac
import im.vector.matrix.android.internal.crypto.verification.VerifInfoMacFactory
@JsonClass(generateAdapter = true)
internal data class MessageVerificationMacContent(
@Json(name = "mac") override val mac: Map<String, String>? = null,
@Json(name = "keys") override val keys: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerifInfoMac {
override val transactionID: String?
get() = relatesTo?.eventId
override fun toEventContent() = this.toContent()
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
return false
}
return true
}
companion object : VerifInfoMacFactory {
override fun create(tid: String, mac: Map<String, String>, keys: String): VerifInfoMac {
return MessageVerificationMacContent(
mac,
keys,
RelationDefaultContent(
RelationType.REFERENCE,
tid
)
)
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
@JsonClass(generateAdapter = true)
class MessageVerificationRequestContent(
@Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
@Json(name = "body") override val body: String,
@Json(name = "from_device") val fromDevice: String,
@Json(name = "methods") val methods: List<String>,
@Json(name = "to") val to: String,
// @Json(name = "timestamp") val timestamp: Int,
@Json(name = "format") val format: String? = null,
@Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent

View file

@ -0,0 +1,61 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.sas.SasMode
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerifInfoStart
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
@JsonClass(generateAdapter = true)
data class MessageVerificationStartContent(
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "hashes") override val hashes: List<String>?,
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>?,
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
@Json(name = "method") override val method: String?,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerifInfoStart {
override fun toCanonicalJson(): String? {
return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this)
}
override val transactionID: String?
get() = relatesTo?.eventId
override fun isValid(): Boolean {
if (
(transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() || method != KeyVerificationStart.VERIF_METHOD_SAS || keyAgreementProtocols.isNullOrEmpty() || hashes.isNullOrEmpty())
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
Timber.e("## received invalid verification request")
return false
}
return true
}
override fun toEventContent() = this.toContent()
}

View file

@ -177,6 +177,9 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask
@Binds
abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask
@Binds @Binds
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice) abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
: ClaimOneTimeKeysForUsersDeviceTask : ClaimOneTimeKeysForUsersDeviceTask

View file

@ -136,6 +136,10 @@ internal class DefaultCryptoService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope
) : CryptoService { ) : CryptoService {
init {
sasVerificationService.cryptoService = this
}
private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
// MXEncrypting instance for each room. // MXEncrypting instance for each room.

View file

@ -17,13 +17,15 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.verification.VerifInfoAccept
import im.vector.matrix.android.internal.crypto.verification.AcceptVerifInfoFactory
import timber.log.Timber import timber.log.Timber
/** /**
* Sent by Bob to accept a verification from a previously sent m.key.verification.start message. * Sent by Bob to accept a verification from a previously sent m.key.verification.start message.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class KeyVerificationAccept( internal data class KeyVerificationAccept(
/** /**
* string to identify the transaction. * string to identify the transaction.
@ -31,39 +33,41 @@ data class KeyVerificationAccept(
* Alices device should record this ID and use it in future messages in this transaction. * Alices device should record this ID and use it in future messages in this transaction.
*/ */
@Json(name = "transaction_id") @Json(name = "transaction_id")
var transactionID: String? = null, override var transactionID: String? = null,
/** /**
* The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device * The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device
*/ */
@Json(name = "key_agreement_protocol") @Json(name = "key_agreement_protocol")
var keyAgreementProtocol: String? = null, override var keyAgreementProtocol: String? = null,
/** /**
* The hash algorithm that Bobs device has selected to use, out of the list proposed by Alices device * The hash algorithm that Bobs device has selected to use, out of the list proposed by Alices device
*/ */
var hash: String? = null, @Json(name = "hash")
override var hash: String? = null,
/** /**
* The message authentication code that Bobs device has selected to use, out of the list proposed by Alices device * The message authentication code that Bobs device has selected to use, out of the list proposed by Alices device
*/ */
@Json(name = "message_authentication_code") @Json(name = "message_authentication_code")
var messageAuthenticationCode: String? = null, override var messageAuthenticationCode: String? = null,
/** /**
* An array of short authentication string methods that Bobs client (and Bob) understands. Must be a subset of the list proposed by Alices device * An array of short authentication string methods that Bobs client (and Bob) understands. Must be a subset of the list proposed by Alices device
*/ */
@Json(name = "short_authentication_string") @Json(name = "short_authentication_string")
var shortAuthenticationStrings: List<String>? = null, override var shortAuthenticationStrings: List<String>? = null,
/** /**
* The hash (encoded as unpadded base64) of the concatenation of the devices ephemeral public key (QB, encoded as unpadded base64) * The hash (encoded as unpadded base64) of the concatenation of the devices ephemeral public key (QB, encoded as unpadded base64)
* and the canonical JSON representation of the m.key.verification.start message. * and the canonical JSON representation of the m.key.verification.start message.
*/ */
var commitment: String? = null @Json(name = "commitment")
) : SendToDeviceObject { override var commitment: String? = null
) : SendToDeviceObject, VerifInfoAccept {
fun isValid(): Boolean { override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() if (transactionID.isNullOrBlank()
|| keyAgreementProtocol.isNullOrBlank() || keyAgreementProtocol.isNullOrBlank()
|| hash.isNullOrBlank() || hash.isNullOrBlank()
@ -76,13 +80,15 @@ data class KeyVerificationAccept(
return true return true
} }
companion object { override fun toSendToDeviceObject() = this
fun create(tid: String,
keyAgreementProtocol: String, companion object : AcceptVerifInfoFactory {
hash: String, override fun create(tid: String,
commitment: String, keyAgreementProtocol: String,
messageAuthenticationCode: String, hash: String,
shortAuthenticationStrings: List<String>): KeyVerificationAccept { commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerifInfoAccept {
return KeyVerificationAccept().apply { return KeyVerificationAccept().apply {
this.transactionID = tid this.transactionID = tid
this.keyAgreementProtocol = keyAgreementProtocol this.keyAgreementProtocol = keyAgreementProtocol

View file

@ -18,40 +18,43 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.internal.crypto.verification.VerifInfoCancel
/** /**
* To device event sent by either party to cancel a key verification. * To device event sent by either party to cancel a key verification.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class KeyVerificationCancel( internal data class KeyVerificationCancel(
/** /**
* the transaction ID of the verification to cancel * the transaction ID of the verification to cancel
*/ */
@Json(name = "transaction_id") @Json(name = "transaction_id")
var transactionID: String? = null, override val transactionID: String? = null,
/** /**
* machine-readable reason for cancelling, see #CancelCode * machine-readable reason for cancelling, see #CancelCode
*/ */
var code: String? = null, override var code: String? = null,
/** /**
* human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given.
*/ */
var reason: String? = null override var reason: String? = null
) : SendToDeviceObject { ) : SendToDeviceObject, VerifInfoCancel {
companion object { companion object {
fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel {
return KeyVerificationCancel().apply { return KeyVerificationCancel(
this.transactionID = tid tid,
this.code = cancelCode.value cancelCode.value,
this.reason = cancelCode.humanReadable cancelCode.humanReadable
} )
} }
} }
fun isValid(): Boolean { override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
return false return false
} }

View file

@ -17,37 +17,33 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.verification.KeyVerifInfoFactory
import im.vector.matrix.android.internal.crypto.verification.VerifInfoKey
/** /**
* Sent by both devices to send their ephemeral Curve25519 public key to the other device. * Sent by both devices to send their ephemeral Curve25519 public key to the other device.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class KeyVerificationKey( internal data class KeyVerificationKey(
/** /**
* the ID of the transaction that the message is part of * the ID of the transaction that the message is part of
*/ */
@Json(name = "transaction_id") @Json(name = "transaction_id") override var transactionID: String? = null,
@JvmField
var transactionID: String? = null,
/** /**
* The devices ephemeral public key, as an unpadded base64 string * The devices ephemeral public key, as an unpadded base64 string
*/ */
@JvmField @Json(name = "key") override val key: String? = null
var key: String? = null
) : SendToDeviceObject { ) : SendToDeviceObject, VerifInfoKey {
companion object { companion object : KeyVerifInfoFactory {
fun create(tid: String, key: String): KeyVerificationKey { override fun create(tid: String, pubKey: String): KeyVerificationKey {
return KeyVerificationKey().apply { return KeyVerificationKey(tid, pubKey)
this.transactionID = tid
this.key = key
}
} }
} }
fun isValid(): Boolean { override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { if (transactionID.isNullOrBlank() || key.isNullOrBlank()) {
return false return false
} }

View file

@ -17,49 +17,32 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.verification.VerifInfoMac
import im.vector.matrix.android.internal.crypto.verification.VerifInfoMacFactory
/** /**
* Sent by both devices to send the MAC of their device key to the other device. * Sent by both devices to send the MAC of their device key to the other device.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class KeyVerificationMac( internal data class KeyVerificationMac(
/** @Json(name = "transaction_id") override val transactionID: String? = null,
* the ID of the transaction that the message is part of @Json(name = "mac") override val mac: Map<String, String>? = null,
*/ @Json(name = "key") override val keys: String? = null
@Json(name = "transaction_id")
var transactionID: String? = null,
/** ) : SendToDeviceObject, VerifInfoMac {
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key
*/
@JvmField
var mac: Map<String, String>? = null,
/** override fun isValid(): Boolean {
* The MAC of the comma-separated, sorted list of key IDs given in the mac property,
* as an unpadded base64 string, calculated using the MAC key.
* For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will
* give the MAC of the string ed25519:ABCDEFG,ed25519:HIJKLMN.
*/
@JvmField
var keys: String? = null
) : SendToDeviceObject {
fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
return false return false
} }
return true return true
} }
companion object { override fun toSendToDeviceObject(): SendToDeviceObject? = this
fun create(tid: String, mac: Map<String, String>, keys: String): KeyVerificationMac {
return KeyVerificationMac().apply { companion object : VerifInfoMacFactory {
this.transactionID = tid override fun create(tid: String, mac: Map<String, String>, keys: String): VerifInfoMac {
this.mac = mac return KeyVerificationMac(tid, mac, keys)
this.keys = keys
}
} }
} }
} }

View file

@ -0,0 +1,48 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
/**
* Requests a key verification with another user's devices.
*/
@JsonClass(generateAdapter = true)
data class KeyVerificationRequest(
@Json(name = "from_device")
val fromDevice: String,
/** The verification methods supported by the sender. */
val methods: List<String> = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
/**
* The POSIX timestamp in milliseconds for when the request was made.
* If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
* the message should be ignored by the receiver.
*/
val timestamp: Int,
@Json(name = "transaction_id")
var transactionID: String? = null
) : SendToDeviceObject, VerificationInfo {
override fun isValid(): Boolean {
// TODO
return true
}
}

View file

@ -19,21 +19,27 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.sas.SasMode import im.vector.matrix.android.api.session.crypto.sas.SasMode
import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerifInfoStart
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber import timber.log.Timber
/** /**
* Sent by Alice to initiate an interactive key verification. * Sent by Alice to initiate an interactive key verification.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class KeyVerificationStart : SendToDeviceObject { class KeyVerificationStart : SendToDeviceObject, VerifInfoStart {
override fun toCanonicalJson(): String? {
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
}
/** /**
* Alices device ID * Alices device ID
*/ */
@Json(name = "from_device") @Json(name = "from_device")
var fromDevice: String? = null override var fromDevice: String? = null
var method: String? = null override var method: String? = null
/** /**
* String to identify the transaction. * String to identify the transaction.
@ -41,7 +47,7 @@ class KeyVerificationStart : SendToDeviceObject {
* Alices device should record this ID and use it in future messages in this transaction. * Alices device should record this ID and use it in future messages in this transaction.
*/ */
@Json(name = "transaction_id") @Json(name = "transaction_id")
var transactionID: String? = null override var transactionID: String? = null
/** /**
* An array of key agreement protocols that Alices client understands. * An array of key agreement protocols that Alices client understands.
@ -49,13 +55,13 @@ class KeyVerificationStart : SendToDeviceObject {
* Other methods may be defined in the future * Other methods may be defined in the future
*/ */
@Json(name = "key_agreement_protocols") @Json(name = "key_agreement_protocols")
var keyAgreementProtocols: List<String>? = null override var keyAgreementProtocols: List<String>? = null
/** /**
* An array of hashes that Alices client understands. * An array of hashes that Alices client understands.
* Must include sha256. Other methods may be defined in the future. * Must include sha256. Other methods may be defined in the future.
*/ */
var hashes: List<String>? = null override var hashes: List<String>? = null
/** /**
* An array of message authentication codes that Alices client understands. * An array of message authentication codes that Alices client understands.
@ -63,7 +69,7 @@ class KeyVerificationStart : SendToDeviceObject {
* Other methods may be defined in the future. * Other methods may be defined in the future.
*/ */
@Json(name = "message_authentication_codes") @Json(name = "message_authentication_codes")
var messageAuthenticationCodes: List<String>? = null override var messageAuthenticationCodes: List<String>? = null
/** /**
* An array of short authentication string methods that Alices client (and Alice) understands. * An array of short authentication string methods that Alices client (and Alice) understands.
@ -72,13 +78,13 @@ class KeyVerificationStart : SendToDeviceObject {
* Other methods may be defined in the future * Other methods may be defined in the future
*/ */
@Json(name = "short_authentication_string") @Json(name = "short_authentication_string")
var shortAuthenticationStrings: List<String>? = null override var shortAuthenticationStrings: List<String>? = null
companion object { companion object {
const val VERIF_METHOD_SAS = "m.sas.v1" const val VERIF_METHOD_SAS = "m.sas.v1"
} }
fun isValid(): Boolean { override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() if (transactionID.isNullOrBlank()
|| fromDevice.isNullOrBlank() || fromDevice.isNullOrBlank()
|| method != VERIF_METHOD_SAS || method != VERIF_METHOD_SAS
@ -95,4 +101,6 @@ class KeyVerificationStart : SendToDeviceObject {
} }
return true return true
} }
override fun toSendToDeviceObject() = this
} }

View file

@ -0,0 +1,80 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitCallback
import javax.inject.Inject
internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
data class Params(val roomId: String,
val event: Event,
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
val keepKeys: List<String>? = null,
val crypto: CryptoService
)
}
internal class DefaultEncryptEventTask @Inject constructor(
// private val crypto: CryptoService
private val localEchoUpdater: LocalEchoUpdater
) : EncryptEventTask {
override suspend fun execute(params: EncryptEventTask.Params): Event {
if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
val localEvent = params.event
if (localEvent.eventId == null) {
throw IllegalArgumentException()
}
localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
params.keepKeys?.forEach {
localMutableContent.remove(it)
}
// try {
awaitCallback<MXEncryptEventContentResult> {
params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
}.let { result ->
val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep ->
localEvent.content?.get(toKeep)?.let {
// put it back in the encrypted thing
modifiedContent[toKeep] = it
}
}
val safeResult = result.copy(eventContent = modifiedContent)
return localEvent.copy(
type = safeResult.eventType,
content = safeResult.eventContent
)
}
// } catch (throwable: Throwable) {
// val sendState = when (throwable) {
// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
// else -> SendState.UNDELIVERED
// }
// localEchoUpdater.updateSendState(localEvent.eventId, sendState)
// throw throwable
// }
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.tasks
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface RequestVerificationDMTask : Task<RequestVerificationDMTask.Params, SendResponse> {
data class Params(
val roomId: String,
val from: String,
val methods: List<String>,
val to: String,
val cryptoService: CryptoService
)
}
internal class DefaultRequestVerificationDMTask @Inject constructor(
private val localEchoUpdater: LocalEchoUpdater,
private val localEchoEventFactory: LocalEchoEventFactory,
private val encryptEventTask: DefaultEncryptEventTask,
private val monarchy: Monarchy,
private val roomAPI: RoomAPI)
: RequestVerificationDMTask {
override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse {
val event = createRequestEvent(params)
val localID = event.eventId!!
try {
localEchoUpdater.updateSendState(localID, SendState.SENDING)
val executeRequest = executeRequest<SendResponse> {
apiCall = roomAPI.send(
localID,
roomId = params.roomId,
content = event.content,
eventType = event.type // message or room.encrypted
)
}
localEchoUpdater.updateSendState(localID, SendState.SENT)
return executeRequest
} catch (e: Throwable) {
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
throw e
}
}
private suspend fun createRequestEvent(params: RequestVerificationDMTask.Params): Event {
val event = localEchoEventFactory.createVerificationRequest(params.roomId, params.from, params.to, params.methods).also {
localEchoEventFactory.saveLocalEcho(monarchy, it)
}
if (params.cryptoService.isRoomEncrypted(params.roomId)) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
params.roomId,
event,
listOf("m.relates_to"),
params.cryptoService
))
} catch (throwable: Throwable) {
// We said it's ok to send verification request in clear
}
}
return event
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.tasks
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, SendResponse> {
data class Params(
val type: String,
val roomId: String,
val content: Content,
val cryptoService: CryptoService?
)
}
internal class DefaultSendVerificationMessageTask @Inject constructor(
private val localEchoUpdater: LocalEchoUpdater,
private val localEchoEventFactory: LocalEchoEventFactory,
private val encryptEventTask: DefaultEncryptEventTask,
private val monarchy: Monarchy,
@UserId private val userId: String,
private val roomAPI: RoomAPI) : SendVerificationMessageTask {
override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse {
val event = createRequestEvent(params)
val localID = event.eventId!!
try {
localEchoUpdater.updateSendState(localID, SendState.SENDING)
val executeRequest = executeRequest<SendResponse> {
apiCall = roomAPI.send(
localID,
roomId = params.roomId,
content = event.content,
eventType = event.type
)
}
localEchoUpdater.updateSendState(localID, SendState.SENT)
return executeRequest
} catch (e: Throwable) {
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
throw e
}
}
private suspend fun createRequestEvent(params: SendVerificationMessageTask.Params): Event {
val localID = LocalEcho.createLocalEchoId()
val event = Event(
roomId = params.roomId,
originServerTs = System.currentTimeMillis(),
senderId = userId,
eventId = localID,
type = params.type,
content = params.content,
unsignedData = UnsignedData(age = null, transactionId = localID)
).also {
localEchoEventFactory.saveLocalEcho(monarchy, it)
}
if (params.cryptoService?.isRoomEncrypted(params.roomId) == true) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
params.roomId,
event,
listOf("m.relates_to"),
params.cryptoService
))
} catch (throwable: Throwable) {
// We said it's ok to send verification request in clear
}
}
return event
}
}

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import android.util.Base64 import android.util.Base64
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
@ -23,33 +24,20 @@ import im.vector.matrix.android.api.session.crypto.sas.SasMode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber import timber.log.Timber
internal class IncomingSASVerificationTransaction( internal class DefaultIncomingSASVerificationTransaction(
private val sasVerificationService: DefaultSasVerificationService, setDeviceVerificationAction: SetDeviceVerificationAction,
private val setDeviceVerificationAction: SetDeviceVerificationAction, override val credentials: Credentials,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor,
deviceFingerprint: String, deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserID: String) otherUserID: String
: SASVerificationTransaction( ) : SASVerificationTransaction(
sasVerificationService,
setDeviceVerificationAction, setDeviceVerificationAction,
credentials, credentials,
cryptoStore, cryptoStore,
sendToDeviceTask,
taskExecutor,
deviceFingerprint, deviceFingerprint,
transactionId, transactionId,
otherUserID, otherUserID,
@ -78,10 +66,10 @@ internal class IncomingSASVerificationTransaction(
} }
} }
override fun onVerificationStart(startReq: KeyVerificationStart) { override fun onVerificationStart(startReq: VerifInfoStart) {
Timber.v("## SAS received verification request from state $state") Timber.v("## SAS I: received verification request from state $state")
if (state != SasVerificationTxState.None) { if (state != SasVerificationTxState.None) {
Timber.e("## received verification request from invalid state") Timber.e("## SAS I: received verification request from invalid state")
// should I cancel?? // should I cancel??
throw IllegalStateException("Interactive Key verification already started") throw IllegalStateException("Interactive Key verification already started")
} }
@ -92,7 +80,7 @@ internal class IncomingSASVerificationTransaction(
override fun performAccept() { override fun performAccept() {
if (state != SasVerificationTxState.OnStarted) { if (state != SasVerificationTxState.OnStarted) {
Timber.e("## Cannot perform accept from state $state") Timber.e("## SAS Cannot perform accept from state $state")
return return
} }
@ -109,7 +97,7 @@ internal class IncomingSASVerificationTransaction(
if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() }
|| agreedShortCode.isNullOrEmpty()) { || agreedShortCode.isNullOrEmpty()) {
// Failed to find agreement // Failed to find agreement
Timber.e("## Failed to find agreement ") Timber.e("## SAS Failed to find agreement ")
cancel(CancelCode.UnknownMethod) cancel(CancelCode.UnknownMethod)
return return
} }
@ -118,15 +106,15 @@ internal class IncomingSASVerificationTransaction(
val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId) val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId)
if (mxDeviceInfo?.fingerprint() == null) { if (mxDeviceInfo?.fingerprint() == null) {
Timber.e("## Failed to find device key ") Timber.e("## SAS Failed to find device key ")
// TODO force download keys!! // TODO force download keys!!
// would be probably better to download the keys // would be probably better to download the keys
// for now I cancel // for now I cancel
cancel(CancelCode.User) cancel(CancelCode.User)
} else { } else {
// val otherKey = info.identityKey() // val otherKey = info.identityKey()
// need to jump back to correct thread // need to jump back to correct thread
val accept = KeyVerificationAccept.create( val accept = transport.createAccept(
tid = transactionId, tid = transactionId,
keyAgreementProtocol = agreedProtocol!!, keyAgreementProtocol = agreedProtocol!!,
hash = agreedHash!!, hash = agreedHash!!,
@ -138,13 +126,13 @@ internal class IncomingSASVerificationTransaction(
} }
} }
private fun doAccept(accept: KeyVerificationAccept) { private fun doAccept(accept: VerifInfoAccept) {
this.accepted = accept this.accepted = accept
Timber.v("## SAS accept request id:$transactionId") Timber.v("## SAS incoming accept request id:$transactionId")
// The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB,
// concatenated with the canonical JSON representation of the content of the m.key.verification.start message // concatenated with the canonical JSON representation of the content of the m.key.verification.start message
val concat = getSAS().publicKey + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) val concat = getSAS().publicKey + startReq!!.toCanonicalJson()
accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" accept.commitment = hashUsingAgreedHashMethod(concat) ?: ""
// we need to send this to other device now // we need to send this to other device now
state = SasVerificationTxState.SendingAccept state = SasVerificationTxState.SendingAccept
@ -156,15 +144,15 @@ internal class IncomingSASVerificationTransaction(
} }
} }
override fun onVerificationAccept(accept: KeyVerificationAccept) { override fun onVerificationAccept(accept: VerifInfoAccept) {
Timber.v("## SAS invalid message for incoming request id:$transactionId") Timber.v("## SAS invalid message for incoming request id:$transactionId")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
} }
override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) {
Timber.v("## SAS received key for request id:$transactionId") Timber.v("## SAS received key for request id:$transactionId")
if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) { if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) {
Timber.e("## received key from invalid state $state") Timber.e("## SAS received key from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }
@ -175,7 +163,7 @@ internal class IncomingSASVerificationTransaction(
// sending Bobs public key QB // sending Bobs public key QB
val pubKey = getSAS().publicKey val pubKey = getSAS().publicKey
val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) val keyToDevice = transport.createKey(transactionId, pubKey)
// we need to send this to other device now // we need to send this to other device now
state = SasVerificationTxState.SendingKey state = SasVerificationTxState.SendingKey
this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) {
@ -206,14 +194,16 @@ internal class IncomingSASVerificationTransaction(
// emoji: generate six bytes by using HKDF. // emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
Timber.e("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.e("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}")
}
state = SasVerificationTxState.ShortCodeReady state = SasVerificationTxState.ShortCodeReady
} }
override fun onKeyVerificationMac(vKey: KeyVerificationMac) { override fun onKeyVerificationMac(vKey: VerifInfoMac) {
Timber.v("## SAS received mac for request id:$transactionId") Timber.v("## SAS I: received mac for request id:$transactionId")
// Check for state? // Check for state?
if (state != SasVerificationTxState.SendingKey if (state != SasVerificationTxState.SendingKey
&& state != SasVerificationTxState.KeySent && state != SasVerificationTxState.KeySent
@ -221,7 +211,7 @@ internal class IncomingSASVerificationTransaction(
&& state != SasVerificationTxState.ShortCodeAccepted && state != SasVerificationTxState.ShortCodeAccepted
&& state != SasVerificationTxState.SendingMac && state != SasVerificationTxState.SendingMac
&& state != SasVerificationTxState.MacSent) { && state != SasVerificationTxState.MacSent) {
Timber.e("## received key from invalid state $state") Timber.e("## SAS I: received key from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }

View file

@ -21,34 +21,22 @@ import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRe
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber import timber.log.Timber
internal class OutgoingSASVerificationRequest( internal class DefaultOutgoingSASVerificationRequest(
private val sasVerificationService: DefaultSasVerificationService, setDeviceVerificationAction: SetDeviceVerificationAction,
private val setDeviceVerificationAction: SetDeviceVerificationAction, credentials: Credentials,
private val credentials: Credentials, cryptoStore: IMXCryptoStore,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor,
deviceFingerprint: String, deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserId: String, otherUserId: String,
otherDeviceId: String) otherDeviceId: String
: SASVerificationTransaction( ) : SASVerificationTransaction(
sasVerificationService,
setDeviceVerificationAction, setDeviceVerificationAction,
credentials, credentials,
cryptoStore, cryptoStore,
sendToDeviceTask,
taskExecutor,
deviceFingerprint, deviceFingerprint,
transactionId, transactionId,
otherUserId, otherUserId,
@ -78,14 +66,14 @@ internal class OutgoingSASVerificationRequest(
} }
} }
override fun onVerificationStart(startReq: KeyVerificationStart) { override fun onVerificationStart(startReq: VerifInfoStart) {
Timber.e("## onVerificationStart - unexpected id:$transactionId") Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
} }
fun start() { fun start() {
if (state != SasVerificationTxState.None) { if (state != SasVerificationTxState.None) {
Timber.e("## start verification from invalid state") Timber.e("## SAS O: start verification from invalid state")
// should I cancel?? // should I cancel??
throw IllegalStateException("Interactive Key verification already started") throw IllegalStateException("Interactive Key verification already started")
} }
@ -111,10 +99,33 @@ internal class OutgoingSASVerificationRequest(
) )
} }
override fun onVerificationAccept(accept: KeyVerificationAccept) { // fun request() {
Timber.v("## onVerificationAccept id:$transactionId") // if (state != SasVerificationTxState.None) {
// Timber.e("## start verification from invalid state")
// // should I cancel??
// throw IllegalStateException("Interactive Key verification already started")
// }
//
// val requestMessage = KeyVerificationRequest(
// fromDevice = session.sessionParams.credentials.deviceId ?: "",
// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
// timestamp = System.currentTimeMillis().toInt(),
// transactionID = transactionId
// )
//
// sendToOther(
// EventType.KEY_VERIFICATION_REQUEST,
// requestMessage,
// SasVerificationTxState.None,
// CancelCode.User,
// null
// )
// }
override fun onVerificationAccept(accept: VerifInfoAccept) {
Timber.v("## SAS O: onVerificationAccept id:$transactionId")
if (state != SasVerificationTxState.Started) { if (state != SasVerificationTxState.Started) {
Timber.e("## received accept request from invalid state $state") Timber.e("## SAS O: received accept request from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }
@ -123,7 +134,7 @@ internal class OutgoingSASVerificationRequest(
|| !KNOWN_HASHES.contains(accept.hash) || !KNOWN_HASHES.contains(accept.hash)
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode) || !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|| accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) { || accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) {
Timber.e("## received accept request from invalid state") Timber.e("## SAS O: received accept request from invalid state")
cancel(CancelCode.UnknownMethod) cancel(CancelCode.UnknownMethod)
return return
} }
@ -137,7 +148,7 @@ internal class OutgoingSASVerificationRequest(
// and replies with a to_device message with type set to “m.key.verification.key”, sending Alices public key QA // and replies with a to_device message with type set to “m.key.verification.key”, sending Alices public key QA
val pubKey = getSAS().publicKey val pubKey = getSAS().publicKey
val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) val keyToDevice = transport.createKey(transactionId, pubKey)
// we need to send this to other device now // we need to send this to other device now
state = SasVerificationTxState.SendingKey state = SasVerificationTxState.SendingKey
sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) {
@ -148,8 +159,8 @@ internal class OutgoingSASVerificationRequest(
} }
} }
override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) {
Timber.v("## onKeyVerificationKey id:$transactionId") Timber.v("## SAS O: onKeyVerificationKey id:$transactionId")
if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) { if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) {
Timber.e("## received key from invalid state $state") Timber.e("## received key from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
@ -163,7 +174,7 @@ internal class OutgoingSASVerificationRequest(
// in Bobs m.key.verification.key and the content of Alices m.key.verification.start message. // in Bobs m.key.verification.key and the content of Alices m.key.verification.start message.
// check commitment // check commitment
val concat = vKey.key + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) val concat = vKey.key + startReq!!.toCanonicalJson()
val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" val otherCommitment = hashUsingAgreedHashMethod(concat) ?: ""
if (accepted!!.commitment.equals(otherCommitment)) { if (accepted!!.commitment.equals(otherCommitment)) {
@ -190,14 +201,14 @@ internal class OutgoingSASVerificationRequest(
} }
} }
override fun onKeyVerificationMac(vKey: KeyVerificationMac) { override fun onKeyVerificationMac(vKey: VerifInfoMac) {
Timber.v("## onKeyVerificationMac id:$transactionId") Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
if (state != SasVerificationTxState.OnKeyReceived if (state != SasVerificationTxState.OnKeyReceived
&& state != SasVerificationTxState.ShortCodeReady && state != SasVerificationTxState.ShortCodeReady
&& state != SasVerificationTxState.ShortCodeAccepted && state != SasVerificationTxState.ShortCodeAccepted
&& state != SasVerificationTxState.SendingMac && state != SasVerificationTxState.SendingMac
&& state != SasVerificationTxState.MacSent) { && state != SasVerificationTxState.MacSent) {
Timber.e("## received key from invalid state $state") Timber.e("## SAS O: received key from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }

View file

@ -21,6 +21,7 @@ import android.os.Looper
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
@ -35,24 +37,23 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask
import im.vector.matrix.android.internal.crypto.tasks.RequestVerificationDMTask
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.lang.Exception import java.util.*
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.collections.set
/**
* Manages all current verifications transactions with short codes.
* Short codes interactive verification is a more user friendly way of verifying devices
* that is still maintaining a good level of security (alternative to the 43-character strings compare method).
*/
@SessionScope @SessionScope
internal class DefaultSasVerificationService @Inject constructor(private val credentials: Credentials, internal class DefaultSasVerificationService @Inject constructor(private val credentials: Credentials,
@ -61,12 +62,18 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val sendToDeviceTask: SendToDeviceTask, private val sendToDeviceTask: SendToDeviceTask,
private val requestVerificationDMTask: DefaultRequestVerificationDMTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
private val sasToDeviceTransportFactory: SasToDeviceTransportFactory,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: VerificationTransaction.Listener, SasVerificationService { : VerificationTransaction.Listener, SasVerificationService {
private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
// Cannot be injected in constructor as it creates a dependency cycle
lateinit var cryptoService: CryptoService
// map [sender : [transaction]] // map [sender : [transaction]]
private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>() private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>()
@ -96,6 +103,39 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
} }
} }
fun onRoomEvent(event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onRoomStartRequestReceived(event)
}
EventType.KEY_VERIFICATION_CANCEL -> {
onRoomCancelReceived(event)
}
EventType.KEY_VERIFICATION_ACCEPT -> {
onRoomAcceptReceived(event)
}
EventType.KEY_VERIFICATION_KEY -> {
onRoomKeyRequestReceived(event)
}
EventType.KEY_VERIFICATION_MAC -> {
onRoomMacReceived(event)
}
EventType.KEY_VERIFICATION_DONE -> {
// TODO?
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
onRoomRequestReceived(event)
}
}
else -> {
// ignore
}
}
}
}
private var listeners = ArrayList<SasVerificationService.SasVerificationListener>() private var listeners = ArrayList<SasVerificationService.SasVerificationListener>()
override fun addListener(listener: SasVerificationService.SasVerificationListener) { override fun addListener(listener: SasVerificationService.SasVerificationListener) {
@ -150,15 +190,64 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
} }
} }
fun onRoomRequestReceived(event: Event) {
// TODO
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
}
private suspend fun onRoomStartRequestReceived(event: Event) {
val startReq = event.getClearContent().toModel<MessageVerificationStartContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
val otherUserId = event.senderId
if (startReq?.isValid()?.not() == true) {
Timber.e("## received invalid verification request")
if (startReq.transactionID != null) {
sasTransportRoomMessageFactory.createTransport(event.roomId
?: "", cryptoService).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod
)
}
return
}
handleStart(otherUserId, startReq as VerifInfoStart) {
it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId
?: "", cryptoService)
}?.let {
sasTransportRoomMessageFactory.createTransport(event.roomId
?: "", cryptoService).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
it
)
}
}
private suspend fun onStartRequestReceived(event: Event) { private suspend fun onStartRequestReceived(event: Event) {
Timber.e("## SAS received Start request ${event.eventId}")
val startReq = event.getClearContent().toModel<KeyVerificationStart>()!! val startReq = event.getClearContent().toModel<KeyVerificationStart>()!!
Timber.v("## SAS received Start request $startReq")
val otherUserId = event.senderId val otherUserId = event.senderId
if (!startReq.isValid()) { if (!startReq.isValid()) {
Timber.e("## received invalid verification request") Timber.e("## SAS received invalid verification request")
if (startReq.transactionID != null) { if (startReq.transactionID != null) {
cancelTransaction( // cancelTransaction(
startReq.transactionID!!, // startReq.transactionID!!,
// otherUserId!!,
// startReq.fromDevice ?: event.getSenderKey()!!,
// CancelCode.UnknownMethod
// )
sasToDeviceTransportFactory.createTransport(null).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!, otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!, startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod CancelCode.UnknownMethod
@ -167,8 +256,22 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
return return
} }
// Download device keys prior to everything // Download device keys prior to everything
handleStart(otherUserId, startReq) {
it.transport = sasToDeviceTransportFactory.createTransport(it)
}?.let {
sasToDeviceTransportFactory.createTransport(null).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
it
)
}
}
private suspend fun handleStart(otherUserId: String?, startReq: VerifInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) {
Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}") Timber.v("## SAS onStartRequestReceived $startReq")
val tid = startReq.transactionID!! val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid) val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId) val existingTxs = getExistingTransactionsForUser(otherUserId)
@ -176,43 +279,46 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
// should cancel both! // should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}") Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
existing.cancel(CancelCode.UnexpectedMessage) existing.cancel(CancelCode.UnexpectedMessage)
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) return CancelCode.UnexpectedMessage
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else if (existingTxs?.isEmpty() == false) { } else if (existingTxs?.isEmpty() == false) {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}") Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time. // Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
existingTxs.forEach { existingTxs.forEach {
it.cancel(CancelCode.UnexpectedMessage) it.cancel(CancelCode.UnexpectedMessage)
} }
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) return CancelCode.UnexpectedMessage
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else { } else {
// Ok we can create // Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
val tx = IncomingSASVerificationTransaction( val tx = DefaultIncomingSASVerificationTransaction(
this, // this,
setDeviceVerificationAction, setDeviceVerificationAction,
credentials, credentials,
cryptoStore, cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!, myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!, startReq.transactionID!!,
otherUserId) otherUserId).also { txConfigure(it) }
addTransaction(tx) addTransaction(tx)
tx.acceptToDeviceEvent(otherUserId, startReq) tx.acceptVerificationEvent(otherUserId, startReq)
} else { } else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice return CancelCode.UnknownMethod
?: event.getSenderKey()!!, CancelCode.UnknownMethod) // cancelTransaction(tid, otherUserId, startReq.fromDevice
// ?: event.getSenderKey()!!, CancelCode.UnknownMethod)
} }
} }
} else { } else {
cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) return CancelCode.UnexpectedMessage
// cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} }
return null
} }
private suspend fun checkKeysAreDownloaded(otherUserId: String, private suspend fun checkKeysAreDownloaded(otherUserId: String,
startReq: KeyVerificationStart): MXUsersDevicesMap<MXDeviceInfo>? { startReq: VerifInfoStart): MXUsersDevicesMap<MXDeviceInfo>? {
return try { return try {
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
@ -222,17 +328,36 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
} }
} }
private suspend fun onCancelReceived(event: Event) { private fun onRoomCancelReceived(event: Event) {
val cancelReq = event.getClearContent().toModel<MessageVerificationCancelContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (cancelReq == null || cancelReq.isValid().not()) {
// ignore
Timber.e("## SAS Received invalid key request")
// TODO should we cancel?
return
}
handleOnCancel(event.senderId!!, cancelReq)
}
private fun onCancelReceived(event: Event) {
Timber.v("## SAS onCancelReceived") Timber.v("## SAS onCancelReceived")
val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()!! val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()!!
if (!cancelReq.isValid()) { if (!cancelReq.isValid()) {
// ignore // ignore
Timber.e("## Received invalid accept request") Timber.e("## SAS Received invalid accept request")
return return
} }
val otherUserId = event.senderId!! val otherUserId = event.senderId!!
handleOnCancel(otherUserId, cancelReq)
}
private fun handleOnCancel(otherUserId: String, cancelReq: VerifInfoCancel) {
Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}") Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}")
val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!) val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
if (existing == null) { if (existing == null) {
@ -245,65 +370,119 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
} }
} }
private suspend fun onAcceptReceived(event: Event) { private fun onRoomAcceptReceived(event: Event) {
val acceptReq = event.getClearContent().toModel<KeyVerificationAccept>()!! Timber.d("## SAS Received Accept via DM $event")
val accept = event.getClearContent().toModel<MessageVerificationAcceptContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
?: return
handleAccept(accept, event.senderId!!)
}
private fun onAcceptReceived(event: Event) {
Timber.d("## SAS Received Accept $event")
val acceptReq = event.getClearContent().toModel<KeyVerificationAccept>() ?: return
handleAccept(acceptReq, event.senderId!!)
}
private fun handleAccept(acceptReq: VerifInfoAccept, senderId: String) {
if (!acceptReq.isValid()) { if (!acceptReq.isValid()) {
// ignore // ignore
Timber.e("## Received invalid accept request") Timber.e("## SAS Received invalid accept request")
return return
} }
val otherUserId = event.senderId!! val otherUserId = senderId
val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!) val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!)
if (existing == null) { if (existing == null) {
Timber.e("## Received invalid accept request") Timber.e("## SAS Received invalid accept request")
return return
} }
if (existing is SASVerificationTransaction) { if (existing is SASVerificationTransaction) {
existing.acceptToDeviceEvent(otherUserId, acceptReq) existing.acceptVerificationEvent(otherUserId, acceptReq)
} else { } else {
// not other types now // not other types now
} }
} }
private suspend fun onKeyReceived(event: Event) { private fun onRoomKeyRequestReceived(event: Event) {
val keyReq = event.getClearContent().toModel<MessageVerificationKeyContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (keyReq == null || keyReq.isValid().not()) {
// ignore
Timber.e("## SAS Received invalid key request")
// TODO should we cancel?
return
}
handleKeyReceived(event, keyReq)
}
private fun onKeyReceived(event: Event) {
val keyReq = event.getClearContent().toModel<KeyVerificationKey>()!! val keyReq = event.getClearContent().toModel<KeyVerificationKey>()!!
if (!keyReq.isValid()) { if (!keyReq.isValid()) {
// ignore // ignore
Timber.e("## Received invalid key request") Timber.e("## SAS Received invalid key request")
return return
} }
handleKeyReceived(event, keyReq)
}
private fun handleKeyReceived(event: Event, keyReq: VerifInfoKey) {
Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq")
val otherUserId = event.senderId!! val otherUserId = event.senderId!!
val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!)
if (existing == null) { if (existing == null) {
Timber.e("## Received invalid accept request") Timber.e("## SAS Received invalid accept request")
return return
} }
if (existing is SASVerificationTransaction) { if (existing is SASVerificationTransaction) {
existing.acceptToDeviceEvent(otherUserId, keyReq) existing.acceptVerificationEvent(otherUserId, keyReq)
} else { } else {
// not other types now // not other types now
} }
} }
private suspend fun onMacReceived(event: Event) { private fun onRoomMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!! val macReq = event.getClearContent().toModel<MessageVerificationMacContent>()
?.copy(
if (!macReq.isValid()) { // relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (macReq == null || macReq.isValid().not() || event.senderId == null) {
// ignore // ignore
Timber.e("## Received invalid key request") Timber.e("## SAS Received invalid mac request")
// TODO should we cancel?
return return
} }
val otherUserId = event.senderId!! handleMacReceived(event.senderId, macReq)
val existing = getExistingTransaction(otherUserId, macReq.transactionID!!) }
private fun onMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
if (!macReq.isValid() || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid mac request")
return
}
handleMacReceived(event.senderId, macReq)
}
private fun handleMacReceived(senderId: String, macReq: VerifInfoMac) {
Timber.v("## SAS Received $macReq")
val existing = getExistingTransaction(senderId, macReq.transactionID!!)
if (existing == null) { if (existing == null) {
Timber.e("## Received invalid accept request") Timber.e("## SAS Received invalid accept request")
return return
} }
if (existing is SASVerificationTransaction) { if (existing is SASVerificationTransaction) {
existing.acceptToDeviceEvent(otherUserId, macReq) existing.acceptVerificationEvent(senderId, macReq)
} else { } else {
// not other types known for now // not other types known for now
} }
@ -346,13 +525,10 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
val txID = createUniqueIDForTransaction(userId, deviceID) val txID = createUniqueIDForTransaction(userId, deviceID)
// should check if already one (and cancel it) // should check if already one (and cancel it)
if (KeyVerificationStart.VERIF_METHOD_SAS == method) { if (KeyVerificationStart.VERIF_METHOD_SAS == method) {
val tx = OutgoingSASVerificationRequest( val tx = DefaultOutgoingSASVerificationRequest(
this,
setDeviceVerificationAction, setDeviceVerificationAction,
credentials, credentials,
cryptoStore, cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!, myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID, txID,
userId, userId,
@ -366,6 +542,30 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
} }
} }
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) {
requestVerificationDMTask.configureWith(
RequestVerificationDMTask.Params(
roomId = roomId,
from = credentials.deviceId ?: "",
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
to = userId,
cryptoService = cryptoService
)
) {
this.callback = object : MatrixCallback<SendResponse> {
override fun onSuccess(data: SendResponse) {
callback?.onSuccess(data.eventId)
}
override fun onFailure(failure: Throwable) {
callback?.onFailure(failure)
}
}
constraints = TaskConstraints(true)
retryCount = 3
}.executeBy(taskExecutor)
}
/** /**
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid * This string must be unique for the pair of users performing verification for the duration that the transaction is valid
*/ */
@ -390,24 +590,28 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
this.removeTransaction(tx.otherUserId, tx.transactionId) this.removeTransaction(tx.otherUserId, tx.transactionId)
} }
} }
//
fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { // fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode, roomId: String? = null) {
val cancelMessage = KeyVerificationCancel.create(transactionId, code) // val cancelMessage = KeyVerificationCancel.create(transactionId, code)
val contentMap = MXUsersDevicesMap<Any>() // val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(userId, userDevice, cancelMessage) // contentMap.setObject(userId, userDevice, cancelMessage)
//
sendToDeviceTask // if (roomId != null) {
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { //
this.callback = object : MatrixCallback<Unit> { // } else {
override fun onSuccess(data: Unit) { // sendToDeviceTask
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") // .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
} // this.callback = object : MatrixCallback<Unit> {
// override fun onSuccess(data: Unit) {
override fun onFailure(failure: Throwable) { // Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") // }
} //
} // override fun onFailure(failure: Throwable) {
} // Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
.executeBy(taskExecutor) // }
} // }
// }
// .executeBy(taskExecutor)
// }
// }
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import android.os.Build import android.os.Build
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
@ -26,13 +25,8 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.extensions.toUnsignedInt import im.vector.matrix.android.internal.extensions.toUnsignedInt
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import org.matrix.olm.OlmSAS import org.matrix.olm.OlmSAS
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
import timber.log.Timber import timber.log.Timber
@ -42,12 +36,9 @@ import kotlin.properties.Delegates
* Represents an ongoing short code interactive key verification between two devices. * Represents an ongoing short code interactive key verification between two devices.
*/ */
internal abstract class SASVerificationTransaction( internal abstract class SASVerificationTransaction(
private val sasVerificationService: DefaultSasVerificationService,
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val credentials: Credentials, open val credentials: Credentials,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor,
private val deviceFingerprint: String, private val deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserId: String, otherUserId: String,
@ -55,6 +46,8 @@ internal abstract class SASVerificationTransaction(
isIncoming: Boolean) : isIncoming: Boolean) :
VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) { VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) {
lateinit var transport: SasTransport
companion object { companion object {
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" const val SAS_MAC_SHA256 = "hkdf-hmac-sha256"
@ -95,13 +88,13 @@ internal abstract class SASVerificationTransaction(
private var olmSas: OlmSAS? = null private var olmSas: OlmSAS? = null
var startReq: KeyVerificationStart? = null var startReq: VerifInfoStart? = null
var accepted: KeyVerificationAccept? = null var accepted: VerifInfoAccept? = null
var otherKey: String? = null var otherKey: String? = null
var shortCodeBytes: ByteArray? = null var shortCodeBytes: ByteArray? = null
var myMac: KeyVerificationMac? = null var myMac: VerifInfoMac? = null
var theirMac: KeyVerificationMac? = null var theirMac: VerifInfoMac? = null
fun getSAS(): OlmSAS { fun getSAS(): OlmSAS {
if (olmSas == null) olmSas = OlmSAS() if (olmSas == null) olmSas = OlmSAS()
@ -160,7 +153,7 @@ internal abstract class SASVerificationTransaction(
return return
} }
val macMsg = KeyVerificationMac.create(transactionId, mapOf(keyId to macString), keyStrings) val macMsg = transport.createMac(transactionId, mapOf(keyId to macString), keyStrings)
myMac = macMsg myMac = macMsg
state = SasVerificationTxState.SendingMac state = SasVerificationTxState.SendingMac
sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) { sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) {
@ -176,25 +169,25 @@ internal abstract class SASVerificationTransaction(
} // if not wait for it } // if not wait for it
} }
override fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) { override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
when (event) { when (info) {
is KeyVerificationStart -> onVerificationStart(event) is VerifInfoStart -> onVerificationStart(info)
is KeyVerificationAccept -> onVerificationAccept(event) is VerifInfoAccept -> onVerificationAccept(info)
is KeyVerificationKey -> onKeyVerificationKey(senderId, event) is VerifInfoKey -> onKeyVerificationKey(senderId, info)
is KeyVerificationMac -> onKeyVerificationMac(event) is VerifInfoMac -> onKeyVerificationMac(info)
else -> { else -> {
// nop // nop
} }
} }
} }
abstract fun onVerificationStart(startReq: KeyVerificationStart) abstract fun onVerificationStart(startReq: VerifInfoStart)
abstract fun onVerificationAccept(accept: KeyVerificationAccept) abstract fun onVerificationAccept(accept: VerifInfoAccept)
abstract fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) abstract fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey)
abstract fun onKeyVerificationMac(vKey: KeyVerificationMac) abstract fun onKeyVerificationMac(vKey: VerifInfoMac)
protected fun verifyMacs() { protected fun verifyMacs() {
Timber.v("## SAS verifying macs for id:$transactionId") Timber.v("## SAS verifying macs for id:$transactionId")
@ -245,7 +238,7 @@ internal abstract class SASVerificationTransaction(
// if none of the keys could be verified, then error because the app // if none of the keys could be verified, then error because the app
// should be informed about that // should be informed about that
if (verifiedDevices.isEmpty()) { if (verifiedDevices.isEmpty()) {
Timber.e("Verification: No devices verified") Timber.e("## SAS Verification: No devices verified")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} }
@ -254,6 +247,7 @@ internal abstract class SASVerificationTransaction(
verifiedDevices.forEach { verifiedDevices.forEach {
setDeviceVerified(it, otherUserId) setDeviceVerified(it, otherUserId)
} }
transport.done(transactionId)
state = SasVerificationTxState.Verified state = SasVerificationTxState.Verified
} }
@ -270,41 +264,15 @@ internal abstract class SASVerificationTransaction(
override fun cancel(code: CancelCode) { override fun cancel(code: CancelCode) {
cancelledReason = code cancelledReason = code
state = SasVerificationTxState.Cancelled state = SasVerificationTxState.Cancelled
sasVerificationService.cancelTransaction( transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
transactionId,
otherUserId,
otherDeviceId ?: "",
code)
} }
protected fun sendToOther(type: String, protected fun sendToOther(type: String,
keyToDevice: Any, keyToDevice: VerificationInfo,
nextState: SasVerificationTxState, nextState: SasVerificationTxState,
onErrorReason: CancelCode, onErrorReason: CancelCode,
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
val contentMap = MXUsersDevicesMap<Any>() transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone)
contentMap.setObject(otherUserId, otherDeviceId, keyToDevice)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(type, contentMap, transactionId)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.")
if (onDone != null) {
onDone()
} else {
state = nextState
}
}
override fun onFailure(failure: Throwable) {
Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state")
cancel(onErrorReason)
}
}
}
.executeBy(taskExecutor)
} }
fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? {

View file

@ -0,0 +1,53 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
/**
* SAS verification can be performed using toDevice events or via DM.
* This class abstracts the concept of transport for SAS
*/
internal interface SasTransport {
/**
* Sends a message
*/
fun sendToOther(type: String,
verificationInfo: VerificationInfo,
nextState: SasVerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?)
fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode)
fun done(transactionId: String)
/**
* Creates an accept message suitable for this transport
*/
fun createAccept(tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerifInfoAccept
fun createKey(tid: String,
pubKey: String): VerifInfoKey
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerifInfoMac
}

View file

@ -0,0 +1,129 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.TaskConstraints
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber
import javax.inject.Inject
internal class SasTransportRoomMessage constructor(
private val roomId: String,
private val cryptoService: CryptoService,
// private val tx: SASVerificationTransaction?,
private val sendVerificationMessageTask: SendVerificationMessageTask,
private val taskExecutor: TaskExecutor
) : SasTransport {
override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo")
sendVerificationMessageTask.configureWith(
SendVerificationMessageTask.Params(
type,
roomId,
verificationInfo.toEventContent()!!,
cryptoService
)
) {
constraints = TaskConstraints(true)
retryCount = 3
}
.executeBy(taskExecutor)
}
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code")
sendVerificationMessageTask.configureWith(
SendVerificationMessageTask.Params(
EventType.KEY_VERIFICATION_CANCEL,
roomId,
MessageVerificationCancelContent.create(transactionId, code).toContent(),
cryptoService
)
) {
constraints = TaskConstraints(true)
retryCount = 3
callback = object : MatrixCallback<SendResponse> {
override fun onSuccess(data: SendResponse) {
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
}
}
}
.executeBy(taskExecutor)
}
override fun done(transactionId: String) {
sendVerificationMessageTask.configureWith(
SendVerificationMessageTask.Params(
EventType.KEY_VERIFICATION_DONE,
roomId,
MessageVerificationDoneContent(
relatesTo = RelationDefaultContent(
RelationType.REFERENCE,
transactionId
)
).toContent(),
cryptoService
)
) {
constraints = TaskConstraints(true)
retryCount = 3
}
.executeBy(taskExecutor)
}
override fun createAccept(tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>)
: VerifInfoAccept = MessageVerificationAcceptContent.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings)
override fun createKey(tid: String, pubKey: String): VerifInfoKey = MessageVerificationKeyContent.create(tid, pubKey)
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = MessageVerificationMacContent.create(tid, mac, keys)
}
internal class SasTransportRoomMessageFactory @Inject constructor(
private val sendVerificationMessageTask: DefaultSendVerificationMessageTask,
private val taskExecutor: TaskExecutor) {
fun createTransport(roomId: String,
cryptoService: CryptoService
// tx: SASVerificationTransaction?
): SasTransportRoomMessage {
return SasTransportRoomMessage(roomId, cryptoService, /*tx,*/ sendVerificationMessageTask, taskExecutor)
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber
import javax.inject.Inject
internal class SasTransportToDevice(
private var tx: SASVerificationTransaction?,
private var sendToDeviceTask: SendToDeviceTask,
private var taskExecutor: TaskExecutor
) : SasTransport {
override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo")
val tx = tx ?: return
val contentMap = MXUsersDevicesMap<Any>()
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
?: return Unit.also { tx.cancel() }
contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.")
if (onDone != null) {
onDone()
} else {
tx.state = nextState
}
}
override fun onFailure(failure: Throwable) {
Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state")
tx.cancel(onErrorReason)
}
}
}
.executeBy(taskExecutor)
}
override fun done(transactionId: String) {
// To device do not do anything here
}
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code")
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(userId, userDevice, cancelMessage)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
}
}
}
.executeBy(taskExecutor)
}
override fun createAccept(tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>)
: VerifInfoAccept = KeyVerificationAccept.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings)
override fun createKey(tid: String, pubKey: String): VerifInfoKey = KeyVerificationKey.create(tid, pubKey)
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = KeyVerificationMac.create(tid, mac, keys)
}
internal class SasToDeviceTransportFactory @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor) {
fun createTransport(tx: SASVerificationTransaction?): SasTransportToDevice {
return SasTransportToDevice(tx, sendToDeviceTask, taskExecutor)
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
internal interface VerifInfoAccept : VerificationInfo {
val transactionID: String?
/**
* The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device
*/
val keyAgreementProtocol: String?
/**
* The hash algorithm that Bobs device has selected to use, out of the list proposed by Alices device
*/
val hash: String?
/**
* The message authentication code that Bobs device has selected to use, out of the list proposed by Alices device
*/
val messageAuthenticationCode: String?
/**
* An array of short authentication string methods that Bobs client (and Bob) understands. Must be a subset of the list proposed by Alices device
*/
val shortAuthenticationStrings: List<String>?
/**
* The hash (encoded as unpadded base64) of the concatenation of the devices ephemeral public key (QB, encoded as unpadded base64)
* and the canonical JSON representation of the m.key.verification.start message.
*/
var commitment: String?
}
internal interface AcceptVerifInfoFactory {
fun create(tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerifInfoAccept
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
interface VerifInfoCancel : VerificationInfo {
val transactionID: String?
/**
* machine-readable reason for cancelling, see #CancelCode
*/
val code: String?
/**
* human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given.
*/
val reason: String?
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
/**
* Sent by both devices to send their ephemeral Curve25519 public key to the other device.
*/
internal interface VerifInfoKey : VerificationInfo {
val transactionID: String?
/**
* The devices ephemeral public key, as an unpadded base64 string
*/
val key: String?
}
internal interface KeyVerifInfoFactory {
fun create(tid: String, pubKey: String): VerifInfoKey
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
internal interface VerifInfoMac : VerificationInfo {
val transactionID: String?
/**
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key
*/
val mac: Map<String, String>?
/**
* The MAC of the comma-separated, sorted list of key IDs given in the mac property,
* as an unpadded base64 string, calculated using the MAC key.
* For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will
* give the MAC of the string ed25519:ABCDEFG,ed25519:HIJKLMN.
*/
val keys: String?
}
internal interface VerifInfoMacFactory {
fun create(tid: String, mac: Map<String, String>, keys: String) : VerifInfoMac
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
interface VerifInfoStart : VerificationInfo {
val method: String?
val fromDevice: String?
val transactionID: String?
val keyAgreementProtocols: List<String>?
/**
* An array of hashes that Alices client understands.
* Must include sha256. Other methods may be defined in the future.
*/
val hashes: List<String>?
/**
* An array of message authentication codes that Alices client understands.
* Must include hkdf-hmac-sha256.
* Other methods may be defined in the future.
*/
val messageAuthenticationCodes: List<String>?
/**
* An array of short authentication string methods that Alices client (and Alice) understands.
* Must include decimal.
* This document also describes the emoji method.
* Other methods may be defined in the future
*/
val shortAuthenticationStrings: List<String>?
fun toCanonicalJson(): String?
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
interface VerificationInfo {
fun toEventContent(): Content? = null
fun toSendToDeviceObject(): SendToDeviceObject? = null
fun isValid() : Boolean
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.crypto.verification
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import timber.log.Timber
import java.util.*
import javax.inject.Inject
internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String,
private val cryptoService: CryptoService,
private val sasVerificationService: DefaultSasVerificationService,
private val taskExecutor: TaskExecutor) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> {
EventEntity.types(it, listOf(
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE,
EventType.MESSAGE,
EventType.ENCRYPTED)
)
}
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
// TODO do that in a task
// TODO how to ignore when it's an initial sync?
val events = changeSet.insertions
.asSequence()
.mapNotNull { results[it]?.asDomain() }
.filterNot {
// ignore mines ^^
it.senderId == userId
}
.toList()
events.forEach { event ->
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event")
// decrypt if needed?
if (event.isEncrypted() && event.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
sasVerificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
// TODO If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver.
sasVerificationService.onRoomRequestReceived(event)
}
}
}
}
}
}

View file

@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
/** /**
* Generic interactive key verification transaction * Generic interactive key verification transaction
@ -42,7 +41,7 @@ internal abstract class VerificationTransaction(
listeners.remove(listener) listeners.remove(listener)
} }
abstract fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo)
abstract fun cancel(code: CancelCode) abstract fun cancel(code: CancelCode)
} }

View file

@ -104,7 +104,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
return Failure.ServerError(matrixError, httpCode) return Failure.ServerError(matrixError, httpCode)
} }
} catch (ex: JsonDataException) { } catch (ex: Exception) {
// This is not a MatrixError // This is not a MatrixError
Timber.w("The error returned by the server is not a MatrixError") Timber.w("The error returned by the server is not a MatrixError")
} catch (ex: JsonEncodingException) { } catch (ex: JsonEncodingException) {

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.di.*
@ -164,6 +165,10 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindVerificationEventObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver
@Binds @Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService

View file

@ -157,7 +157,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
override fun deleteFailedEcho(localEcho: TimelineEvent) { override fun deleteFailedEcho(localEcho: TimelineEvent) {
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let { TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId
?: "").findFirst()?.let {
it.deleteFromRealm() it.deleteFromRealm()
} }
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {

View file

@ -286,6 +286,24 @@ internal class LocalEchoEventFactory @Inject constructor(
) )
} }
fun createVerificationRequest(roomId: String, fromDevice: String, to: String, methods: List<String>): Event {
val localID = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localID,
type = EventType.MESSAGE,
content = MessageVerificationRequestContent(
body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId),
fromDevice = fromDevice,
to = to,
methods = methods
).toContent(),
unsignedData = UnsignedData(age = null, transactionId = localID)
)
}
private fun dummyOriginServerTs(): Long { private fun dummyOriginServerTs(): Long {
return System.currentTimeMillis() return System.currentTimeMillis()
} }

View file

@ -20,7 +20,6 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.awaitTransaction
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -28,7 +27,7 @@ internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarc
suspend fun updateSendState(eventId: String, sendState: SendState) { suspend fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("Update local state of $eventId to ${sendState.name}") Timber.v("Update local state of $eventId to ${sendState.name}")
monarchy.awaitTransaction { realm -> monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {

View file

@ -17,4 +17,7 @@
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string> <string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
<string name="no_network_indicator">There is no network connection right now</string> <string name="no_network_indicator">There is no network connection right now</string>
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
</resources> </resources>

View file

@ -207,6 +207,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsNotificationPreferenceFragment::class) @FragmentKey(VectorSettingsNotificationPreferenceFragment::class)
fun bindVectorSettingsNotificationPreferenceFragment(fragment: VectorSettingsNotificationPreferenceFragment): Fragment fun bindVectorSettingsNotificationPreferenceFragment(fragment: VectorSettingsNotificationPreferenceFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsLabsFragment::class)
fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(VectorSettingsPreferencesFragment::class) @FragmentKey(VectorSettingsPreferencesFragment::class)

View file

@ -38,7 +38,9 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick), CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown), MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler); SHRUG("/shrug", "<message>", R.string.command_description_shrug),
// TODO temporary command
VERIFY_USER("/verify", "<userID>", R.string.command_description_spoiler);
val length val length
get() = command.length + 1 get() = command.length + 1

View file

@ -244,6 +244,17 @@ object CommandParser {
ParsedCommand.SendSpoiler(message) ParsedCommand.SendSpoiler(message)
} }
Command.SHRUG.command -> {
val message = textMessage.subSequence(Command.SHRUG.command.length, textMessage.length).trim()
ParsedCommand.SendShrug(message)
}
Command.VERIFY_USER.command -> {
val message = textMessage.substring(Command.VERIFY_USER.command.length).trim()
ParsedCommand.VerifyUser(message)
}
else -> { else -> {
// Unknown command // Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand) ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -46,4 +46,6 @@ sealed class ParsedCommand {
class SetMarkdown(val enable: Boolean) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand()
object ClearScalarToken : ParsedCommand() object ClearScalarToken : ParsedCommand()
class SendSpoiler(val message: String) : ParsedCommand() class SendSpoiler(val message: String) : ParsedCommand()
class SendShrug(val message: CharSequence) : ParsedCommand()
class VerifyUser(val userId: String) : ParsedCommand()
} }

View file

@ -50,7 +50,6 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -377,6 +376,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.SendShrug -> {
val sequence: CharSequence = buildString {
append("¯\\_(ツ)_/¯")
.apply {
if (slashCommandResult.message.isNotEmpty()) {
append(" ")
append(slashCommandResult.message)
}
}
}
room.sendTextMessage(sequence)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.VerifyUser -> {
session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
is ParsedCommand.ChangeTopic -> { is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult) handleChangeTopicSlashCommand(slashCommandResult)
popDraft() popDraft()

View file

@ -64,6 +64,16 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
encryptedItemFactory.create(event, nextEvent, highlight, callback) encryptedItemFactory.create(event, nextEvent, highlight, callback)
} }
} }
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case
// Only visible in developer mode
defaultItemFactory.create(event, highlight, readMarkerVisible, callback)
}
// Unhandled event types (yet) // Unhandled event types (yet)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)

View file

@ -42,7 +42,13 @@ object TimelineDisplayableEvents {
val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
EventType.REDACTION, EventType.REDACTION,
EventType.REACTION EventType.REACTION,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_KEY
) )
} }

View file

@ -23,6 +23,7 @@ import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
@ -256,7 +257,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
fun labAllowedExtendedLogging(): Boolean { fun labAllowedExtendedLogging(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, BuildConfig.DEBUG)
} }
/** /**

View file

@ -17,14 +17,21 @@
package im.vector.riotx.features.settings package im.vector.riotx.features.settings
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.preference.VectorSwitchPreference
import javax.inject.Inject
class VectorSettingsLabsFragment : VectorSettingsBaseFragment() { class VectorSettingsLabsFragment @Inject constructor(val vectorPreferences: VectorPreferences) : VectorSettingsBaseFragment() {
override var titleRes = R.string.room_settings_labs_pref_title override var titleRes = R.string.room_settings_labs_pref_title
override val preferenceXmlRes = R.xml.vector_settings_labs override val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() { override fun bindPref() {
// Lab // Lab
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_ALLOW_EXTENDED_LOGS)?.let {
it.isChecked = vectorPreferences.labAllowedExtendedLogging()
}
// val useCryptoPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference // val useCryptoPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference
// val cryptoIsEnabledPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY) // val cryptoIsEnabledPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY)

View file

@ -1695,6 +1695,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string> <string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string>
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string> <string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
<string name="labs_enable_verification_other_dm">Enable verification other DM</string>
<string name="link_copied_to_clipboard">Link copied to clipboard</string> <string name="link_copied_to_clipboard">Link copied to clipboard</string>

View file

@ -12,6 +12,8 @@
<string name="room_list_quick_actions_leave">"Leave the room"</string> <string name="room_list_quick_actions_leave">"Leave the room"</string>
<string name="notice_member_no_changes">"%1$s made no changes"</string> <string name="notice_member_no_changes">"%1$s made no changes"</string>
<string name="command_description_spoiler">Sends the given message as a spoiler</string> <string name="command_description_spoiler">Sends the given message as a spoiler</string>
<string name="command_description_verify">Request to verify the given userID</string>
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
<string name="spoiler">Spoiler</string> <string name="spoiler">Spoiler</string>
<string name="reaction_search_type_hint">Type keywords to find a reaction.</string> <string name="reaction_search_type_hint">Type keywords to find a reaction.</string>

View file

@ -45,7 +45,6 @@
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
android:title="@string/labs_swipe_to_reply_in_timeline" /> android:title="@string/labs_swipe_to_reply_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS" android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS"