self verification basics
This commit is contained in:
parent
5b3e3a7019
commit
d302fdc655
@ -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 won’t 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>
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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? {
|
||||
|
@ -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).
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -41,7 +41,7 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class UserVerificationFragment : VectorBaseFragment<BottomSheetVerificationChildFragmentBinding>(),
|
||||
UserVerificationController.InteractionListener {
|
||||
BaseEpoxyVerificationController.InteractionListener {
|
||||
|
||||
@Inject lateinit var controller: UserVerificationController
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
@ -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)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user