self verification basics

This commit is contained in:
Valere 2022-11-23 11:27:39 +01:00
parent 5b3e3a7019
commit d302fdc655
23 changed files with 1703 additions and 307 deletions

View File

@ -2287,6 +2287,7 @@
<string name="verification_verify_user">Verify %s</string>
<string name="verification_verified_user">Verified %s</string>
<string name="verification_request_waiting_for">Waiting for %s…</string>
<string name="verification_request_waiting_for_recovery">Verifying from Secure Key or Phrase…</string>
<string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string>
<string name="direct_room_profile_not_encrypted_subtitle">Messages here are not end-to-end encrypted.</string>
<string name="room_profile_encrypted_subtitle">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.</string>
@ -2396,6 +2397,8 @@
<string name="crosssigning_cannot_verify_this_session">Unable to verify this device</string>
<string name="crosssigning_cannot_verify_this_session_desc">You wont be able to access encrypted message history. Reset your Secure Message Backup and verification keys to start fresh.</string>
<string name="verification_verify_with_another_device">Verify with another device</string>
<string name="verification_verify_identity">Verify your identity to access encrypted messages and prove your identity to others.</string>
<string name="verification_open_other_to_verify">Use an existing session to verify this one, granting it access to encrypted messages.</string>
<string name="verification_profile_verify">Verify</string>

View File

@ -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<VerificationMethod>, 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")

View File

@ -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)

View File

@ -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? {

View File

@ -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<VerificationMethod>, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest?
suspend fun requestDeviceVerification(methods: List<VerificationMethod>, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest
/**
* Request key verification with another user via room events (instead of the to-device API).

View File

@ -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),

View File

@ -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<VerificationIntent>? = 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<VerificationIntent>?)): VerificationTransportLayer {
fun setupMultipleSessions() {
val aliceTargetChannels = mutableListOf<Channel<VerificationIntent>>()
val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { aliceTargetChannels }
val bobTargetChannels = mutableListOf<Channel<VerificationIntent>>()
val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bobTargetChannels }
val bob2TargetChannels = mutableListOf<Channel<VerificationIntent>>()
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<SendChannel<VerificationIntent>?>)): VerificationTransportLayer {
return mockk<VerificationTransportLayer> {
coEvery { sendToOther(any(), any(), any()) } answers {
val request = firstArg<KotlinVerificationRequest>()
@ -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<MessageVerificationRequestContent>()?.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<MessageVerificationReadyContent>()
?.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,
)
)
}
}
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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()

View File

@ -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<BottomSheetVerificationBinding>() {
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<out Fragment>, 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)
)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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<BottomSheetVerificationChildFragmentBinding>(),
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)
}
}

View File

@ -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<PendingVerificationRequest> = Uninitialized,
// need something immutable for state to work properly, VerificationTransaction is not
val startedTransaction: Async<VerificationTransactionData> = Uninitialized,
val verifyingFrom4SAction: Async<Boolean> = 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<SelfVerificationViewState, VerificationAction, VerificationBottomSheetViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<SelfVerificationViewModel, SelfVerificationViewState> {
override fun create(initialState: SelfVerificationViewState): SelfVerificationViewModel
}
companion object : MavericksViewModelFactory<SelfVerificationViewModel, SelfVerificationViewState> 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<Map<String, String>>(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<String, String>?) {
// 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")
}
}
}
}

View File

@ -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<EmojiRepresentation>) {
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 {

View File

@ -41,7 +41,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
UserVerificationController.InteractionListener {
BaseEpoxyVerificationController.InteractionListener {
@Inject lateinit var controller: UserVerificationController

View File

@ -59,8 +59,8 @@ import timber.log.Timber
data class UserVerificationViewState(
val pendingRequest: Async<PendingVerificationRequest> = Uninitialized,
val startedTransaction: Async<VerificationTransactionData> = Uninitialized,
// need something immutable for state to work properly, VerificationTransaction is not
val startedTransaction: Async<VerificationTransactionData> = 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()
}
}

View File

@ -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<EmojiRepresentation>) {
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

View File

@ -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)
// }
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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(

View File

@ -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)