To Device Verification Request

This commit is contained in:
Benoit Marty 2020-01-30 18:43:50 +01:00
parent 069bd3c258
commit 225e4e0433
11 changed files with 194 additions and 66 deletions

View File

@ -47,7 +47,10 @@ interface VerificationService {
fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest?
fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String?
fun beginKeyVerification(method: VerificationMethod,
otherUserId: String,
otherDeviceId: String,
transactionId: String?): String?
/**
* Request a key verification from another user using toDevice events.
@ -66,7 +69,10 @@ interface VerificationService {
otherDevices: List<String>?
): PendingVerificationRequest
fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String)
fun declineVerificationRequestInDMs(otherUserId: String,
otherDeviceId: String,
transactionId: String,
roomId: String)
// Only SAS method is supported for the moment
fun beginKeyVerificationInDMs(method: VerificationMethod,
@ -84,6 +90,13 @@ interface VerificationService {
roomId: String,
transactionId: String): Boolean
/**
* Returns false if the request is unknown
*/
fun readyPendingVerification(methods: List<VerificationMethod>,
otherUserId: String,
transactionId: String): Boolean
// fun transactionUpdated(tx: SasVerificationTransaction)
interface VerificationListener {

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReq
internal data class KeyVerificationRequest(
@Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>,
@Json(name = "methods") override val timestamp: Long?,
@Json(name = "timestamp") override val timestamp: Long?,
@Json(name = "transaction_id") override var transactionID: String? = null
) : SendToDeviceObject, VerificationInfoRequest {

View File

@ -55,6 +55,7 @@ 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.model.rest.KeyVerificationReady
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationRequest
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
@ -125,6 +126,9 @@ internal class DefaultVerificationService @Inject constructor(
EventType.KEY_VERIFICATION_MAC -> {
onMacReceived(event)
}
EventType.KEY_VERIFICATION_READY -> {
onReadyReceived(event)
}
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
onRequestReceived(event)
}
@ -263,7 +267,6 @@ internal class DefaultVerificationService @Inject constructor(
}
}
private fun onRequestReceived(event: Event) {
val requestInfo = event.getClearContent().toModel<KeyVerificationRequest>()!!
@ -279,7 +282,7 @@ internal class DefaultVerificationService @Inject constructor(
GlobalScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
Timber.e("## Verification device $otherDeviceId is not knwon")
Timber.e("## Verification device $otherDeviceId is not known")
}
}
@ -294,14 +297,13 @@ internal class DefaultVerificationService @Inject constructor(
isIncoming = true,
otherUserId = senderId, // requestInfo.toUserId,
roomId = null,
transactionId = event.eventId,
transactionId = requestInfo.transactionID,
requestInfo = requestInfo
)
requestsForUser.add(pendingVerificationRequest)
dispatchRequestAdded(pendingVerificationRequest)
}
suspend fun onRoomRequestReceived(event: Event) {
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
@ -318,7 +320,7 @@ internal class DefaultVerificationService @Inject constructor(
// We don't want to block here
GlobalScope.launch {
if (checkKeysAreDownloaded(senderId, fromDevice) == null) {
Timber.e("## SAS Verification device $fromDevice is not knwon")
Timber.e("## SAS Verification device $fromDevice is not known")
}
}
@ -677,7 +679,29 @@ internal class DefaultVerificationService @Inject constructor(
return
}
handleReadyReceived(event.senderId, event.roomId!!, readyReq)
handleReadyReceived(event.senderId, readyReq) {
verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it)
}
}
private suspend fun onReadyReceived(event: Event) {
val readyReq = event.getClearContent().toModel<KeyVerificationReady>()
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid ready request")
// TODO should we cancel?
return
}
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known")
// TODO cancel?
return
}
handleReadyReceived(event.senderId, readyReq) {
verificationTransportToDeviceFactory.createTransport(it)
}
}
private fun onRoomDoneReceived(event: Event) {
@ -722,7 +746,9 @@ internal class DefaultVerificationService @Inject constructor(
}
}
private fun handleReadyReceived(senderId: String, roomId: String, readyReq: VerificationInfoReady) {
private fun handleReadyReceived(senderId: String,
readyReq: VerificationInfoReady,
transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) {
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}")
@ -750,7 +776,7 @@ internal class DefaultVerificationService @Inject constructor(
deviceId ?: "",
false)
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
tx.transport = transportCreator.invoke(tx)
addTransaction(tx)
}
@ -973,8 +999,8 @@ internal class DefaultVerificationService @Inject constructor(
}
}
override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String? {
val txID = createUniqueIDForTransaction(otherUserId, otherDeviceID)
override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? {
val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId)
// should check if already one (and cancel it)
if (method == VerificationMethod.SAS) {
val tx = DefaultOutgoingSASDefaultVerificationTransaction(
@ -986,7 +1012,7 @@ internal class DefaultVerificationService @Inject constructor(
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID,
otherUserId,
otherDeviceID)
otherDeviceId)
tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
addTransaction(tx)
@ -1190,9 +1216,10 @@ internal class DefaultVerificationService @Inject constructor(
transactionId,
otherUserId,
existingRequest.requestInfo?.fromDevice ?: "",
roomId,
existingRequest.requestInfo?.methods,
methods)
methods) {
verificationTransportRoomMessageFactory.createTransport(roomId, it)
}
if (methods.isNullOrEmpty()) {
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
// TODO buttons should not be shown in this case?
@ -1215,13 +1242,52 @@ internal class DefaultVerificationService @Inject constructor(
}
}
override fun readyPendingVerification(methods: List<VerificationMethod>,
otherUserId: String,
transactionId: String): Boolean {
Timber.v("## SAS readyPendingVerification $otherUserId tx:$transactionId")
// Let's find the related request
val existingRequest = getExistingVerificationRequest(otherUserId, transactionId)
if (existingRequest != null) {
// we need to send a ready event, with matching methods
val transport = verificationTransportToDeviceFactory.createTransport(null)
val computedMethods = computeReadyMethods(
transactionId,
otherUserId,
existingRequest.requestInfo?.fromDevice ?: "",
existingRequest.requestInfo?.methods,
methods) {
verificationTransportToDeviceFactory.createTransport(it)
}
if (methods.isNullOrEmpty()) {
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
// TODO buttons should not be shown in this case?
return false
}
// TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods)
transport.sendVerificationReady(
readyMsg,
otherUserId,
existingRequest.requestInfo?.fromDevice ?: "",
null // TODO handle error?
)
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg))
return true
} else {
Timber.e("## SAS readyPendingVerification Verification not found")
// :/ should not be possible... unless live observer very slow
return false
}
}
private fun computeReadyMethods(
transactionId: String,
otherUserId: String,
otherDeviceId: String,
roomId: String,
otherUserMethods: List<String>?,
methods: List<VerificationMethod>): List<String> {
methods: List<VerificationMethod>,
transportCreator: (DefaultVerificationTransaction) -> VerificationTransport): List<String> {
if (otherUserMethods.isNullOrEmpty()) {
return emptyList()
}
@ -1264,7 +1330,7 @@ internal class DefaultVerificationService @Inject constructor(
deviceId ?: "",
false)
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
tx.transport = transportCreator.invoke(tx)
addTransaction(tx)
}

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.VerificationMethod
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
@ -40,7 +39,7 @@ data class PendingVerificationRequest(
val isSuccessful: Boolean = false,
val handledByOtherSession: Boolean = false,
// In case of to device it is sent to a list of devices
val targetDevices: List<String> ? = null
val targetDevices: List<String>? = null
) {
val isReady: Boolean = readyInfo != null
val isSent: Boolean = transactionId != null

View File

@ -80,4 +80,10 @@ internal interface VerificationTransport {
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady
// TODO Refactor
fun sendVerificationReady(keyReq: VerificationInfoReady,
otherUserId: String,
otherDeviceId: String,
callback: (() -> Unit)?)
}

View File

@ -332,6 +332,13 @@ internal class VerificationTransportRoomMessage(
localEchoEventFactory.saveLocalEcho(monarchy, it)
}
}
override fun sendVerificationReady(keyReq: VerificationInfoReady,
otherUserId: String,
otherDeviceId: String,
callback: (() -> Unit)?) {
// Not applicable
}
}
internal class VerificationTransportRoomMessageFactory @Inject constructor(

View File

@ -77,6 +77,30 @@ internal class VerificationTransportToDevice(
.executeBy(taskExecutor)
}
override fun sendVerificationReady(keyReq: VerificationInfoReady,
otherUserId: String,
otherDeviceId: String,
callback: (() -> Unit)?) {
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(otherUserId, otherDeviceId, keyReq)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_READY, contentMap)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## verification [$tx.transactionId] send toDevice request success")
callback?.invoke()
}
override fun onFailure(failure: Throwable) {
Timber.e("## verification [$tx.transactionId] failed to send toDevice request")
}
}
}
.executeBy(taskExecutor)
}
override fun sendToOther(type: String,
verificationInfo: VerificationInfo,
nextState: VerificationTxState,

View File

@ -135,7 +135,16 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
.apply {
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.openRoom(it, pr.roomId ?: "", pr.transactionId)
val roomId = pr.roomId
if (roomId.isNullOrBlank()) {
session?.getVerificationService()
?.readyPendingVerification(supportedVerificationMethods,
pr.otherUserId,
pr.transactionId ?: "")
it.navigator.waitSessionVerification(it)
} else {
it.navigator.openRoom(it, roomId, pr.transactionId)
}
}
}
dismissedAction = Runnable {

View File

@ -47,7 +47,6 @@ import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null,
@ -95,7 +94,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
val userItem = session.getUser(args.otherUserId)
val pr = session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
val isWaitingForOtherMode = args.waitForIncomingRequest
val pr = if (isWaitingForOtherMode) {
// See if active tx for this user and take it
session.getVerificationService().getExistingVerificationRequest(args.otherUserId)
?.firstOrNull { !it.isFinished }
} else {
session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
}
val sasTx = (pr?.transactionId ?: args.verificationId)?.let {
session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? SasVerificationTransaction
@ -105,9 +112,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
}
val isWaitingForOtherMode = args.waitForIncomingRequest
// TODO see if active tx for this user and take it
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
otherUserMxItem = userItem?.toMatrixItem(),
sasTransactionState = sasTx?.state,
@ -177,8 +181,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
is VerificationAction.StartSASVerification -> {
val request = session.getVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
?: return@withState
if (roomId == null) return@withState
val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
if (roomId == null) {
session.getVerificationService().beginKeyVerification(
VerificationMethod.SAS,
otherUserId = request.otherUserId,
otherDeviceId = otherDevice ?: "",
transactionId = action.pendingRequestTransactionId
)
} else {
session.getVerificationService().beginKeyVerificationInDMs(
VerificationMethod.SAS,
transactionId = action.pendingRequestTransactionId,
@ -187,6 +198,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
otherDeviceId = otherDevice ?: "",
callback = null
)
}
Unit
}
is VerificationAction.RemoteQrCodeScanned -> {
@ -194,11 +206,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
.getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
existingTransaction
?.userHasScannedOtherQrCode(action.scannedData)
?.let { cancelCode ->
// Something went wrong
Timber.w("## Something is not right: $cancelCode")
// TODO
}
}
is VerificationAction.OtherUserScannedSuccessfully -> {
val transactionId = state.transactionId ?: return@withState

View File

@ -100,7 +100,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
fun manuallyVerify(device: CryptoDeviceInfo) {
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId)?.let { txID ->
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId, null)?.let { txID ->
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
}
}

View File

@ -33,9 +33,8 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
@ -44,6 +43,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
data class DevicesViewState(
val myDeviceId: String = "",
@ -168,17 +168,14 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
}
private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
// TODO Implement request in to DEVICE!!!
val txID = session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId)
if (txID != null) {
val txID = session.getVerificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId))
_requestLiveData.postValue(LiveEvent(Success(
action.copy(
userId = session.myUserId,
transactionId = txID
transactionId = txID.transactionId
)
)))
}
}
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }