Support .verification.ready event

This commit is contained in:
Valere 2019-12-26 16:59:36 +01:00
parent 308b15b908
commit 4c0cbca4cb
17 changed files with 319 additions and 35 deletions

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.crypto.sas
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
/**
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
@ -39,6 +40,8 @@ interface SasVerificationService {
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
/**
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
* @see beginKeyVerification
@ -50,7 +53,7 @@ interface SasVerificationService {
*/
fun beginKeyVerification(method: String, userId: String, deviceID: String): String?
fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) : PendingVerificationRequest
fun beginKeyVerificationInDMs(method: String,
transactionId: String,
@ -59,13 +62,16 @@ interface SasVerificationService {
otherDeviceId: String,
callback: MatrixCallback<String>?): String?
fun readyPendingVerificationInDMs(transactionId: String)
fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String)
// fun transactionUpdated(tx: SasVerificationTransaction)
interface SasVerificationListener {
fun transactionCreated(tx: SasVerificationTransaction)
fun transactionUpdated(tx: SasVerificationTransaction)
fun markedAsManuallyVerified(userId: String, deviceId: String)
fun markedAsManuallyVerified(userId: String, deviceId: String) {}
fun verificationRequestCreated(pr: PendingVerificationRequest) {}
fun verificationRequestUpdated(pr: PendingVerificationRequest) {}
}
}

View File

