diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 71ccf5b234..42045d1579 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2287,6 +2287,7 @@ Verify %s Verified %s Waiting for %s… + Verifying from Secure Key or Phrase… Messages in this room are not end-to-end encrypted. Messages here are not end-to-end encrypted. Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them. @@ -2396,6 +2397,8 @@ Unable to verify this device You won’t be able to access encrypted message history. Reset your Secure Message Backup and verification keys to start fresh. + Verify with another device + Verify your identity to access encrypted messages and prove your identity to others. Use an existing session to verify this one, granting it access to encrypted messages. Verify diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index f7642c7e77..665cc50627 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -63,7 +63,7 @@ import javax.inject.Inject @SessionScope internal class DefaultVerificationService @Inject constructor( @UserId private val userId: String, - @DeviceId private val deviceId: String?, + @DeviceId private val myDeviceId: String?, private val cryptoStore: IMXCryptoStore, // private val outgoingKeyRequestManager: OutgoingKeyRequestManager, // private val secretShareManager: SecretShareManager, @@ -582,7 +582,7 @@ internal class DefaultVerificationService @Inject constructor( } else if (userId > otherUserId) { return true } else { - return otherDeviceId < deviceId ?: "" + return otherDeviceId < myDeviceId ?: "" } } @@ -1299,8 +1299,10 @@ internal class DefaultVerificationService @Inject constructor( override suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest { // TODO refactor this with the DM one - val targetDevices = otherDeviceId?.let { listOf(it) } ?: cryptoStore.getUserDevices(otherUserId) - ?.values?.map { it.deviceId }.orEmpty() + val targetDevices = otherDeviceId?.let { listOf(it) } + ?: cryptoStore.getUserDevices(otherUserId) + ?.filter { it.key != myDeviceId } + ?.values?.map { it.deviceId }.orEmpty() Timber.i("## Requesting verification to user: $otherUserId with device list $targetDevices") diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt index b08bd58c5c..994fc2f33b 100644 --- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt +++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt @@ -1354,7 +1354,14 @@ internal class VerificationActor @AssistedInject constructor( } } - val methodValues = if (verificationTrustBackend.getMyTrustedMasterKeyBase64() != null) { + // XXX We should probably throw here if you try to verify someone else from an untrusted session + val shouldShowQROption = if (msg.otherUserId == myUserId) { + true + } else { + // It's verifying someone else, I should trust my key before doing it? + verificationTrustBackend.getUserMasterKeyBase64(myUserId) != null + } + val methodValues = if (shouldShowQROption) { // Add reciprocate method if application declares it can scan or show QR codes // Not sure if it ok to do that (?) val reciprocateMethod = msg.methods @@ -1452,8 +1459,11 @@ internal class VerificationActor @AssistedInject constructor( cancelRequest(matchingRequest, CancelCode.UnexpectedMessage) return } - // for room verification - if (msg.fromUser == myUserId && msg.readyInfo.fromDevice != myDevice) { + // for room verification (user) + // TODO if room and incoming I should check that right? + // actually it will not reach that point? handleReadyByAnotherOfMySessionReceived would be called instead? and + // the actor never sees event send by me in rooms + if (matchingRequest.otherUserId != myUserId && msg.fromUser == myUserId && msg.readyInfo.fromDevice != myDevice) { // it's a ready from another of my devices, so we should just // ignore following messages related to that request Timber.tag(loggerTag.value) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt index 51df8c1548..0f4ac1bdda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt @@ -16,11 +16,11 @@ package org.matrix.android.sdk.api.session.crypto.verification -sealed class VerificationEvent(val transactionId: String) { - data class RequestAdded(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId) - data class RequestUpdated(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId) - data class TransactionAdded(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId) - data class TransactionUpdated(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId) +sealed class VerificationEvent(val transactionId: String, val otherUserId: String) { + data class RequestAdded(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId, request.otherUserId) + data class RequestUpdated(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId, request.otherUserId) + data class TransactionAdded(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId, transaction.otherUserId) + data class TransactionUpdated(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId, transaction.otherUserId) } fun VerificationEvent.getRequest(): PendingVerificationRequest? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt index 7c5612dc0b..ac09fe884d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt @@ -58,7 +58,7 @@ interface VerificationService { * If no specific device should be verified, but we would like to request * verification from all our devices, use [requestSelfKeyVerification] instead. */ - suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest? + suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest /** * Request key verification with another user via room events (instead of the to-device API). diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt index 8351d021b8..493a5c13a9 100644 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt @@ -100,6 +100,7 @@ internal class FakeCryptoStoreForVerification(private val mode: StoreMode) { val aliceMxId = "alice@example.com" val bobMxId = "bob@example.com" val bobDeviceId = "MKRJDSLYGA" + val bobDeviceId2 = "RRIWTEKZEI" val aliceDevice1Id = "MGDAADVDMG" @@ -147,6 +148,22 @@ internal class FakeCryptoStoreForVerification(private val mode: StoreMode) { unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios") ) + val aBobDevice2 = CryptoDeviceInfo( + deviceId = bobDeviceId2, + userId = bobMxId, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + keys = mapOf( + "curve25519:$bobDeviceId" to "mE4WKAcyRRv7Gk1IDIVm0lZNzb8g9YL2eRQZUHmkkCI", + "ed25519:$bobDeviceId" to "yB/9LITHTqrvdXWDR2k6Qw/MDLUBWABlP9v2eYuqHPE", + ), + signatures = mapOf( + bobMxId to mapOf( + "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA", + ) + ), + unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Android") + ) + private val aliceMSKBase = CryptoCrossSigningKey( userId = aliceMxId, usages = listOf(KeyUsage.MASTER.value), diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt index ceebfc5f6e..992e9ffcd0 100644 --- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt +++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt @@ -22,6 +22,7 @@ import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady @@ -53,8 +54,8 @@ internal class VerificationActorHelper { var aliceChannel: SendChannel? = null fun setUpActors(): TestData { - val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel } - val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel } + val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { listOf(bobChannel) } + val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { listOf(aliceChannel) } val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice) val aliceActor = fakeActor( @@ -82,7 +83,53 @@ internal class VerificationActorHelper { ) } - private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel?)): VerificationTransportLayer { + fun setupMultipleSessions() { + val aliceTargetChannels = mutableListOf>() + val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { aliceTargetChannels } + val bobTargetChannels = mutableListOf>() + val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bobTargetChannels } + val bob2TargetChannels = mutableListOf>() + val bob2TransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bob2TargetChannels } + + val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice) + val aliceActor = fakeActor( + actorAScope, + FakeCryptoStoreForVerification.aliceMxId, + fakeAliceStore.instance, + aliceTransportLayer, + ) + + val fakeBobStore1 = FakeCryptoStoreForVerification(StoreMode.Bob) + val bobActor = fakeActor( + actorBScope, + FakeCryptoStoreForVerification.bobMxId, + fakeBobStore1.instance, + bobTransportLayer + ) + + val actorCScope = CoroutineScope(SupervisorJob()) + val fakeBobStore2 = FakeCryptoStoreForVerification(StoreMode.Bob) + every { fakeBobStore2.instance.getMyDeviceId() } returns FakeCryptoStoreForVerification.bobDeviceId2 + every { fakeBobStore2.instance.getMyDevice() } returns FakeCryptoStoreForVerification.aBobDevice2 + + val bobActor2 = fakeActor( + actorCScope, + FakeCryptoStoreForVerification.bobMxId, + fakeBobStore2.instance, + bobTransportLayer + ) + + aliceTargetChannels.add(bobActor.channel) + aliceTargetChannels.add(bobActor2.channel) + + bobTargetChannels.add(aliceActor.channel) + bobTargetChannels.add(bobActor2.channel) + + bob2TargetChannels.add(aliceActor.channel) + bob2TargetChannels.add(bobActor.channel) + } + + private fun mockTransportTo(fromUser: String, otherChannel: (() -> List?>)): VerificationTransportLayer { return mockk { coEvery { sendToOther(any(), any(), any()) } answers { val request = firstArg() @@ -93,64 +140,76 @@ internal class VerificationActorHelper { when (type) { EventType.KEY_VERIFICATION_READY -> { val readyContent = info.asValidObject() - otherChannel()?.send( - VerificationIntent.OnReadyReceived( - transactionId = request.requestId, - fromUser = fromUser, - viaRoom = request.roomId, - readyInfo = readyContent as ValidVerificationInfoReady, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnReadyReceived( + transactionId = request.requestId, + fromUser = fromUser, + viaRoom = request.roomId, + readyInfo = readyContent as ValidVerificationInfoReady, + ) + ) + } } EventType.KEY_VERIFICATION_START -> { val startContent = info.asValidObject() - otherChannel()?.send( - VerificationIntent.OnStartReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validVerificationInfoStart = startContent as ValidVerificationInfoStart, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnStartReceived( + fromUser = fromUser, + viaRoom = request.roomId, + validVerificationInfoStart = startContent as ValidVerificationInfoStart, + ) + ) + } } EventType.KEY_VERIFICATION_ACCEPT -> { val content = info.asValidObject() - otherChannel()?.send( - VerificationIntent.OnAcceptReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validAccept = content as ValidVerificationInfoAccept, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnAcceptReceived( + fromUser = fromUser, + viaRoom = request.roomId, + validAccept = content as ValidVerificationInfoAccept, + ) + ) + } } EventType.KEY_VERIFICATION_KEY -> { val content = info.asValidObject() - otherChannel()?.send( - VerificationIntent.OnKeyReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validKey = content as ValidVerificationInfoKey, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnKeyReceived( + fromUser = fromUser, + viaRoom = request.roomId, + validKey = content as ValidVerificationInfoKey, + ) + ) + } } EventType.KEY_VERIFICATION_MAC -> { val content = info.asValidObject() - otherChannel()?.send( - VerificationIntent.OnMacReceived( - fromUser = fromUser, - viaRoom = request.roomId, - validMac = content as ValidVerificationInfoMac, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnMacReceived( + fromUser = fromUser, + viaRoom = request.roomId, + validMac = content as ValidVerificationInfoMac, + ) + ) + } } EventType.KEY_VERIFICATION_DONE -> { val content = info.asValidObject() - otherChannel()?.send( - VerificationIntent.OnDoneReceived( - fromUser = fromUser, - viaRoom = request.roomId, - transactionId = (content as ValidVerificationDone).transactionId, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnDoneReceived( + fromUser = fromUser, + viaRoom = request.roomId, + transactionId = (content as ValidVerificationDone).transactionId, + ) + ) + } } } } @@ -167,26 +226,30 @@ internal class VerificationActorHelper { val requestContent = content.toModel()?.copy( transactionId = fakeEventId )?.asValidObject() - otherChannel()?.send( - VerificationIntent.OnVerificationRequestReceived( - requestContent!!, - senderId = FakeCryptoStoreForVerification.aliceMxId, - roomId = roomId, - timeStamp = 0 - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnVerificationRequestReceived( + requestContent!!, + senderId = FakeCryptoStoreForVerification.aliceMxId, + roomId = roomId, + timeStamp = 0 + ) + ) + } } EventType.KEY_VERIFICATION_READY -> { val readyContent = content.toModel() ?.asValidObject() - otherChannel()?.send( - VerificationIntent.OnReadyReceived( - transactionId = readyContent!!.transactionId, - fromUser = fromUser, - viaRoom = roomId, - readyInfo = readyContent, - ) - ) + otherChannel().onEach { + it?.send( + VerificationIntent.OnReadyReceived( + transactionId = readyContent!!.transactionId, + fromUser = fromUser, + viaRoom = roomId, + readyInfo = readyContent, + ) + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 16c423da5b..fb8a05e384 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -31,6 +31,7 @@ import im.vector.app.features.createdirect.CreateDirectRoomViewModel import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel import im.vector.app.features.crypto.quads.SharedSecureStorageViewModel import im.vector.app.features.crypto.recover.BootstrapSharedViewModel +import im.vector.app.features.crypto.verification.self.SelfVerificationViewModel import im.vector.app.features.crypto.verification.user.UserVerificationViewModel import im.vector.app.features.devtools.RoomDevToolViewModel import im.vector.app.features.discovery.DiscoverySettingsViewModel @@ -594,6 +595,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(UserVerificationViewModel::class) fun userVerificationBottomSheetViewModelFactory(factory: UserVerificationViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(SelfVerificationViewModel::class) + fun selfVerificationBottomSheetViewModelFactory(factory: SelfVerificationViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(CreatePollViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 3a3797af22..4e60a6d831 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -187,8 +187,9 @@ class IncomingVerificationRequestHandler @Inject constructor( (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { val roomId = pr.roomId if (roomId.isNullOrBlank()) { - // TODO - // it.navigator.waitSessionVerification(it) + if (pr.otherUserId == session?.myUserId) { + it.navigator.showIncomingSelfVerification(it, pr.transactionId) + } } else { it.navigator.openRoom( context = it, diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt index 4201277f4d..55b09d4a4b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt @@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction // TODO Remove otherUserId and transactionId when it's not necessary. Should be known by the ViewModel, no? sealed class VerificationAction : VectorViewModelAction { object RequestVerificationByDM : VerificationAction() + object RequestSelfVerification : VerificationAction() object StartSASVerification : VerificationAction() data class RemoteQrCodeScanned(val otherUserId: String, val transactionId: String, val scannedData: String) : VerificationAction() object OtherUserScannedSuccessfully : VerificationAction() @@ -28,6 +29,7 @@ sealed class VerificationAction : VectorViewModelAction { object SASMatchAction : VerificationAction() object SASDoNotMatchAction : VerificationAction() data class GotItConclusion(val verified: Boolean) : VerificationAction() + object FailedToGetKeysFrom4S : VerificationAction() object SkipVerification : VerificationAction() object VerifyFromPassphrase : VerificationAction() object ReadyPendingVerification : VerificationAction() diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationBottomSheet.kt new file mode 100644 index 0000000000..9fabdc3d12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationBottomSheet.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2022 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.app.features.crypto.verification.self + +import android.app.Activity +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetVerificationBinding +import im.vector.app.features.crypto.quads.SharedSecureStorageActivity +import im.vector.app.features.crypto.verification.VerificationAction +import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import kotlin.reflect.KClass + +class SelfVerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override val showExpanded = true + + @Parcelize + data class Args( + // when verifying a new session from an existing safe one + val targetDevice: String? = null, + // when we started from an incoming request + val transactionId: String? = null, + ) : Parcelable + + private val viewModel by fragmentViewModel(SelfVerificationViewModel::class) + + override fun getBinding( + inflater: LayoutInflater, + container: ViewGroup? + ) = BottomSheetVerificationBinding.inflate(inflater, container, false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + showFragment(SelfVerificationFragment::class) + } + + private val secretStartForActivityResult = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + val result = activityResult.data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT) + val reset = activityResult.data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false + if (result != null) { + viewModel.handle(VerificationAction.GotResultFromSsss(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)) + } else if (reset) { + // all have been reset, so we are verified? + viewModel.handle(VerificationAction.SecuredStorageHasBeenReset) + } + } else { + viewModel.handle(VerificationAction.CancelledFromSsss) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.observeViewEvents { event -> + when (event) { + VerificationBottomSheetViewEvents.AccessSecretStore -> { + secretStartForActivityResult.launch( + SharedSecureStorageActivity.newReadIntent( + requireContext(), + null, // use default key + listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), + SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS + ) + ) + } + VerificationBottomSheetViewEvents.Dismiss -> { + dismiss() + } + VerificationBottomSheetViewEvents.GoToSettings -> { + // nop for user verificaiton + } + is VerificationBottomSheetViewEvents.ModalError -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.dialog_title_error)) + .setMessage(event.errorMessage) + .setCancelable(false) + .setPositiveButton(R.string.ok, null) + .show() + } + } + } + } + + // viewModel.observeViewEvents { +// when (it) { +// is VerificationBottomSheetViewEvents.Dismiss -> dismiss() +// is VerificationBottomSheetViewEvents.AccessSecretStore -> { +// secretStartForActivityResult.launch( +// SharedSecureStorageActivity.newReadIntent( +// requireContext(), +// null, // use default key +// listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME), +// SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS +// ) +// ) +// } +// is VerificationBottomSheetViewEvents.ModalError -> { +// MaterialAlertDialogBuilder(requireContext()) +// .setTitle(getString(R.string.dialog_title_error)) +// .setMessage(it.errorMessage) +// .setCancelable(false) +// .setPositiveButton(R.string.ok, null) +// .show() +// Unit +// } +// VerificationBottomSheetViewEvents.GoToSettings -> { +// dismiss() +// (activity as? VectorBaseActivity<*>)?.let { activity -> +// activity.navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY) +// } +// } +// } +// } +// } + + override fun invalidate() = withState(viewModel) { state -> +// avatarRenderer.render(state.otherUserMxItem, views.otherUserAvatarImageView) + views.otherUserShield.isVisible = false + if (state.isThisSessionVerified) { + views.otherUserAvatarImageView.setImageResource( + R.drawable.ic_shield_trusted + ) + views.otherUserNameText.text = getString(R.string.verification_profile_verify) + } else { + views.otherUserAvatarImageView.setImageResource( + R.drawable.ic_shield_black + ) + views.otherUserNameText.text = getString(R.string.crosssigning_verify_this_session) + } + + super.invalidate() + } + + private fun showFragment(fragmentClass: KClass, argsParcelable: Parcelable? = null) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + childFragmentManager.commitTransaction { + replace( + R.id.bottomSheetFragmentContainer, + fragmentClass.java, + argsParcelable?.toMvRxBundle(), + fragmentClass.simpleName + ) + } + } + } + + companion object { + fun verifyOwnUntrustedDevice(): SelfVerificationBottomSheet { + return SelfVerificationBottomSheet().apply { + setArguments( + Args() + ) + } + } + + fun forTransaction(transactionId: String): SelfVerificationBottomSheet { + return SelfVerificationBottomSheet().apply { + setArguments( + Args(transactionId = transactionId) + ) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt new file mode 100644 index 0000000000..aee26922ca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2022 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.app.features.crypto.verification.self + +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.epoxy.bottomSheetDividerItem +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.crypto.verification.epoxy.bottomSheetSelfWaitItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import im.vector.app.features.crypto.verification.user.BaseEpoxyVerificationController +import im.vector.app.features.crypto.verification.user.VerificationTransactionData +import im.vector.app.features.crypto.verification.user.bottomDone +import im.vector.app.features.crypto.verification.user.gotIt +import im.vector.app.features.crypto.verification.user.renderAcceptDeclineRequest +import im.vector.app.features.crypto.verification.user.renderCancel +import im.vector.app.features.crypto.verification.user.renderSasTransaction +import im.vector.app.features.crypto.verification.user.renderStartTransactionOptions +import im.vector.app.features.crypto.verification.user.verifiedSuccessTile +import im.vector.app.features.html.EventHtmlRenderer +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState +import timber.log.Timber +import javax.inject.Inject + +class SelfVerificationController @Inject constructor( + stringProvider: StringProvider, + colorProvider: ColorProvider, + eventHtmlRenderer: EventHtmlRenderer, +) : BaseEpoxyVerificationController(stringProvider, colorProvider, eventHtmlRenderer) { + + interface InteractionListener : BaseEpoxyVerificationController.InteractionListener { + fun onClickRecoverFromPassphrase() + fun onClickSkip() + fun onClickResetSecurity() + fun onDoneFrom4S() + fun keysNotIn4S() + } + + var state: SelfVerificationViewState? = null + + fun update(state: SelfVerificationViewState) { + Timber.w("VALR controller updated $state") + this.state = state + requestModelBuild() + } + + override fun buildModels() { + val state = this.state ?: return + when (state.pendingRequest) { + Uninitialized -> { + renderBaseNoActiveRequest(state) + } + else -> { + renderRequest(state) + } + } + } + + private fun renderBaseNoActiveRequest(state: SelfVerificationViewState) { + if (state.verifyingFrom4SAction !is Uninitialized) { + render4SCheckState(state) + } else { + renderNoRequestStarted(state) + } + } + + private fun renderRequest(state: SelfVerificationViewState) { + val host = this + when (state.pendingRequest) { + Uninitialized -> { + // let's add option to start one + val styledText = stringProvider.getString(R.string.verify_new_session_notice) + + bottomSheetVerificationNoticeItem { + id("notice") + notice(styledText.toEpoxyCharSequence()) + } + + bottomSheetDividerItem { + id("sep") + } + bottomSheetVerificationActionItem { + id("start") + title(host.stringProvider.getString(R.string.start_verification)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { host.listener?.onClickOnVerificationStart() } + } + } + is Loading -> { + bottomSheetSelfWaitItem { + id("waiting") + } +// bottomSheetVerificationWaitingItem { +// id("waiting_pr_loading") +// // title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUserMxItem.getBestName())) +// } + } + is Success -> { + val pendingRequest = state.pendingRequest.invoke() + when (pendingRequest.state) { + EVerificationState.WaitingForReady -> { + bottomSheetSelfWaitItem { + id("waiting") + } + } + EVerificationState.Requested -> { + // add accept buttons? + renderAcceptDeclineRequest() + bottomSheetVerificationActionItem { + id("not me") + title(host.stringProvider.getString(R.string.verify_new_session_was_not_me)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { + TODO() +// host.listener?.wasNotMe() + } + } + } + EVerificationState.Ready -> { + // add start options + renderStartTransactionOptions(pendingRequest, true) + } + EVerificationState.Started, + EVerificationState.WeStarted -> { + // nothing to do, in this case the active transaction is shown + renderActiveTransaction(state) + } + EVerificationState.WaitingForDone, + EVerificationState.Done -> { + verifiedSuccessTile() + bottomDone { + listener?.onDone(true) + } + } + EVerificationState.Cancelled -> { + renderCancel(pendingRequest.cancelConclusion ?: CancelCode.User) + } + EVerificationState.HandledByOtherSession -> { + // we should dismiss + } + } + } + is Fail -> { + // TODO + } + } + } + + private fun renderNoRequestStarted(state: SelfVerificationViewState) { + val host = this + bottomSheetVerificationNoticeItem { + id("notice") + notice(host.stringProvider.getString(R.string.verification_verify_identity).toEpoxyCharSequence()) + } + bottomSheetDividerItem { + id("notice_div") + } + // Option to verify with another device + if (state.hasAnyOtherSession) { + bottomSheetVerificationActionItem { + id("start") + title(host.stringProvider.getString(R.string.verification_verify_with_another_device)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { (host.listener as? InteractionListener)?.onClickOnVerificationStart() } + } + + bottomSheetDividerItem { + id("start_div") + } + } + + if (state.quadSContainsSecrets) { + bottomSheetVerificationActionItem { + id("passphrase") + title(host.stringProvider.getString(R.string.verification_cannot_access_other_session)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + subTitle(host.stringProvider.getString(R.string.verification_use_passphrase)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { (host.listener as? InteractionListener)?.onClickRecoverFromPassphrase() } + } + + bottomSheetDividerItem { + id("start_div") + } + } + + // option to reset all + bottomSheetVerificationActionItem { + id("reset") + title(host.stringProvider.getString(R.string.bad_passphrase_key_reset_all_action)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + subTitle(host.stringProvider.getString(R.string.secure_backup_reset_all_no_other_devices)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { (host.listener as? InteractionListener)?.onClickResetSecurity() } + } + + if (!state.isVerificationRequired) { + bottomSheetDividerItem { + id("reset_div") + } + + bottomSheetVerificationActionItem { + id("skip") + title(host.stringProvider.getString(R.string.action_skip)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + listener { (host.listener as? InteractionListener)?.onClickSkip() } + } + } + } + + private fun render4SCheckState(state: SelfVerificationViewState) { + val host = this + when (val action = state.verifyingFrom4SAction) { + is Fail -> { + } + is Loading -> { + bottomSheetVerificationWaitingItem { + id("waiting") + title(host.stringProvider.getString(R.string.verification_request_waiting_for_recovery)) + } + } + is Success -> { + val invoke = action.invoke() + if (invoke) { + verifiedSuccessTile() + bottomDone { (host.listener as? InteractionListener)?.onDoneFrom4S() } + } else { + bottomSheetVerificationNoticeItem { + id("notice_4s_failed'") + notice( + host.stringProvider.getString( + R.string.error_failed_to_import_keys + ) + .toEpoxyCharSequence() + ) + } + gotIt { (host.listener as? InteractionListener)?.keysNotIn4S() } + } + } + else -> { + // nop + } + } + } + + private fun renderActiveTransaction(state: SelfVerificationViewState) { + val transaction = state.startedTransaction + val host = this + when (transaction) { + is Loading -> { + // Loading => We are starting a transaction + bottomSheetVerificationWaitingItem { + id("waiting") + title(host.stringProvider.getString(R.string.please_wait)) + } + } + is Success -> { + // Success => There is an active transaction + renderTransaction(state, transaction = transaction.invoke()) + } + is Fail -> { + // todo + } + is Uninitialized -> { + } + } + } + + private fun renderTransaction(state: SelfVerificationViewState, transaction: VerificationTransactionData) { + when (transaction) { + is VerificationTransactionData.QrTransactionData -> { + // TODO + // renderQrTransaction(transaction, state.otherUserMxItem) + } + is VerificationTransactionData.SasTransactionData -> { + renderSasTransaction(transaction) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationFragment.kt new file mode 100644 index 0000000000..67d57a2ae4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationFragment.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2022 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.app.features.crypto.verification.self + +import android.app.Activity +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding +import im.vector.app.features.crypto.verification.VerificationAction +import im.vector.app.features.qrcode.QrCodeScannerActivity +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class SelfVerificationFragment : VectorBaseFragment(), + SelfVerificationController.InteractionListener { + + @Inject lateinit var controller: SelfVerificationController + + private val viewModel by parentFragmentViewModel(SelfVerificationViewModel::class) + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding { + return BottomSheetVerificationChildFragmentBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + } + + override fun onDestroyView() { + views.bottomSheetVerificationRecyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + private fun setupRecyclerView() { + views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun invalidate() = withState(viewModel) { state -> + Timber.w("VALR: invalidate with State: ${state.pendingRequest}") + controller.update(state) + } + + override fun onClickRecoverFromPassphrase() { + viewModel.handle(VerificationAction.VerifyFromPassphrase) + } + + override fun onClickSkip() { + TODO("Not yet implemented") + } + + override fun onClickResetSecurity() { + TODO("Not yet implemented") + } + + override fun onDoneFrom4S() { + viewModel.handle(VerificationAction.GotItConclusion(true)) + } + + override fun keysNotIn4S() { + viewModel.handle(VerificationAction.FailedToGetKeysFrom4S) + } + + override fun onClickOnVerificationStart() { + viewModel.handle(VerificationAction.RequestSelfVerification) + } + + override fun onDone(b: Boolean) { + viewModel.handle(VerificationAction.GotItConclusion(b)) + } + + override fun onDoNotMatchButtonTapped() { + viewModel.handle(VerificationAction.SASDoNotMatchAction) + } + + override fun onMatchButtonTapped() { + viewModel.handle(VerificationAction.SASMatchAction) + } + + private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + doOpenQRCodeScanner() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_camera) + } + } + + override fun openCamera() { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) { + doOpenQRCodeScanner() + } + } + + private fun doOpenQRCodeScanner() { + QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher) + } + + private val scanActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + val scannedQrCode = QrCodeScannerActivity.getResultText(activityResult.data) + val wasQrCode = QrCodeScannerActivity.getResultIsQrCode(activityResult.data) + + if (wasQrCode && !scannedQrCode.isNullOrBlank()) { + onRemoteQrCodeScanned(scannedQrCode) + } else { + Timber.w("It was not a QR code, or empty result") + } + } + } + + private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(viewModel) { state -> + viewModel.handle( + VerificationAction.RemoteQrCodeScanned( + state.pendingRequest.invoke()?.otherUserId.orEmpty(), + state.pendingRequest.invoke()?.transactionId.orEmpty(), + remoteQrCode + ) + ) + } + + override fun doVerifyBySas() { + viewModel.handle(VerificationAction.StartSASVerification) + } + + override fun onUserDeniesQrCodeScanned() { + viewModel.handle(VerificationAction.OtherUserDidNotScanned) + } + + override fun onUserConfirmsQrCodeScanned() { + viewModel.handle(VerificationAction.OtherUserScannedSuccessfully) + } + + override fun acceptRequest() { + viewModel.handle(VerificationAction.ReadyPendingVerification) + } + + override fun declineRequest() { + viewModel.handle(VerificationAction.CancelPendingVerification) + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt new file mode 100644 index 0000000000..a46835dfdd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt @@ -0,0 +1,428 @@ +/* + * Copyright (c) 2022 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.app.features.crypto.verification.self + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.crypto.verification.VerificationAction +import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents +import im.vector.app.features.crypto.verification.user.VerificationTransactionData +import im.vector.app.features.crypto.verification.user.toDataClass +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isSecureBackupRequired +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified +import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils +import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.getRequest +import org.matrix.android.sdk.api.session.crypto.verification.getTransaction +import org.matrix.android.sdk.api.util.fromBase64 +import timber.log.Timber + +data class SelfVerificationViewState( + val pendingRequest: Async = Uninitialized, + // need something immutable for state to work properly, VerificationTransaction is not + val startedTransaction: Async = Uninitialized, + val verifyingFrom4SAction: Async = Uninitialized, + val otherDeviceId: String? = null, + val transactionId: String? = null, + val currentDeviceCanCrossSign: Boolean = false, + val userWantsToCancel: Boolean = false, + val hasAnyOtherSession: Boolean = false, + val quadSContainsSecrets: Boolean = false, + val isVerificationRequired: Boolean = false, + val isThisSessionVerified: Boolean = false, +) : MavericksState { + + constructor(args: SelfVerificationBottomSheet.Args) : this( + transactionId = args.transactionId, + otherDeviceId = args.targetDevice, + ) +} + +class SelfVerificationViewModel @AssistedInject constructor( + @Assisted private val initialState: SelfVerificationViewState, + private val session: Session, + private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, + private val rawService: RawService, + private val stringProvider: StringProvider, + private val matrix: Matrix, +) : + VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SelfVerificationViewState): SelfVerificationViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + + if (initialState.transactionId != null) { + setState { + copy(pendingRequest = Loading()) + } + viewModelScope.launch { + session.cryptoService().verificationService().getExistingVerificationRequest(session.myUserId, initialState.transactionId)?.let { + setState { + copy(pendingRequest = Success(it)) + } + } + } + } + observeRequestsAndTransactions() + // This is async, but at this point should be in cache + // so it's ok to not wait until result + viewModelScope.launch(Dispatchers.IO) { + val wellKnown = rawService.getElementWellknown(session.sessionParams) + setState { + copy(isVerificationRequired = wellKnown?.isSecureBackupRequired().orFalse()) + } + } + + val hasAnyOtherSession = session.cryptoService() + .getCryptoDeviceInfo(session.myUserId) + .any { + it.deviceId != session.sessionParams.deviceId + } + + setState { + copy( + currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), + quadSContainsSecrets = session.sharedSecretStorageService().isRecoverySetup(), + hasAnyOtherSession = hasAnyOtherSession + ) + } + + viewModelScope.launch { + val isThisSessionVerified = session.cryptoService().crossSigningService().isCrossSigningVerified() + setState { + copy( + isThisSessionVerified = isThisSessionVerified, + ) + } + } +// + } + + private fun observeRequestsAndTransactions() { + session.cryptoService().verificationService() + .requestEventFlow() + .filter { + it.otherUserId == session.myUserId + } + .onEach { + it.getRequest()?.let { + setState { + copy( + pendingRequest = Success(it), + ) + } + } + it.getTransaction()?.let { + val dClass = it.toDataClass() + if (dClass != null) { + setState { + copy( + startedTransaction = Success(dClass), + ) + } + } else { + setState { + copy( + startedTransaction = Fail(IllegalArgumentException("Unsupported Transaction")), + ) + } + } + } + } + .launchIn(viewModelScope) + } + + override fun handle(action: VerificationAction) { + when (action) { + VerificationAction.CancelPendingVerification -> { + withState { state -> + state.pendingRequest.invoke()?.let { + viewModelScope.launch { + session.cryptoService().verificationService() + .cancelVerificationRequest(it) + } + } + } + } + VerificationAction.CancelledFromSsss -> { + setState { + copy(verifyingFrom4SAction = Uninitialized) + } + } + is VerificationAction.GotItConclusion -> { + // just dismiss + _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) + } + is VerificationAction.GotResultFromSsss -> handleSecretBackFromSSSS(action) + VerificationAction.OtherUserDidNotScanned -> { + withState { state -> + state.startedTransaction.invoke()?.let { + viewModelScope.launch { + val tx = session.cryptoService().verificationService() + .getExistingTransaction(it.otherUserId, it.transactionId) + as? QrCodeVerificationTransaction + tx?.otherUserDidNotScannedMyQrCode() + } + } + } + } + VerificationAction.OtherUserScannedSuccessfully -> { + withState { state -> + state.startedTransaction.invoke()?.let { + viewModelScope.launch { + val tx = session.cryptoService().verificationService() + .getExistingTransaction(it.otherUserId, it.transactionId) + as? QrCodeVerificationTransaction + tx?.otherUserScannedMyQrCode() + } + } + } + } + VerificationAction.ReadyPendingVerification -> { + withState { state -> + state.pendingRequest.invoke()?.let { + viewModelScope.launch { + session.cryptoService().verificationService() + .readyPendingVerification( + supportedVerificationMethodsProvider.provide(), + it.otherUserId, it.transactionId + ) + } + } + } + } + is VerificationAction.RemoteQrCodeScanned -> { + setState { + copy(startedTransaction = Loading()) + } + withState { state -> + val request = state.pendingRequest.invoke() ?: return@withState + viewModelScope.launch { + try { + session.cryptoService().verificationService() + .reciprocateQRVerification( + request.otherUserId, + request.transactionId, + action.scannedData + ) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to reciprocated") + setState { + copy(startedTransaction = Fail(failure)) + } + } + } + } + } + is VerificationAction.SASDoNotMatchAction -> { + withState { state -> + viewModelScope.launch { + val transaction = session.cryptoService().verificationService() + .getExistingTransaction(session.myUserId, state.transactionId.orEmpty()) + (transaction as? SasVerificationTransaction)?.shortCodeDoesNotMatch() + } + } + } + is VerificationAction.SASMatchAction -> { + withState { state -> + viewModelScope.launch { + val transaction = session.cryptoService().verificationService() + .getExistingTransaction(session.myUserId, state.transactionId.orEmpty()) + (transaction as? SasVerificationTransaction)?.userHasVerifiedShortCode() + } + } + } + VerificationAction.SecuredStorageHasBeenReset -> TODO() + VerificationAction.SkipVerification -> TODO() + VerificationAction.StartSASVerification -> { + withState { state -> + val request = state.pendingRequest.invoke() ?: return@withState + viewModelScope.launch { + session.cryptoService().verificationService() + .startKeyVerification(VerificationMethod.SAS, session.myUserId, request.transactionId) + } + } + } + VerificationAction.VerifyFromPassphrase -> { + setState { copy(verifyingFrom4SAction = Loading()) } + _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore) + } + VerificationAction.FailedToGetKeysFrom4S -> { + setState { + copy( + verifyingFrom4SAction = Uninitialized, + quadSContainsSecrets = false + ) + } + } + VerificationAction.RequestSelfVerification -> { + handleRequestVerification() + } + VerificationAction.RequestVerificationByDM -> { + // not applicable in self verification + } + } + } + + private fun handleRequestVerification() { + setState { + copy( + pendingRequest = Loading() + ) + } + var targetDevice: String? = null + withState { state -> + targetDevice = state.otherDeviceId + } + viewModelScope.launch { + try { + val request = session.cryptoService().verificationService().requestDeviceVerification( + supportedVerificationMethodsProvider.provide(), + session.myUserId, + targetDevice + ) + setState { + copy( + pendingRequest = Success(request), + transactionId = request.transactionId + ) + } + } catch (failure: Throwable) { + setState { + copy( + pendingRequest = Loading(), + ) + } + } + } + } + + private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) { + viewModelScope.launch(Dispatchers.IO) { + try { + action.cypherData.fromBase64().inputStream().use { ins -> + val res = matrix.secureStorageService().loadSecureSecret>(ins, action.alias) + val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( + res?.get(MASTER_KEY_SSSS_NAME), + res?.get(USER_SIGNING_KEY_SSSS_NAME), + res?.get(SELF_SIGNING_KEY_SSSS_NAME) + ) + if (trustResult.isVerified()) { + // Sign this device and upload the signature + try { + session.cryptoService().crossSigningService().trustDevice(session.sessionParams.deviceId) + } catch (failure: Exception) { + Timber.w(failure, "Failed to sign my device after recovery") + } + + setState { + copy( + verifyingFrom4SAction = Success(true), + ) + } + tentativeRestoreBackup(res) + } else { + setState { + copy( + verifyingFrom4SAction = Success(false), + ) + } + } + } + } catch (failure: Throwable) { + setState { + copy( + verifyingFrom4SAction = Fail(failure) + ) + } + _viewEvents.post( + VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage ?: stringProvider.getString(R.string.unexpected_error)) + ) + } + } + } + + private fun tentativeRestoreBackup(res: Map?) { + // on session scope because will happen after viewmodel is cleared + session.coroutineScope.launch { + // It's not a good idea to download the full backup, it might take very long + // and use a lot of resources + // Just check that the key is valid and store it, the backup will be used megolm session per + // megolm session when an UISI is encountered + try { + val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { + Timber.v("## Keybackup secret not restored from SSSS") + } + + val version = session.cryptoService().keysBackupService().getCurrentVersion()?.toKeysVersionResult() ?: return@launch + + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + val backupRecoveryKey = BackupUtils.recoveryKeyFromBase58(recoveryKey) + val isValid = backupRecoveryKey + ?.let { session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(it) } + ?: false + if (isValid) { + session.cryptoService().keysBackupService().saveBackupRecoveryKey(backupRecoveryKey, version.version) + // session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true) + } + } catch (failure: Throwable) { + // Just ignore for now + Timber.e(failure, "## Failed to restore backup after SSSS recovery") + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt index 597ff58faa..cdc276a6db 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationController.kt @@ -26,13 +26,10 @@ import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.list.buttonPositiveDestructiveButtonBarItem import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem -import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationEmojisItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem -import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem import im.vector.app.features.displayname.getBestName import im.vector.app.features.html.EventHtmlRenderer @@ -40,23 +37,18 @@ import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation -import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.QRCodeVerificationState -import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber import javax.inject.Inject -class UserVerificationController @Inject constructor( - private val stringProvider: StringProvider, - private val colorProvider: ColorProvider, - private val eventHtmlRenderer: EventHtmlRenderer, +abstract class BaseEpoxyVerificationController( + val stringProvider: StringProvider, + val colorProvider: ColorProvider, + val eventHtmlRenderer: EventHtmlRenderer, ) : EpoxyController() { interface InteractionListener { - fun acceptRequest() - fun declineRequest() fun onClickOnVerificationStart() fun onDone(b: Boolean) fun onDoNotMatchButtonTapped() @@ -65,9 +57,23 @@ class UserVerificationController @Inject constructor( fun doVerifyBySas() fun onUserDeniesQrCodeScanned() fun onUserConfirmsQrCodeScanned() + fun acceptRequest() + fun declineRequest() } var listener: InteractionListener? = null +} + +class UserVerificationController @Inject constructor( + stringProvider: StringProvider, + colorProvider: ColorProvider, + eventHtmlRenderer: EventHtmlRenderer, +) : BaseEpoxyVerificationController(stringProvider, colorProvider, eventHtmlRenderer) { + +// interface InteractionListener: BaseEpoxyVerificationController.InteractionListener { +// } + +// var listener: InteractionListener? = null var state: UserVerificationViewState? = null @@ -126,17 +132,11 @@ class UserVerificationController @Inject constructor( } EVerificationState.Requested -> { // add accept buttons? - buttonPositiveDestructiveButtonBarItem { - id("accept_decline") - positiveText(host.stringProvider.getString(R.string.action_accept).toEpoxyCharSequence()) - destructiveText(host.stringProvider.getString(R.string.action_decline).toEpoxyCharSequence()) - positiveButtonClickAction { host.listener?.acceptRequest() } - destructiveButtonClickAction { host.listener?.declineRequest() } - } + renderAcceptDeclineRequest() } EVerificationState.Ready -> { // add start options - renderStartTransactionOptions(state, pendingRequest) + renderStartTransactionOptions(pendingRequest, false) } EVerificationState.Started, EVerificationState.WeStarted -> { @@ -145,21 +145,7 @@ class UserVerificationController @Inject constructor( } EVerificationState.WaitingForDone, EVerificationState.Done -> { - bottomSheetVerificationNoticeItem { - id("notice") - notice( - host.stringProvider.getString( - R.string.verification_conclusion_ok_notice - ) - .toEpoxyCharSequence() - ) - } - - bottomSheetVerificationBigImageItem { - id("image") - roomEncryptionTrustLevel(RoomEncryptionTrustLevel.Trusted) - } - + verifiedSuccessTile() bottomDone() } EVerificationState.Cancelled -> { @@ -167,6 +153,7 @@ class UserVerificationController @Inject constructor( } EVerificationState.HandledByOtherSession -> { // we should dismiss + bottomDone { listener?.onDone(false) } } } } @@ -176,64 +163,64 @@ class UserVerificationController @Inject constructor( } } - private fun renderStartTransactionOptions(state: UserVerificationViewState, request: PendingVerificationRequest) { - val scanCodeInstructions = stringProvider.getString(R.string.verification_scan_notice) - val host = this - val scanOtherCodeTitle = stringProvider.getString(R.string.verification_scan_their_code) - val compareEmojiSubtitle = stringProvider.getString(R.string.verification_scan_emoji_subtitle) - - bottomSheetVerificationNoticeItem { - id("notice") - notice(scanCodeInstructions.toEpoxyCharSequence()) - } - - if (request.weShouldDisplayQRCode && !request.qrCodeText.isNullOrEmpty()) { - bottomSheetVerificationQrCodeItem { - id("qr") - data(request.qrCodeText!!) - } - - bottomSheetDividerItem { - id("sep0") - } - } - - if (request.weShouldShowScanOption) { - bottomSheetVerificationActionItem { - id("openCamera") - title(scanOtherCodeTitle) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - iconRes(R.drawable.ic_camera) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - listener { host.listener?.openCamera() } - } - - bottomSheetDividerItem { - id("sep1") - } - - bottomSheetVerificationActionItem { - id("openEmoji") - title(host.stringProvider.getString(R.string.verification_scan_emoji_title)) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) - subTitle(compareEmojiSubtitle) - iconRes(R.drawable.ic_arrow_right) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) - listener { host.listener?.doVerifyBySas() } - } - } else if (request.isSasSupported) { - bottomSheetVerificationActionItem { - id("openEmoji") - title(host.stringProvider.getString(R.string.verification_no_scan_emoji_title)) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - iconRes(R.drawable.ic_arrow_right) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) - listener { host.listener?.doVerifyBySas() } - } - } else { - // ??? can this happen - } - } +// private fun renderStartTransactionOptions(request: PendingVerificationRequest) { +// val scanCodeInstructions = stringProvider.getString(R.string.verification_scan_notice) +// val host = this +// val scanOtherCodeTitle = stringProvider.getString(R.string.verification_scan_their_code) +// val compareEmojiSubtitle = stringProvider.getString(R.string.verification_scan_emoji_subtitle) +// +// bottomSheetVerificationNoticeItem { +// id("notice") +// notice(scanCodeInstructions.toEpoxyCharSequence()) +// } +// +// if (request.weShouldDisplayQRCode && !request.qrCodeText.isNullOrEmpty()) { +// bottomSheetVerificationQrCodeItem { +// id("qr") +// data(request.qrCodeText!!) +// } +// +// bottomSheetDividerItem { +// id("sep0") +// } +// } +// +// if (request.weShouldShowScanOption) { +// bottomSheetVerificationActionItem { +// id("openCamera") +// title(scanOtherCodeTitle) +// titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) +// iconRes(R.drawable.ic_camera) +// iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) +// listener { host.listener?.openCamera() } +// } +// +// bottomSheetDividerItem { +// id("sep1") +// } +// +// bottomSheetVerificationActionItem { +// id("openEmoji") +// title(host.stringProvider.getString(R.string.verification_scan_emoji_title)) +// titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) +// subTitle(compareEmojiSubtitle) +// iconRes(R.drawable.ic_arrow_right) +// iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) +// listener { host.listener?.doVerifyBySas() } +// } +// } else if (request.isSasSupported) { +// bottomSheetVerificationActionItem { +// id("openEmoji") +// title(host.stringProvider.getString(R.string.verification_no_scan_emoji_title)) +// titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) +// iconRes(R.drawable.ic_arrow_right) +// iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) +// listener { host.listener?.doVerifyBySas() } +// } +// } else { +// // ??? can this happen +// } +// } private fun renderActiveTransaction(state: UserVerificationViewState) { val transaction = state.startedTransaction @@ -343,118 +330,6 @@ class UserVerificationController @Inject constructor( } } - private fun renderSasTransaction(transaction: VerificationTransactionData.SasTransactionData) { - val host = this - when (val txState = transaction.state) { - SasTransactionState.SasShortCodeReady -> { - buildEmojiItem(transaction.emojiCodeRepresentation.orEmpty()) - } - is SasTransactionState.SasMacReceived -> { - if (!txState.codeConfirmed) { - buildEmojiItem(transaction.emojiCodeRepresentation.orEmpty()) - } else { - // waiting - bottomSheetVerificationWaitingItem { - id("waiting") - title(host.stringProvider.getString(R.string.please_wait)) - } - } - } - is SasTransactionState.Cancelled, - is SasTransactionState.Done -> { - // should show request status - } - else -> { - // waiting - bottomSheetVerificationWaitingItem { - id("waiting") - title(host.stringProvider.getString(R.string.please_wait)) - } - } - } - } - - private fun renderCancel(cancelCode: CancelCode) { - val host = this - when (cancelCode) { - CancelCode.QrCodeInvalid -> { - // TODO - } - CancelCode.MismatchedUser, - CancelCode.MismatchedSas, - CancelCode.MismatchedCommitment, - CancelCode.MismatchedKeys -> { - bottomSheetVerificationNoticeItem { - id("notice") - notice(host.stringProvider.getString(R.string.verification_conclusion_not_secure).toEpoxyCharSequence()) - } - - bottomSheetVerificationBigImageItem { - id("image") - roomEncryptionTrustLevel(RoomEncryptionTrustLevel.Warning) - } - - bottomSheetVerificationNoticeItem { - id("warning_notice") - notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised)).toEpoxyCharSequence()) - } - } - else -> { - bottomSheetVerificationNoticeItem { - id("notice_cancelled") - notice(host.stringProvider.getString(R.string.verify_cancelled_notice).toEpoxyCharSequence()) - } - } - } - } - - private fun buildEmojiItem(emoji: List) { - val host = this - bottomSheetVerificationNoticeItem { - id("notice") - notice(host.stringProvider.getString(R.string.verification_emoji_notice).toEpoxyCharSequence()) - } - - bottomSheetVerificationEmojisItem { - id("emojis") - emojiRepresentation0(emoji[0]) - emojiRepresentation1(emoji[1]) - emojiRepresentation2(emoji[2]) - emojiRepresentation3(emoji[3]) - emojiRepresentation4(emoji[4]) - emojiRepresentation5(emoji[5]) - emojiRepresentation6(emoji[6]) - } - - buildSasCodeActions() - } - - private fun buildSasCodeActions() { - val host = this - bottomSheetDividerItem { - id("sepsas0") - } - bottomSheetVerificationActionItem { - id("ko") - title(host.stringProvider.getString(R.string.verification_sas_do_not_match)) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) - iconRes(R.drawable.ic_check_off) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) - listener { host.listener?.onDoNotMatchButtonTapped() } - } - bottomSheetDividerItem { - id("sepsas1") - } - bottomSheetVerificationActionItem { - id("ok") - title(host.stringProvider.getString(R.string.verification_sas_match)) - titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - iconRes(R.drawable.ic_check_on) - iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - listener { host.listener?.onMatchButtonTapped() } - } - } - private fun bottomDone() { val host = this bottomSheetDividerItem { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationFragment.kt index a7275d70b6..71925c1c31 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationFragment.kt @@ -41,7 +41,7 @@ import javax.inject.Inject @AndroidEntryPoint class UserVerificationFragment : VectorBaseFragment(), - UserVerificationController.InteractionListener { + BaseEpoxyVerificationController.InteractionListener { @Inject lateinit var controller: UserVerificationController diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationViewModel.kt index e5c2d7b40e..ef2e8a8994 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/user/UserVerificationViewModel.kt @@ -59,8 +59,8 @@ import timber.log.Timber data class UserVerificationViewState( val pendingRequest: Async = Uninitialized, - val startedTransaction: Async = Uninitialized, // need something immutable for state to work properly, VerificationTransaction is not + val startedTransaction: Async = Uninitialized, val otherUserMxItem: MatrixItem, val otherUserId: String, val otherDeviceId: String? = null, @@ -103,7 +103,7 @@ sealed class VerificationTransactionData( ) : VerificationTransactionData(transactionId, otherUserId) } -private fun VerificationTransaction.toDataClass(): VerificationTransactionData? { +fun VerificationTransaction.toDataClass(): VerificationTransactionData? { return when (this) { is SasVerificationTransaction -> { VerificationTransactionData.SasTransactionData( @@ -255,7 +255,6 @@ class UserVerificationViewModel @AssistedInject constructor( } } } - VerificationAction.CancelledFromSsss -> TODO() is VerificationAction.GotItConclusion -> { // just dismiss _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) @@ -369,7 +368,6 @@ class UserVerificationViewModel @AssistedInject constructor( } } } - VerificationAction.SkipVerification -> TODO() is VerificationAction.StartSASVerification -> { withState { state -> val request = state.pendingRequest.invoke() ?: return@withState @@ -379,8 +377,14 @@ class UserVerificationViewModel @AssistedInject constructor( } } } - VerificationAction.VerifyFromPassphrase -> TODO() - VerificationAction.SecuredStorageHasBeenReset -> TODO() + VerificationAction.CancelledFromSsss, + VerificationAction.SkipVerification, + VerificationAction.VerifyFromPassphrase, + VerificationAction.SecuredStorageHasBeenReset, + VerificationAction.FailedToGetKeysFrom4S -> { + // Not applicable for user verification + } + VerificationAction.RequestSelfVerification -> TODO() } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/user/VerificationEpoxyExt.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/user/VerificationEpoxyExt.kt new file mode 100644 index 0000000000..661e9ab7b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/user/VerificationEpoxyExt.kt @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2022 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.app.features.crypto.verification.user + +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.bottomSheetDividerItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationEmojisItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem +import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState + +fun BaseEpoxyVerificationController.verifiedSuccessTile() { + val host = this + bottomSheetVerificationNoticeItem { + id("notice_done") + notice( + host.stringProvider.getString( + R.string.verification_conclusion_ok_notice + ) + .toEpoxyCharSequence() + ) + } + bottomSheetVerificationBigImageItem { + id("image") + roomEncryptionTrustLevel(RoomEncryptionTrustLevel.Trusted) + } +} + +fun BaseEpoxyVerificationController.bottomDone(listener: ClickListener) { + val host = this + bottomSheetDividerItem { + id("sep_done") + } + + bottomSheetVerificationActionItem { + id("done") + title(host.stringProvider.getString(R.string.done)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) +// listener { host.listener?.onDone(true) } + listener(listener) + } +} + +fun BaseEpoxyVerificationController.gotIt(listener: ClickListener) { + val host = this + bottomSheetDividerItem { + id("sep_gotit") + } + + bottomSheetVerificationActionItem { + id("gotit") + title(host.stringProvider.getString(R.string.action_got_it)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener(listener) + } +} + +fun BaseEpoxyVerificationController.renderStartTransactionOptions(request: PendingVerificationRequest, isMe: Boolean) { + val scanCodeInstructions: String + val scanOtherCodeTitle: String + val compareEmojiSubtitle: String + if (isMe) { + scanCodeInstructions = stringProvider.getString(R.string.verification_scan_self_notice) + scanOtherCodeTitle = stringProvider.getString(R.string.verification_scan_with_this_device) + compareEmojiSubtitle = stringProvider.getString(R.string.verification_scan_self_emoji_subtitle) + } else { + scanCodeInstructions = stringProvider.getString(R.string.verification_scan_notice) + scanOtherCodeTitle = stringProvider.getString(R.string.verification_scan_their_code) + compareEmojiSubtitle = stringProvider.getString(R.string.verification_scan_emoji_subtitle) + } + val host = this + + bottomSheetVerificationNoticeItem { + id("notice") + notice(scanCodeInstructions.toEpoxyCharSequence()) + } + + if (request.weShouldDisplayQRCode && !request.qrCodeText.isNullOrEmpty()) { + bottomSheetVerificationQrCodeItem { + id("qr") + data(request.qrCodeText!!) + } + + bottomSheetDividerItem { + id("sep0") + } + } + + if (request.weShouldShowScanOption) { + bottomSheetVerificationActionItem { + id("openCamera") + title(scanOtherCodeTitle) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + iconRes(R.drawable.ic_camera) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + listener { host.listener?.openCamera() } + } + + bottomSheetDividerItem { + id("sep1") + } + + bottomSheetVerificationActionItem { + id("openEmoji") + title(host.stringProvider.getString(R.string.verification_scan_emoji_title)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + subTitle(compareEmojiSubtitle) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { host.listener?.doVerifyBySas() } + } + } else if (request.isSasSupported) { + bottomSheetVerificationActionItem { + id("openEmoji") + title(host.stringProvider.getString(R.string.verification_no_scan_emoji_title)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + listener { host.listener?.doVerifyBySas() } + } + } else { + // ??? can this happen + } +} + +fun BaseEpoxyVerificationController.renderAcceptDeclineRequest() { + val host = this + bottomSheetDividerItem { + id("sep_accept_Decline") + } + bottomSheetVerificationActionItem { + id("accept_pr") + title(host.stringProvider.getString(R.string.action_accept)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + listener { host.listener?.acceptRequest() } + } + bottomSheetVerificationActionItem { + id("decline_pr") + title(host.stringProvider.getString(R.string.action_decline)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + listener { host.listener?.declineRequest() } + } +} + +fun BaseEpoxyVerificationController.renderCancel(cancelCode: CancelCode) { + val host = this + when (cancelCode) { + CancelCode.QrCodeInvalid -> { + // TODO + } + CancelCode.MismatchedUser, + CancelCode.MismatchedSas, + CancelCode.MismatchedCommitment, + CancelCode.MismatchedKeys -> { + bottomSheetVerificationNoticeItem { + id("notice") + notice(host.stringProvider.getString(R.string.verification_conclusion_not_secure).toEpoxyCharSequence()) + } + + bottomSheetVerificationBigImageItem { + id("image") + roomEncryptionTrustLevel(RoomEncryptionTrustLevel.Warning) + } + + bottomSheetVerificationNoticeItem { + id("warning_notice") + notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised)).toEpoxyCharSequence()) + } + } + else -> { + bottomSheetVerificationNoticeItem { + id("notice_cancelled") + notice(host.stringProvider.getString(R.string.verify_cancelled_notice).toEpoxyCharSequence()) + } + } + } +} + +fun BaseEpoxyVerificationController.buildEmojiItem(emoji: List) { + val host = this + bottomSheetVerificationNoticeItem { + id("notice") + notice(host.stringProvider.getString(R.string.verification_emoji_notice).toEpoxyCharSequence()) + } + + bottomSheetVerificationEmojisItem { + id("emojis") + emojiRepresentation0(emoji[0]) + emojiRepresentation1(emoji[1]) + emojiRepresentation2(emoji[2]) + emojiRepresentation3(emoji[3]) + emojiRepresentation4(emoji[4]) + emojiRepresentation5(emoji[5]) + emojiRepresentation6(emoji[6]) + } + + buildSasCodeActions() +} + +fun BaseEpoxyVerificationController.buildSasCodeActions() { + val host = this + bottomSheetDividerItem { + id("sepsas0") + } + bottomSheetVerificationActionItem { + id("ko") + title(host.stringProvider.getString(R.string.verification_sas_do_not_match)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + iconRes(R.drawable.ic_check_off) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + listener { host.listener?.onDoNotMatchButtonTapped() } + } + bottomSheetDividerItem { + id("sepsas1") + } + bottomSheetVerificationActionItem { + id("ok") + title(host.stringProvider.getString(R.string.verification_sas_match)) + titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + iconRes(R.drawable.ic_check_on) + iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + listener { host.listener?.onMatchButtonTapped() } + } +} + +fun BaseEpoxyVerificationController.renderSasTransaction(transaction: VerificationTransactionData.SasTransactionData) { + val host = this + when (val txState = transaction.state) { + SasTransactionState.SasShortCodeReady -> { + buildEmojiItem(transaction.emojiCodeRepresentation.orEmpty()) + } + is SasTransactionState.SasMacReceived -> { + if (!txState.codeConfirmed) { + buildEmojiItem(transaction.emojiCodeRepresentation.orEmpty()) + } else { + // waiting + bottomSheetVerificationWaitingItem { + id("waiting") + title(host.stringProvider.getString(R.string.please_wait)) + } + } + } + is SasTransactionState.Cancelled, + is SasTransactionState.Done -> { + // should show request status + } + else -> { + // waiting + bottomSheetVerificationWaitingItem { + id("waiting") + title(host.stringProvider.getString(R.string.please_wait)) + } + } + } +} + +class VerificationEpoxyExt diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index ed274a31d3..0c65cf51c8 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -64,7 +64,6 @@ import im.vector.app.features.home.room.list.home.release.ReleaseNotesActivity import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.navigation.Navigator -import im.vector.app.features.navigation.SettingsActivityPayload import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.onboarding.AuthenticationDescription import im.vector.app.features.permalink.NavigationInterceptor @@ -77,6 +76,7 @@ import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.rageshake.ReportType import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler +import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.spaces.SpaceCreationActivity @@ -87,9 +87,11 @@ import im.vector.app.features.spaces.share.ShareSpaceBottomSheet import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.workers.signout.ServerBackupStatusViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.sync.InitialSyncStrategy @@ -452,8 +454,16 @@ class HomeActivity : R.string.crosssigning_verify_this_session, R.string.confirm_your_identity ) { - TODO() - // it.navigator.waitSessionVerification(it) + // check first if it's not an outdated request? + activeSessionHolder.getSafeActiveSession()?.let { session -> + session.coroutineScope.launch { + if (!session.cryptoService().crossSigningService().isCrossSigningVerified()) { + withContext(Dispatchers.Main) { + it.navigator.requestSelfSessionVerification(it) + } + } + } + } } } @@ -464,11 +474,11 @@ class HomeActivity : R.string.crosssigning_verify_this_session, R.string.confirm_your_identity ) { - navigator.openSettings(this, SettingsActivityPayload.SecurityPrivacy) +// navigator.openSettings(this, SettingsActivityPayload.SecurityPrivacy) // if (event.waitForIncomingRequest) { // //it.navigator.waitSessionVerification(it) // } else { -// it.navigator.requestSelfSessionVerification(it) + it.navigator.requestSelfSessionVerification(it) // } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index 4147cf7186..dba4a66918 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -23,7 +23,7 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents data class CurrentSessionNotVerified( val userItem: MatrixItem.UserItem, - val waitForIncomingRequest: Boolean = true, +// val waitForIncomingRequest: Boolean = true, ) : HomeActivityViewEvents data class CurrentSessionCannotBeVerified( val userItem: MatrixItem.UserItem, diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index c49dc6a168..3059187045 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -396,8 +396,6 @@ class HomeActivityViewModel @AssistedInject constructor( _viewEvents.post( HomeActivityViewEvents.CurrentSessionNotVerified( session.getUserOrDefault(session.myUserId).toMatrixItem(), - // Always send request instead of waiting for an incoming as per recent EW changes - false ) ) } else { diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 5280f0feea..95bf74f1a2 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -52,6 +52,7 @@ import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.crypto.verification.self.SelfVerificationBottomSheet import im.vector.app.features.devtools.RoomDevToolActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.arguments.TimelineArgs @@ -104,6 +105,7 @@ import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgsBuilder import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.getRoom @@ -266,12 +268,13 @@ class DefaultNavigator @Inject constructor( override fun requestSelfSessionVerification(context: Context) { coroutineScope.launch { val session = sessionHolder.getSafeActiveSession() ?: return@launch - val otherSessions = session.cryptoService() - .getCryptoDeviceInfoList(session.myUserId) - .filter { it.deviceId != session.sessionParams.deviceId } - .map { it.deviceId } +// val otherSessions = session.cryptoService() +// .getCryptoDeviceInfoList(session.myUserId) +// .filter { it.deviceId != session.sessionParams.deviceId } +// .map { it.deviceId } if (context is AppCompatActivity) { - TODO() + SelfVerificationBottomSheet.verifyOwnUntrustedDevice() + .show(context.supportFragmentManager, "VERIF") // if (otherSessions.isNotEmpty()) { // val pr = session.cryptoService().verificationService().requestSelfKeyVerification( // supportedVerificationMethodsProvider.provide()) @@ -285,11 +288,13 @@ class DefaultNavigator @Inject constructor( } } -// override fun waitSessionVerification(fragmentActivity: FragmentActivity) { + override fun showIncomingSelfVerification(fragmentActivity: FragmentActivity, transactionId: String) { // val session = sessionHolder.getSafeActiveSession() ?: return -// VerificationBottomSheet.forSelfVerification(session) -// .show(fragmentActivity.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) -// } + coroutineScope.launch(Dispatchers.Main) { + SelfVerificationBottomSheet.forTransaction(transactionId) + .show(fragmentActivity.supportFragmentManager, "SELF_VERIF_TAG") + } + } override fun upgradeSessionSecurity(fragmentActivity: FragmentActivity, initCrossSigningOnly: Boolean) { BootstrapBottomSheet.show( diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 9be2d4d274..23fbedf573 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -83,7 +83,7 @@ interface Navigator { fun requestSelfSessionVerification(context: Context) -// fun waitSessionVerification(fragmentActivity: FragmentActivity) + fun showIncomingSelfVerification(fragmentActivity: FragmentActivity, transactionId: String) fun upgradeSessionSecurity(fragmentActivity: FragmentActivity, initCrossSigningOnly: Boolean)