Support verification using room transport

This commit is contained in:
Valere 2019-12-01 12:44:29 +01:00
parent 853518fbb2
commit 6137a88a6f
57 changed files with 1939 additions and 288 deletions

View file

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

View file

@ -16,19 +16,42 @@
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 {
fun addListener(listener: SasVerificationListener)
fun removeListener(listener: SasVerificationListener)
/**
* Mark this device as verified manually
*/
fun markedLocallyAsManuallyVerified(userId: String, deviceID: String)
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
/**
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
* @see beginKeyVerification
*/
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 requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
// fun transactionUpdated(tx: SasVerificationTransaction)
interface SasVerificationListener {

View file

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

View file

@ -72,6 +72,7 @@ object EventType {
const val KEY_VERIFICATION_KEY = "m.key.verification.key"
const val KEY_VERIFICATION_MAC = "m.key.verification.mac"
const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel"
const val KEY_VERIFICATION_DONE = "m.key.verification.done"
// Relation Events
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_LOCATION = "m.location"
const val MSGTYPE_FILE = "m.file"
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
// 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

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
abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask
@Binds
abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask
@Binds
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
: ClaimOneTimeKeysForUsersDeviceTask

View file

@ -136,6 +136,10 @@ internal class DefaultCryptoService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope
) : CryptoService {
init {
sasVerificationService.cryptoService = this
}
private val uiHandler = Handler(Looper.getMainLooper())
// 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.JsonClass
import im.vector.matrix.android.internal.crypto.verification.VerifInfoAccept
import im.vector.matrix.android.internal.crypto.verification.AcceptVerifInfoFactory
import timber.log.Timber
/**
* Sent by Bob to accept a verification from a previously sent m.key.verification.start message.
*/
@JsonClass(generateAdapter = true)
data class KeyVerificationAccept(
internal data class KeyVerificationAccept(
/**
* 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.
*/
@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
*/
@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
*/
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
*/
@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
*/
@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)
* and the canonical JSON representation of the m.key.verification.start message.
*/
var commitment: String? = null
) : SendToDeviceObject {
@Json(name = "commitment")
override var commitment: String? = null
) : SendToDeviceObject, VerifInfoAccept {
fun isValid(): Boolean {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| keyAgreementProtocol.isNullOrBlank()
|| hash.isNullOrBlank()
@ -76,13 +80,15 @@ data class KeyVerificationAccept(
return true
}
companion object {
fun create(tid: String,
override fun toSendToDeviceObject() = this
companion object : AcceptVerifInfoFactory {
override fun create(tid: String,
keyAgreementProtocol: String,
hash: String,
commitment: String,
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): KeyVerificationAccept {
shortAuthenticationStrings: List<String>): VerifInfoAccept {
return KeyVerificationAccept().apply {
this.transactionID = tid
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.JsonClass
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.
*/
@JsonClass(generateAdapter = true)
data class KeyVerificationCancel(
internal data class KeyVerificationCancel(
/**
* the transaction ID of the verification to cancel
*/
@Json(name = "transaction_id")
var transactionID: String? = null,
override val transactionID: String? = null,
/**
* 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.
*/
var reason: String? = null
) : SendToDeviceObject {
override var reason: String? = null
) : SendToDeviceObject, VerifInfoCancel {
companion object {
fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel {
return KeyVerificationCancel().apply {
this.transactionID = tid
this.code = cancelCode.value
this.reason = cancelCode.humanReadable
}
return KeyVerificationCancel(
tid,
cancelCode.value,
cancelCode.humanReadable
)
}
}
fun isValid(): Boolean {
override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
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.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.
*/
@JsonClass(generateAdapter = true)
data class KeyVerificationKey(
internal data class KeyVerificationKey(
/**
* the ID of the transaction that the message is part of
*/
@Json(name = "transaction_id")
@JvmField
var transactionID: String? = null,
@Json(name = "transaction_id") override var transactionID: String? = null,
/**
* The devices ephemeral public key, as an unpadded base64 string
*/
@JvmField
var key: String? = null
@Json(name = "key") override val key: String? = null
) : SendToDeviceObject {
) : SendToDeviceObject, VerifInfoKey {
companion object {
fun create(tid: String, key: String): KeyVerificationKey {
return KeyVerificationKey().apply {
this.transactionID = tid
this.key = key
}
companion object : KeyVerifInfoFactory {
override fun create(tid: String, pubKey: String): KeyVerificationKey {
return KeyVerificationKey(tid, pubKey)
}
}
fun isValid(): Boolean {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || key.isNullOrBlank()) {
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.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.
*/
@JsonClass(generateAdapter = true)
data class KeyVerificationMac(
/**
* the ID of the transaction that the message is part of
*/
@Json(name = "transaction_id")
var transactionID: String? = null,
internal data class KeyVerificationMac(
@Json(name = "transaction_id") override val transactionID: String? = null,
@Json(name = "mac") override val mac: Map<String, String>? = null,
@Json(name = "key") override val keys: String? = null
/**
* 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,
) : SendToDeviceObject, VerifInfoMac {
/**
* 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 {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
return false
}
return true
}
companion object {
fun create(tid: String, mac: Map<String, String>, keys: String): KeyVerificationMac {
return KeyVerificationMac().apply {
this.transactionID = tid
this.mac = mac
this.keys = keys
}
override fun toSendToDeviceObject(): SendToDeviceObject? = this
companion object : VerifInfoMacFactory {
override fun create(tid: String, mac: Map<String, String>, keys: String): VerifInfoMac {
return KeyVerificationMac(tid, mac, 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 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.VerifInfoStart
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
/**
* Sent by Alice to initiate an interactive key verification.
*/
@JsonClass(generateAdapter = true)
class KeyVerificationStart : SendToDeviceObject {
class KeyVerificationStart : SendToDeviceObject, VerifInfoStart {
override fun toCanonicalJson(): String? {
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
}
/**
* Alices device ID
*/
@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.
@ -41,7 +47,7 @@ class KeyVerificationStart : SendToDeviceObject {
* Alices device should record this ID and use it in future messages in this transaction.
*/
@Json(name = "transaction_id")
var transactionID: String? = null
override var transactionID: String? = null
/**
* 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
*/
@Json(name = "key_agreement_protocols")
var keyAgreementProtocols: List<String>? = null
override var keyAgreementProtocols: List<String>? = null
/**
* An array of hashes that Alices client understands.
* 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.
@ -63,7 +69,7 @@ class KeyVerificationStart : SendToDeviceObject {
* Other methods may be defined in the future.
*/
@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.
@ -72,13 +78,13 @@ class KeyVerificationStart : SendToDeviceObject {
* Other methods may be defined in the future
*/
@Json(name = "short_authentication_string")
var shortAuthenticationStrings: List<String>? = null
override var shortAuthenticationStrings: List<String>? = null
companion object {
const val VERIF_METHOD_SAS = "m.sas.v1"
}
fun isValid(): Boolean {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| fromDevice.isNullOrBlank()
|| method != VERIF_METHOD_SAS
@ -95,4 +101,6 @@ class KeyVerificationStart : SendToDeviceObject {
}
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
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.session.crypto.sas.CancelCode
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.events.model.EventType
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.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
internal class IncomingSASVerificationTransaction(
private val sasVerificationService: DefaultSasVerificationService,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val credentials: Credentials,
internal class DefaultIncomingSASVerificationTransaction(
setDeviceVerificationAction: SetDeviceVerificationAction,
override val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor,
deviceFingerprint: String,
transactionId: String,
otherUserID: String)
: SASVerificationTransaction(
sasVerificationService,
otherUserID: String
) : SASVerificationTransaction(
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
deviceFingerprint,
transactionId,
otherUserID,
@ -78,10 +66,10 @@ internal class IncomingSASVerificationTransaction(
}
}
override fun onVerificationStart(startReq: KeyVerificationStart) {
Timber.v("## SAS received verification request from state $state")
override fun onVerificationStart(startReq: VerifInfoStart) {
Timber.v("## SAS I: received verification request from state $state")
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??
throw IllegalStateException("Interactive Key verification already started")
}
@ -92,7 +80,7 @@ internal class IncomingSASVerificationTransaction(
override fun performAccept() {
if (state != SasVerificationTxState.OnStarted) {
Timber.e("## Cannot perform accept from state $state")
Timber.e("## SAS Cannot perform accept from state $state")
return
}
@ -109,7 +97,7 @@ internal class IncomingSASVerificationTransaction(
if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() }
|| agreedShortCode.isNullOrEmpty()) {
// Failed to find agreement
Timber.e("## Failed to find agreement ")
Timber.e("## SAS Failed to find agreement ")
cancel(CancelCode.UnknownMethod)
return
}
@ -118,7 +106,7 @@ internal class IncomingSASVerificationTransaction(
val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId)
if (mxDeviceInfo?.fingerprint() == null) {
Timber.e("## Failed to find device key ")
Timber.e("## SAS Failed to find device key ")
// TODO force download keys!!
// would be probably better to download the keys
// for now I cancel
@ -126,7 +114,7 @@ internal class IncomingSASVerificationTransaction(
} else {
// val otherKey = info.identityKey()
// need to jump back to correct thread
val accept = KeyVerificationAccept.create(
val accept = transport.createAccept(
tid = transactionId,
keyAgreementProtocol = agreedProtocol!!,
hash = agreedHash!!,
@ -138,13 +126,13 @@ internal class IncomingSASVerificationTransaction(
}
}
private fun doAccept(accept: KeyVerificationAccept) {
private fun doAccept(accept: VerifInfoAccept) {
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,
// 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) ?: ""
// we need to send this to other device now
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")
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")
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)
return
}
@ -175,7 +163,7 @@ internal class IncomingSASVerificationTransaction(
// sending Bobs public key QB
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
state = SasVerificationTxState.SendingKey
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.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
Timber.e("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
Timber.e("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}")
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}")
}
state = SasVerificationTxState.ShortCodeReady
}
override fun onKeyVerificationMac(vKey: KeyVerificationMac) {
Timber.v("## SAS received mac for request id:$transactionId")
override fun onKeyVerificationMac(vKey: VerifInfoMac) {
Timber.v("## SAS I: received mac for request id:$transactionId")
// Check for state?
if (state != SasVerificationTxState.SendingKey
&& state != SasVerificationTxState.KeySent
@ -221,7 +211,7 @@ internal class IncomingSASVerificationTransaction(
&& state != SasVerificationTxState.ShortCodeAccepted
&& state != SasVerificationTxState.SendingMac
&& state != SasVerificationTxState.MacSent) {
Timber.e("## received key from invalid state $state")
Timber.e("## SAS I: received key from invalid state $state")
cancel(CancelCode.UnexpectedMessage)
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.events.model.EventType
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.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
internal class OutgoingSASVerificationRequest(
private val sasVerificationService: DefaultSasVerificationService,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor,
internal class DefaultOutgoingSASVerificationRequest(
setDeviceVerificationAction: SetDeviceVerificationAction,
credentials: Credentials,
cryptoStore: IMXCryptoStore,
deviceFingerprint: String,
transactionId: String,
otherUserId: String,
otherDeviceId: String)
: SASVerificationTransaction(
sasVerificationService,
otherDeviceId: String
) : SASVerificationTransaction(
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
deviceFingerprint,
transactionId,
otherUserId,
@ -78,14 +66,14 @@ internal class OutgoingSASVerificationRequest(
}
}
override fun onVerificationStart(startReq: KeyVerificationStart) {
Timber.e("## onVerificationStart - unexpected id:$transactionId")
override fun onVerificationStart(startReq: VerifInfoStart) {
Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId")
cancel(CancelCode.UnexpectedMessage)
}
fun start() {
if (state != SasVerificationTxState.None) {
Timber.e("## start verification from invalid state")
Timber.e("## SAS O: start verification from invalid state")
// should I cancel??
throw IllegalStateException("Interactive Key verification already started")
}
@ -111,10 +99,33 @@ internal class OutgoingSASVerificationRequest(
)
}
override fun onVerificationAccept(accept: KeyVerificationAccept) {
Timber.v("## onVerificationAccept id:$transactionId")
// fun request() {
// 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) {
Timber.e("## received accept request from invalid state $state")
Timber.e("## SAS O: received accept request from invalid state $state")
cancel(CancelCode.UnexpectedMessage)
return
}
@ -123,7 +134,7 @@ internal class OutgoingSASVerificationRequest(
|| !KNOWN_HASHES.contains(accept.hash)
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|| 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)
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
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
state = SasVerificationTxState.SendingKey
sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) {
@ -148,8 +159,8 @@ internal class OutgoingSASVerificationRequest(
}
}
override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) {
Timber.v("## onKeyVerificationKey id:$transactionId")
override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) {
Timber.v("## SAS O: onKeyVerificationKey id:$transactionId")
if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) {
Timber.e("## received key from invalid state $state")
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.
// check commitment
val concat = vKey.key + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!)
val concat = vKey.key + startReq!!.toCanonicalJson()
val otherCommitment = hashUsingAgreedHashMethod(concat) ?: ""
if (accepted!!.commitment.equals(otherCommitment)) {
@ -190,14 +201,14 @@ internal class OutgoingSASVerificationRequest(
}
}
override fun onKeyVerificationMac(vKey: KeyVerificationMac) {
Timber.v("## onKeyVerificationMac id:$transactionId")
override fun onKeyVerificationMac(vKey: VerifInfoMac) {
Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
if (state != SasVerificationTxState.OnKeyReceived
&& state != SasVerificationTxState.ShortCodeReady
&& state != SasVerificationTxState.ShortCodeAccepted
&& state != SasVerificationTxState.SendingMac
&& state != SasVerificationTxState.MacSent) {
Timber.e("## received key from invalid state $state")
Timber.e("## SAS O: received key from invalid state $state")
cancel(CancelCode.UnexpectedMessage)
return
}

View file

@ -21,6 +21,7 @@ import android.os.Looper
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
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.SasVerificationService
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.EventType
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.MyDeviceInfoHolder
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.rest.*
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.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.configureWith
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.Exception
import java.util.UUID
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
/**
* 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).
*/
import kotlin.collections.set
@SessionScope
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 setDeviceVerificationAction: SetDeviceVerificationAction,
private val sendToDeviceTask: SendToDeviceTask,
private val requestVerificationDMTask: DefaultRequestVerificationDMTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
private val sasToDeviceTransportFactory: SasToDeviceTransportFactory,
private val taskExecutor: TaskExecutor)
: VerificationTransaction.Listener, SasVerificationService {
private val uiHandler = Handler(Looper.getMainLooper())
// Cannot be injected in constructor as it creates a dependency cycle
lateinit var cryptoService: CryptoService
// map [sender : [transaction]]
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>()
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) {
Timber.e("## SAS received Start request ${event.eventId}")
val startReq = event.getClearContent().toModel<KeyVerificationStart>()!!
Timber.v("## SAS received Start request $startReq")
val otherUserId = event.senderId
if (!startReq.isValid()) {
Timber.e("## received invalid verification request")
Timber.e("## SAS received invalid verification request")
if (startReq.transactionID != null) {
cancelTransaction(
startReq.transactionID!!,
// cancelTransaction(
// startReq.transactionID!!,
// otherUserId!!,
// startReq.fromDevice ?: event.getSenderKey()!!,
// CancelCode.UnknownMethod
// )
sasToDeviceTransportFactory.createTransport(null).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod
@ -167,8 +256,22 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
return
}
// 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) {
Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}")
Timber.v("## SAS onStartRequestReceived $startReq")
val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId)
@ -176,43 +279,46 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
// should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
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) {
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.
existingTxs.forEach {
it.cancel(CancelCode.UnexpectedMessage)
}
cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
return CancelCode.UnexpectedMessage
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else {
// Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
val tx = IncomingSASVerificationTransaction(
this,
val tx = DefaultIncomingSASVerificationTransaction(
// this,
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId)
otherUserId).also { txConfigure(it) }
addTransaction(tx)
tx.acceptToDeviceEvent(otherUserId, startReq)
tx.acceptVerificationEvent(otherUserId, startReq)
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
return CancelCode.UnknownMethod
// cancelTransaction(tid, otherUserId, startReq.fromDevice
// ?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
} 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,
startReq: KeyVerificationStart): MXUsersDevicesMap<MXDeviceInfo>? {
startReq: VerifInfoStart): MXUsersDevicesMap<MXDeviceInfo>? {
return try {
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
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")
val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()!!
if (!cancelReq.isValid()) {
// ignore
Timber.e("## Received invalid accept request")
Timber.e("## SAS Received invalid accept request")
return
}
val otherUserId = event.senderId!!
handleOnCancel(otherUserId, cancelReq)
}
private fun handleOnCancel(otherUserId: String, cancelReq: VerifInfoCancel) {
Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}")
val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
if (existing == null) {
@ -245,65 +370,119 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
}
}
private suspend fun onAcceptReceived(event: Event) {
val acceptReq = event.getClearContent().toModel<KeyVerificationAccept>()!!
private fun onRoomAcceptReceived(event: Event) {
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()) {
// ignore
Timber.e("## Received invalid accept request")
Timber.e("## SAS Received invalid accept request")
return
}
val otherUserId = event.senderId!!
val otherUserId = senderId
val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!)
if (existing == null) {
Timber.e("## Received invalid accept request")
Timber.e("## SAS Received invalid accept request")
return
}
if (existing is SASVerificationTransaction) {
existing.acceptToDeviceEvent(otherUserId, acceptReq)
existing.acceptVerificationEvent(otherUserId, acceptReq)
} else {
// 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>()!!
if (!keyReq.isValid()) {
// ignore
Timber.e("## Received invalid key request")
Timber.e("## SAS Received invalid key request")
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 existing = getExistingTransaction(otherUserId, keyReq.transactionID!!)
if (existing == null) {
Timber.e("## Received invalid accept request")
Timber.e("## SAS Received invalid accept request")
return
}
if (existing is SASVerificationTransaction) {
existing.acceptToDeviceEvent(otherUserId, keyReq)
existing.acceptVerificationEvent(otherUserId, keyReq)
} else {
// not other types now
}
}
private suspend fun onMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
if (!macReq.isValid()) {
private fun onRoomMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<MessageVerificationMacContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (macReq == null || macReq.isValid().not() || event.senderId == null) {
// ignore
Timber.e("## Received invalid key request")
Timber.e("## SAS Received invalid mac request")
// TODO should we cancel?
return
}
val otherUserId = event.senderId!!
val existing = getExistingTransaction(otherUserId, macReq.transactionID!!)
handleMacReceived(event.senderId, macReq)
}
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) {
Timber.e("## Received invalid accept request")
Timber.e("## SAS Received invalid accept request")
return
}
if (existing is SASVerificationTransaction) {
existing.acceptToDeviceEvent(otherUserId, macReq)
existing.acceptVerificationEvent(senderId, macReq)
} else {
// not other types known for now
}
@ -346,13 +525,10 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
val txID = createUniqueIDForTransaction(userId, deviceID)
// should check if already one (and cancel it)
if (KeyVerificationStart.VERIF_METHOD_SAS == method) {
val tx = OutgoingSASVerificationRequest(
this,
val tx = DefaultOutgoingSASVerificationRequest(
setDeviceVerificationAction,
credentials,
cryptoStore,
sendToDeviceTask,
taskExecutor,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID,
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
*/
@ -390,24 +590,28 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
this.removeTransaction(tx.otherUserId, tx.transactionId)
}
}
fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
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)
}
//
// fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode, roomId: String? = null) {
// val cancelMessage = KeyVerificationCancel.create(transactionId, code)
// val contentMap = MXUsersDevicesMap<Any>()
// contentMap.setObject(userId, userDevice, cancelMessage)
//
// if (roomId != null) {
//
// } else {
// 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)
// }
// }
}

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.verification
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.session.crypto.sas.CancelCode
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.model.MXDeviceInfo
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.tasks.SendToDeviceTask
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.OlmUtility
import timber.log.Timber
@ -42,12 +36,9 @@ import kotlin.properties.Delegates
* Represents an ongoing short code interactive key verification between two devices.
*/
internal abstract class SASVerificationTransaction(
private val sasVerificationService: DefaultSasVerificationService,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val credentials: Credentials,
open val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor,
private val deviceFingerprint: String,
transactionId: String,
otherUserId: String,
@ -55,6 +46,8 @@ internal abstract class SASVerificationTransaction(
isIncoming: Boolean) :
VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) {
lateinit var transport: SasTransport
companion object {
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
const val SAS_MAC_SHA256 = "hkdf-hmac-sha256"
@ -95,13 +88,13 @@ internal abstract class SASVerificationTransaction(
private var olmSas: OlmSAS? = null
var startReq: KeyVerificationStart? = null
var accepted: KeyVerificationAccept? = null
var startReq: VerifInfoStart? = null
var accepted: VerifInfoAccept? = null
var otherKey: String? = null
var shortCodeBytes: ByteArray? = null
var myMac: KeyVerificationMac? = null
var theirMac: KeyVerificationMac? = null
var myMac: VerifInfoMac? = null
var theirMac: VerifInfoMac? = null
fun getSAS(): OlmSAS {
if (olmSas == null) olmSas = OlmSAS()
@ -160,7 +153,7 @@ internal abstract class SASVerificationTransaction(
return
}
val macMsg = KeyVerificationMac.create(transactionId, mapOf(keyId to macString), keyStrings)
val macMsg = transport.createMac(transactionId, mapOf(keyId to macString), keyStrings)
myMac = macMsg
state = SasVerificationTxState.SendingMac
sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) {
@ -176,25 +169,25 @@ internal abstract class SASVerificationTransaction(
} // if not wait for it
}
override fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) {
when (event) {
is KeyVerificationStart -> onVerificationStart(event)
is KeyVerificationAccept -> onVerificationAccept(event)
is KeyVerificationKey -> onKeyVerificationKey(senderId, event)
is KeyVerificationMac -> onKeyVerificationMac(event)
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
when (info) {
is VerifInfoStart -> onVerificationStart(info)
is VerifInfoAccept -> onVerificationAccept(info)
is VerifInfoKey -> onKeyVerificationKey(senderId, info)
is VerifInfoMac -> onKeyVerificationMac(info)
else -> {
// 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() {
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
// should be informed about that
if (verifiedDevices.isEmpty()) {
Timber.e("Verification: No devices verified")
Timber.e("## SAS Verification: No devices verified")
cancel(CancelCode.MismatchedKeys)
return
}
@ -254,6 +247,7 @@ internal abstract class SASVerificationTransaction(
verifiedDevices.forEach {
setDeviceVerified(it, otherUserId)
}
transport.done(transactionId)
state = SasVerificationTxState.Verified
}
@ -270,41 +264,15 @@ internal abstract class SASVerificationTransaction(
override fun cancel(code: CancelCode) {
cancelledReason = code
state = SasVerificationTxState.Cancelled
sasVerificationService.cancelTransaction(
transactionId,
otherUserId,
otherDeviceId ?: "",
code)
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
}
protected fun sendToOther(type: String,
keyToDevice: Any,
keyToDevice: VerificationInfo,
nextState: SasVerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
val contentMap = MXUsersDevicesMap<Any>()
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)
transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone)
}
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.SasVerificationTransaction
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
/**
* Generic interactive key verification transaction
@ -42,7 +41,7 @@ internal abstract class VerificationTransaction(
listeners.remove(listener)
}
abstract fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject)
abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo)
abstract fun cancel(code: CancelCode)
}

View file

@ -104,7 +104,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
return Failure.ServerError(matrixError, httpCode)
}
} catch (ex: JsonDataException) {
} catch (ex: Exception) {
// This is not a MatrixError
Timber.w("The error returned by the server is not a MatrixError")
} 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.homeserver.HomeServerCapabilitiesService
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.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.*
@ -164,6 +165,10 @@ internal abstract class SessionModule {
@IntoSet
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindVerificationEventObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver
@Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService

View file

@ -157,7 +157,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
override fun deleteFailedEcho(localEcho: TimelineEvent) {
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()
}
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 {
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.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
@ -28,7 +27,7 @@ internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarc
suspend fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("Update local state of $eventId to ${sendState.name}")
monarchy.awaitTransaction { realm ->
monarchy.writeAsync { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) {
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="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>

View file

@ -207,6 +207,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsNotificationPreferenceFragment::class)
fun bindVectorSettingsNotificationPreferenceFragment(fragment: VectorSettingsNotificationPreferenceFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsLabsFragment::class)
fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment
@Binds
@IntoMap
@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),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
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
get() = command.length + 1

View file

@ -244,6 +244,17 @@ object CommandParser {
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 -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -46,4 +46,6 @@ sealed class ParsedCommand {
class SetMarkdown(val enable: Boolean) : ParsedCommand()
object ClearScalarToken : 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.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
@ -377,6 +376,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
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 -> {
handleChangeTopicSlashCommand(slashCommandResult)
popDraft()

View file

@ -64,6 +64,16 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
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)
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(
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 androidx.core.content.edit
import androidx.preference.PreferenceManager
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.themes.ThemeUtils
@ -256,7 +257,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
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
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 val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() {
// 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 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="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>

View file

@ -12,6 +12,8 @@
<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="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="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:title="@string/labs_swipe_to_reply_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS"