Notify other devices of acceptance of verification request

This commit is contained in:
Jorge Martín 2022-05-03 09:47:40 +02:00
parent f7303789a0
commit 123ad87eda
7 changed files with 95 additions and 2 deletions

1
changelog.d/5724.sdk Normal file
View file

@ -0,0 +1 @@
- Notifies other devices when a verification request sent from an Android device is accepted.`

View file

@ -27,11 +27,13 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -252,4 +254,48 @@ class VerificationTest : InstrumentedTest {
cryptoTestData.cleanUp(testHelper) cryptoTestData.cleanUp(testHelper)
} }
@Test
fun test_selfVerificationAcceptedCancelsItForOtherSessions() {
val defaultSessionParams = SessionTestParams(true)
val testHelper = CommonTestHelper(context())
val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
val serviceOfVerified = aliceSessionToVerify.cryptoService().verificationService()
val serviceOfVerifier = aliceSessionThatVerifies.cryptoService().verificationService()
val serviceOfUserWhoReceivesCancellation = aliceSessionThatReceivesCanceledEvent.cryptoService().verificationService()
serviceOfVerifier.addListener(object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// Accept verification request
serviceOfVerifier.readyPendingVerification(
verificationMethods,
pr.otherUserId,
pr.transactionId!!,
)
}
})
serviceOfVerified.requestKeyVerification(
methods = verificationMethods,
otherUserId = aliceSessionToVerify.myUserId,
otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId),
)
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId)
requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
}
}
testHelper.signOutAndClose(aliceSessionToVerify)
testHelper.signOutAndClose(aliceSessionThatVerifies)
testHelper.signOutAndClose(aliceSessionThatReceivesCanceledEvent)
}
} }

View file

@ -28,7 +28,8 @@ enum class CancelCode(val value: String, val humanReadable: String) {
MismatchedKeys("m.key_mismatch", "Key mismatch"), MismatchedKeys("m.key_mismatch", "Key mismatch"),
UserError("m.user_error", "User error"), UserError("m.user_error", "User error"),
MismatchedUser("m.user_mismatch", "User mismatch"), MismatchedUser("m.user_mismatch", "User mismatch"),
QrCodeInvalid("m.qr_code.invalid", "Invalid QR code") QrCodeInvalid("m.qr_code.invalid", "Invalid QR code"),
AcceptedByAnotherDevice("m.accepted", "Verification request accepted by another device")
} }
fun safeValueOf(code: String?): CancelCode { fun safeValueOf(code: String?): CancelCode {

View file

@ -942,6 +942,22 @@ internal class DefaultVerificationService @Inject constructor(
readyInfo = readyReq readyInfo = readyReq
) )
) )
notifyOthersOfAcceptance(readyReq.transactionId, readyReq.fromDevice)
}
/**
* Gets a list of device ids excluding the current one.
*/
private fun getMyOtherDeviceIds(): List<String> = cryptoStore.getUserDevices(userId)?.keys?.filter { it != deviceId }.orEmpty()
/**
* Notifies other devices that the current verification transaction is being handled by [acceptedByDeviceId].
*/
private fun notifyOthersOfAcceptance(transactionId: String, acceptedByDeviceId: String) {
val deviceIds = getMyOtherDeviceIds().filter { it != acceptedByDeviceId }
val transport = verificationTransportToDeviceFactory.createTransport(null)
transport.cancelTransaction(transactionId, userId, deviceIds, CancelCode.AcceptedByAnotherDevice)
} }
private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? {

View file

@ -49,6 +49,11 @@ internal interface VerificationTransport {
otherUserDeviceId: String?, otherUserDeviceId: String?,
code: CancelCode) code: CancelCode)
fun cancelTransaction(transactionId: String,
otherUserId: String,
otherUserDeviceIds: List<String>,
code: CancelCode)
fun done(transactionId: String, fun done(transactionId: String,
onDone: (() -> Unit)?) onDone: (() -> Unit)?)

View file

@ -160,6 +160,9 @@ internal class VerificationTransportRoomMessage(
} }
} }
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List<String>, code: CancelCode) =
cancelTransaction(transactionId, otherUserId, null, code)
override fun done(transactionId: String, override fun done(transactionId: String,
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
Timber.d("## SAS sending done for $transactionId") Timber.d("## SAS sending done for $transactionId")

View file

@ -193,6 +193,27 @@ internal class VerificationTransportToDevice(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List<String>, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code")
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
val contentMap = MXUsersDevicesMap<Any>()
val messages = otherUserDeviceIds.associateWith { cancelMessage }
contentMap.setObjects(otherUserId, messages)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) {
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, override fun createAccept(tid: String,
keyAgreementProtocol: String, keyAgreementProtocol: String,
hash: String, hash: String,