@ -89,4 +89,6 @@ interface RoomService {
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
}

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.MessageVerificationReadyFactory
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady
@JsonClass(generateAdapter = true)
internal data class MessageVerificationReadyContent(
@Json(name = "from_device") override val fromDevice: String? = null,
@Json(name = "methods") override val methods: List<String>? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoReady {
override val transactionID: String?
get() = relatesTo?.eventId
override fun toEventContent() = this.toContent()
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) {
return false
}
return true
}
companion object : MessageVerificationReadyFactory {
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
return MessageVerificationReadyContent(
fromDevice = fromDevice,
methods = methods,
relatesTo = RelationDefaultContent(
RelationType.REFERENCE,
tid
)
)
}
}
}

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.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady
/**
* Requests a key verification with another user's devices.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationReady(
@Json(name = "from_device") override val fromDevice: String?,
//TODO add qr?
@Json(name = "methods") override val methods: List<String>? = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
@Json(name = "transaction_id") override var transactionID: String? = null
) : SendToDeviceObject, VerificationInfoReady {
override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty()
}
}

View File

@ -43,6 +43,7 @@ data class KeyVerificationStart(
companion object {
const val VERIF_METHOD_SAS = "m.sas.v1"
const val VERIF_METHOD_SCAN = "m.qr_code.scan.v1"
}
override fun isValid(): Boolean {

View File

@ -33,7 +33,8 @@ internal class DefaultIncomingSASVerificationTransaction(
private val cryptoStore: IMXCryptoStore,
deviceFingerprint: String,
transactionId: String,
otherUserID: String
otherUserID: String,
val autoAccept: Boolean = false
) : SASVerificationTransaction(
setDeviceVerificationAction,
credentials,
@ -76,6 +77,10 @@ internal class DefaultIncomingSASVerificationTransaction(
this.startReq = startReq
state = SasVerificationTxState.OnStarted
this.otherDeviceId = startReq.fromDevice
if (autoAccept) {
performAccept()
}
}
override fun performAccept() {

View File

@ -78,12 +78,8 @@ internal class DefaultSasVerificationService @Inject constructor(
/**
* Map [sender: [PendingVerificationRequest]]
*/
private val incomingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
private val pendingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
/**
* Map [sender: [PendingVerificationRequest]]
*/
private val outgoingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
// Event received from the sync
fun onToDeviceEvent(event: Event) {
@ -130,6 +126,9 @@ internal class DefaultSasVerificationService @Inject constructor(
EventType.KEY_VERIFICATION_MAC -> {
onRoomMacReceived(event)
}
EventType.KEY_VERIFICATION_READY -> {
onRoomReadyReceived(event)
}
EventType.KEY_VERIFICATION_DONE -> {
// TODO?
}
@ -185,6 +184,31 @@ internal class DefaultSasVerificationService @Inject constructor(
}
}
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
uiHandler.post {
listeners.forEach {
try {
it.verificationRequestCreated(tx)
} catch (e: Throwable) {
Timber.e(e, "## Error while notifying listeners")
}
}
}
}
private fun dispatchRequestUpdated(tx: PendingVerificationRequest) {
uiHandler.post {
listeners.forEach {
try {
it.verificationRequestUpdated(tx)
} catch (e: Throwable) {
Timber.e(e, "## Error while notifying listeners")
}
}
}
}
override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
deviceID,
@ -204,13 +228,21 @@ internal class DefaultSasVerificationService @Inject constructor(
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
?: return
val senderId = event.senderId ?: return
if (requestInfo.toUserId != credentials.userId) {
//I should ignore this, it's not for me
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
return
}
// Remember this request
val requestsForUser = incomingRequests[senderId]
val requestsForUser = pendingRequests[senderId]
?: ArrayList<PendingVerificationRequest>().also {
incomingRequests[event.senderId] = it
pendingRequests[event.senderId] = it
}
val pendingVerificationRequest = PendingVerificationRequest(
isIncoming = true,
otherUserId = senderId,//requestInfo.toUserId,
transactionId = event.eventId,
requestInfo = requestInfo
)
@ -320,6 +352,10 @@ internal class DefaultSasVerificationService @Inject constructor(
// Ok we can create
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
// If there is a corresponding request, we can auto accept
// as we are the one requesting in first place (or we accepted the request)
val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID }
?: false
val tx = DefaultIncomingSASVerificationTransaction(
// this,
setDeviceVerificationAction,
@ -327,7 +363,8 @@ internal class DefaultSasVerificationService @Inject constructor(
cryptoStore,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
otherUserId).also { txConfigure(it) }
otherUserId,
autoAccept).also { txConfigure(it) }
addTransaction(tx)
tx.acceptVerificationEvent(otherUserId, startReq)
} else {
@ -490,6 +527,21 @@ internal class DefaultSasVerificationService @Inject constructor(
handleMacReceived(event.senderId, macReq)
}
private fun onRoomReadyReceived(event: Event) {
val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>()
?.copy(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid ready request")
// TODO should we cancel?
return
}
handleReadyReceived(event.senderId, readyReq)
}
private fun onMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
@ -515,12 +567,27 @@ internal class DefaultSasVerificationService @Inject constructor(
}
}
private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID }
if (existingRequest == null) {
Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}")
return
}
updateOutgoingPendingRequest(existingRequest.copy(readyInfo = readyReq))
}
override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? {
synchronized(lock = txMap) {
return txMap[otherUser]?.get(tid)
}
}
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
synchronized(lock = pendingRequests) {
return pendingRequests[otherUser]
}
}
private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
synchronized(txMap) {
return txMap[otherUser]?.values
@ -534,13 +601,11 @@ internal class DefaultSasVerificationService @Inject constructor(
}
private fun addTransaction(tx: VerificationTransaction) {
tx.otherUserId.let { otherUserId ->
synchronized(txMap) {
val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() }
txInnerMap[tx.transactionId] = tx
dispatchTxAdded(tx)
tx.addListener(this)
}
synchronized(txMap) {
val txInnerMap = txMap.getOrPut(tx.otherUserId) { HashMap() }
txInnerMap[tx.transactionId] = tx
dispatchTxAdded(tx)
tx.addListener(this)
}
}
@ -570,12 +635,14 @@ internal class DefaultSasVerificationService @Inject constructor(
}
}
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) {
val requestsForUser = outgoingRequests[userId]
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
: PendingVerificationRequest {
Timber.i("Requesting verification to user: $userId in room ${roomId}")
val requestsForUser = pendingRequests[userId]
?: ArrayList<PendingVerificationRequest>().also {
outgoingRequests[userId] = it
pendingRequests[userId] = it
}
val params = requestVerificationDMTask.createParamsAndLocalEcho(
roomId = roomId,
from = credentials.deviceId ?: "",
@ -583,13 +650,21 @@ internal class DefaultSasVerificationService @Inject constructor(
to = userId,
cryptoService = cryptoService
)
val verificationRequest = PendingVerificationRequest(
isIncoming = false,
localID = params.event.eventId ?: "",
otherUserId = userId
)
requestsForUser.add(verificationRequest)
dispatchRequestAdded(verificationRequest)
requestVerificationDMTask.configureWith(
params
) {
this.callback = object : MatrixCallback<SendResponse> {
override fun onSuccess(data: SendResponse) {
params.event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
requestsForUser.add(PendingVerificationRequest(
updateOutgoingPendingRequest(verificationRequest.copy(
transactionId = data.eventId,
requestInfo = it
))
@ -604,6 +679,24 @@ internal class DefaultSasVerificationService @Inject constructor(
constraints = TaskConstraints(true)
retryCount = 3
}.executeBy(taskExecutor)
return verificationRequest
}
private fun updateOutgoingPendingRequest(updated: PendingVerificationRequest) {
val requestsForUser = pendingRequests[updated.otherUserId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[updated.otherUserId] = it
}
val index = requestsForUser.indexOfFirst {
it.transactionId == updated.transactionId
|| it.transactionId == null && it.localID == updated.localID
}
if (index != -1) {
requestsForUser.removeAt(index)
}
requestsForUser.add(updated)
dispatchRequestUpdated(updated)
}
override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String,
@ -628,9 +721,27 @@ internal class DefaultSasVerificationService @Inject constructor(
}
}
override fun readyPendingVerificationInDMs(transactionId: String) {
//
override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String) {
// Let's find the related request
getExistingVerificationRequest(otherUserId)?.find { it.transactionId == transactionId }?.let {
//we need to send a ready event, with matching methods
val transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService, null)
val methods = it.requestInfo?.methods?.intersect(listOf(KeyVerificationStart.VERIF_METHOD_SAS))?.toList()
if (methods.isNullOrEmpty()) {
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
return@let
}
//TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods)
transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg,
SasVerificationTxState.None,
CancelCode.User,
null // TODO handle error?
)
updateOutgoingPendingRequest(it.copy(readyInfo = readyMsg))
}
}
/**
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
*/

View File

@ -16,12 +16,20 @@
package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import java.util.*
/**
* Stores current pending verification requests
*/
internal data class PendingVerificationRequest(
val transactionId: String?,
val requestInfo: MessageVerificationRequestContent?,
data class PendingVerificationRequest(
val isIncoming: Boolean = false,
val localID: String = UUID.randomUUID().toString(),
val otherUserId: String,
val transactionId: String? = null,
val requestInfo: MessageVerificationRequestContent? = null,
val readyInfo: VerificationInfoReady? = null
)
) {
val isReady: Boolean = readyInfo != null
val isSent: Boolean = transactionId != null
}

View File

@ -58,4 +58,7 @@ internal interface SasTransport {
shortAuthenticationStrings: List<String>) : VerificationInfoStart
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady
}

View File

@ -167,6 +167,17 @@ internal class SasTransportRoomMessage(
)
)
}
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
return MessageVerificationReadyContent(
fromDevice = fromDevice,
relatesTo = RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = tid
),
methods = methods
)
}
}
internal class SasTransportRoomMessageFactory @Inject constructor(

View File

@ -126,6 +126,14 @@ internal class SasTransportToDevice(
messageAuthenticationCodes,
shortAuthenticationStrings)
}
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
return KeyVerificationReady(
transactionID = tid,
fromDevice = fromDevice,
methods = methods
)
}
}
internal class SasTransportToDeviceFactory @Inject constructor(

View File

@ -18,7 +18,7 @@ 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
internal interface VerificationInfo {
interface VerificationInfo {
fun toEventContent(): Content? = null
fun toSendToDeviceObject(): SendToDeviceObject? = null
fun isValid() : Boolean

View File

@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.verification
* The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly
* with a m.key.verification.start event instead.
*/
internal interface VerificationInfoReady : VerificationInfo {
interface VerificationInfoReady : VerificationInfo {
val transactionID: String?
@ -36,3 +36,7 @@ internal interface VerificationInfoReady : VerificationInfo {
*/
val methods: List<String>?
}
internal interface MessageVerificationReadyFactory {
fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady
}

View File

@ -56,6 +56,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_READY,
EventType.MESSAGE,
EventType.ENCRYPTED)
)
@ -141,6 +142,14 @@ internal class VerificationMessageLiveObserver @Inject constructor(
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_READY == event.type) {
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
transactionsHandledByOtherDevice.remove(it)
@ -162,6 +171,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> {
sasVerificationService.onRoomEvent(event)
}

View File

@ -86,6 +86,20 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
})
}
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val roomId = RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.findAll()?.let { dms ->
dms.firstOrNull {
it.otherMemberIds.contains(otherUserId)
}
}
?.roomId ?: return null
return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) }
}
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm ->

