diff --git a/changelog.d/5724.sdk b/changelog.d/5724.sdk new file mode 100644 index 0000000000..5a0a37fe31 --- /dev/null +++ b/changelog.d/5724.sdk @@ -0,0 +1 @@ +- Notifies other devices when a verification request sent from an Android device is accepted.` diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index df3b2ffe27..ceebc3cd01 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -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.UserPasswordAuth 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.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import java.util.concurrent.CountDownLatch import kotlin.coroutines.Continuation @@ -252,4 +254,48 @@ class VerificationTest : InstrumentedTest { 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) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt index 5a025f37e1..e4716d7794 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/CancelCode.kt @@ -28,7 +28,8 @@ enum class CancelCode(val value: String, val humanReadable: String) { MismatchedKeys("m.key_mismatch", "Key mismatch"), UserError("m.user_error", "User error"), 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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index 6da674d6e4..af48283767 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -939,9 +939,25 @@ internal class DefaultVerificationService @Inject constructor( updatePendingRequest( existingRequest.copy( - readyInfo = readyReq + readyInfo = readyReq ) ) + + notifyOthersOfAcceptance(readyReq.transactionId, readyReq.fromDevice) + } + + /** + * Gets a list of device ids excluding the current one. + */ + private fun getMyOtherDeviceIds(): List = 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? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt index c12aea9d52..8538e5a5af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransport.kt @@ -49,6 +49,11 @@ internal interface VerificationTransport { otherUserDeviceId: String?, code: CancelCode) + fun cancelTransaction(transactionId: String, + otherUserId: String, + otherUserDeviceIds: List, + code: CancelCode) + fun done(transactionId: String, onDone: (() -> Unit)?) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index e32828af23..03df849d22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -160,6 +160,9 @@ internal class VerificationTransportRoomMessage( } } + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) = + cancelTransaction(transactionId, otherUserId, null, code) + override fun done(transactionId: String, onDone: (() -> Unit)?) { Timber.d("## SAS sending done for $transactionId") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt index bc24ef2966..974adf3888 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -193,6 +193,27 @@ internal class VerificationTransportToDevice( .executeBy(taskExecutor) } + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceIds: List, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val cancelMessage = KeyVerificationCancel.create(transactionId, code) + val contentMap = MXUsersDevicesMap() + val messages = otherUserDeviceIds.associateWith { cancelMessage } + contentMap.setObjects(otherUserId, messages) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) { + this.callback = object : MatrixCallback { + 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,