View File

@ -49,14 +49,16 @@ enum class VerificationState {
UNKNOWN,
REQUEST,
WAITING,
READY,
CANCELED_BY_ME,
CANCELED_BY_OTHER,
DONE
}
fun VerificationState.isCanceled() : Boolean {
fun VerificationState.isCanceled(): Boolean {
return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
}
/**
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/
@ -119,6 +121,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY -> {
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
@ -459,6 +462,9 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
EventType.KEY_VERIFICATION_DONE -> {
updateVerificationState(currentState, VerificationState.DONE)
}
EventType.KEY_VERIFICATION_READY -> {
updateVerificationState(currentState, VerificationState.READY)
}
else -> VerificationState.REQUEST
}
@ -474,7 +480,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
}
private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState) : VerificationState {
private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState): VerificationState {
// Cancel is always prioritary ?
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
// consider as canceled

View File

@ -30,7 +30,7 @@
<!-- This part is inserted in verify_by_scanning_description-->
<string name="verify_open_camera_link">open your camera</string>
<string name="verify_by_emoji_title">Verify by emoji</string>
<string name="verify_by_emoji_title">Verify by Emoji</string>
<string name="verify_by_emoji_description">If you cant scan the code above, verify by comparing a short, unique selection of emoji.</string>
<string name="aria_qr_code_description">QR code image</string>