diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 71ccf5b234..42045d1579 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -2287,6 +2287,7 @@
Verify %s
Verified %s
Waiting for %s…
+ Verifying from Secure Key or Phrase…
Messages in this room are not end-to-end encrypted.
Messages here are not end-to-end encrypted.
Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.
@@ -2396,6 +2397,8 @@
Unable to verify this device
You won’t be able to access encrypted message history. Reset your Secure Message Backup and verification keys to start fresh.
+ Verify with another device
+ Verify your identity to access encrypted messages and prove your identity to others.
Use an existing session to verify this one, granting it access to encrypted messages.
Verify
diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index f7642c7e77..665cc50627 100644
--- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -63,7 +63,7 @@ import javax.inject.Inject
@SessionScope
internal class DefaultVerificationService @Inject constructor(
@UserId private val userId: String,
- @DeviceId private val deviceId: String?,
+ @DeviceId private val myDeviceId: String?,
private val cryptoStore: IMXCryptoStore,
// private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
// private val secretShareManager: SecretShareManager,
@@ -582,7 +582,7 @@ internal class DefaultVerificationService @Inject constructor(
} else if (userId > otherUserId) {
return true
} else {
- return otherDeviceId < deviceId ?: ""
+ return otherDeviceId < myDeviceId ?: ""
}
}
@@ -1299,8 +1299,10 @@ internal class DefaultVerificationService @Inject constructor(
override suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest {
// TODO refactor this with the DM one
- val targetDevices = otherDeviceId?.let { listOf(it) } ?: cryptoStore.getUserDevices(otherUserId)
- ?.values?.map { it.deviceId }.orEmpty()
+ val targetDevices = otherDeviceId?.let { listOf(it) }
+ ?: cryptoStore.getUserDevices(otherUserId)
+ ?.filter { it.key != myDeviceId }
+ ?.values?.map { it.deviceId }.orEmpty()
Timber.i("## Requesting verification to user: $otherUserId with device list $targetDevices")
diff --git a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt
index b08bd58c5c..994fc2f33b 100644
--- a/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt
+++ b/matrix-sdk-android/src/kotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActor.kt
@@ -1354,7 +1354,14 @@ internal class VerificationActor @AssistedInject constructor(
}
}
- val methodValues = if (verificationTrustBackend.getMyTrustedMasterKeyBase64() != null) {
+ // XXX We should probably throw here if you try to verify someone else from an untrusted session
+ val shouldShowQROption = if (msg.otherUserId == myUserId) {
+ true
+ } else {
+ // It's verifying someone else, I should trust my key before doing it?
+ verificationTrustBackend.getUserMasterKeyBase64(myUserId) != null
+ }
+ val methodValues = if (shouldShowQROption) {
// Add reciprocate method if application declares it can scan or show QR codes
// Not sure if it ok to do that (?)
val reciprocateMethod = msg.methods
@@ -1452,8 +1459,11 @@ internal class VerificationActor @AssistedInject constructor(
cancelRequest(matchingRequest, CancelCode.UnexpectedMessage)
return
}
- // for room verification
- if (msg.fromUser == myUserId && msg.readyInfo.fromDevice != myDevice) {
+ // for room verification (user)
+ // TODO if room and incoming I should check that right?
+ // actually it will not reach that point? handleReadyByAnotherOfMySessionReceived would be called instead? and
+ // the actor never sees event send by me in rooms
+ if (matchingRequest.otherUserId != myUserId && msg.fromUser == myUserId && msg.readyInfo.fromDevice != myDevice) {
// it's a ready from another of my devices, so we should just
// ignore following messages related to that request
Timber.tag(loggerTag.value)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt
index 51df8c1548..0f4ac1bdda 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationEvent.kt
@@ -16,11 +16,11 @@
package org.matrix.android.sdk.api.session.crypto.verification
-sealed class VerificationEvent(val transactionId: String) {
- data class RequestAdded(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId)
- data class RequestUpdated(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId)
- data class TransactionAdded(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId)
- data class TransactionUpdated(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId)
+sealed class VerificationEvent(val transactionId: String, val otherUserId: String) {
+ data class RequestAdded(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId, request.otherUserId)
+ data class RequestUpdated(val request: PendingVerificationRequest) : VerificationEvent(request.transactionId, request.otherUserId)
+ data class TransactionAdded(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId, transaction.otherUserId)
+ data class TransactionUpdated(val transaction: VerificationTransaction) : VerificationEvent(transaction.transactionId, transaction.otherUserId)
}
fun VerificationEvent.getRequest(): PendingVerificationRequest? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt
index 7c5612dc0b..ac09fe884d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationService.kt
@@ -58,7 +58,7 @@ interface VerificationService {
* If no specific device should be verified, but we would like to request
* verification from all our devices, use [requestSelfKeyVerification] instead.
*/
- suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest?
+ suspend fun requestDeviceVerification(methods: List, otherUserId: String, otherDeviceId: String?): PendingVerificationRequest
/**
* Request key verification with another user via room events (instead of the to-device API).
diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt
index 8351d021b8..493a5c13a9 100644
--- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt
+++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/FakeCryptoStoreForVerification.kt
@@ -100,6 +100,7 @@ internal class FakeCryptoStoreForVerification(private val mode: StoreMode) {
val aliceMxId = "alice@example.com"
val bobMxId = "bob@example.com"
val bobDeviceId = "MKRJDSLYGA"
+ val bobDeviceId2 = "RRIWTEKZEI"
val aliceDevice1Id = "MGDAADVDMG"
@@ -147,6 +148,22 @@ internal class FakeCryptoStoreForVerification(private val mode: StoreMode) {
unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios")
)
+ val aBobDevice2 = CryptoDeviceInfo(
+ deviceId = bobDeviceId2,
+ userId = bobMxId,
+ algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
+ keys = mapOf(
+ "curve25519:$bobDeviceId" to "mE4WKAcyRRv7Gk1IDIVm0lZNzb8g9YL2eRQZUHmkkCI",
+ "ed25519:$bobDeviceId" to "yB/9LITHTqrvdXWDR2k6Qw/MDLUBWABlP9v2eYuqHPE",
+ ),
+ signatures = mapOf(
+ bobMxId to mapOf(
+ "ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA",
+ )
+ ),
+ unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Android")
+ )
+
private val aliceMSKBase = CryptoCrossSigningKey(
userId = aliceMxId,
usages = listOf(KeyUsage.MASTER.value),
diff --git a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt
index ceebfc5f6e..992e9ffcd0 100644
--- a/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt
+++ b/matrix-sdk-android/src/testKotlinCrypto/java/org/matrix/android/sdk/internal/crypto/verification/VerificationActorHelper.kt
@@ -22,6 +22,7 @@ import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady
@@ -53,8 +54,8 @@ internal class VerificationActorHelper {
var aliceChannel: SendChannel? = null
fun setUpActors(): TestData {
- val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { bobChannel }
- val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { aliceChannel }
+ val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { listOf(bobChannel) }
+ val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { listOf(aliceChannel) }
val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice)
val aliceActor = fakeActor(
@@ -82,7 +83,53 @@ internal class VerificationActorHelper {
)
}
- private fun mockTransportTo(fromUser: String, otherChannel: (() -> SendChannel?)): VerificationTransportLayer {
+ fun setupMultipleSessions() {
+ val aliceTargetChannels = mutableListOf>()
+ val aliceTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.aliceMxId) { aliceTargetChannels }
+ val bobTargetChannels = mutableListOf>()
+ val bobTransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bobTargetChannels }
+ val bob2TargetChannels = mutableListOf>()
+ val bob2TransportLayer = mockTransportTo(FakeCryptoStoreForVerification.bobMxId) { bob2TargetChannels }
+
+ val fakeAliceStore = FakeCryptoStoreForVerification(StoreMode.Alice)
+ val aliceActor = fakeActor(
+ actorAScope,
+ FakeCryptoStoreForVerification.aliceMxId,
+ fakeAliceStore.instance,
+ aliceTransportLayer,
+ )
+
+ val fakeBobStore1 = FakeCryptoStoreForVerification(StoreMode.Bob)
+ val bobActor = fakeActor(
+ actorBScope,
+ FakeCryptoStoreForVerification.bobMxId,
+ fakeBobStore1.instance,
+ bobTransportLayer
+ )
+
+ val actorCScope = CoroutineScope(SupervisorJob())
+ val fakeBobStore2 = FakeCryptoStoreForVerification(StoreMode.Bob)
+ every { fakeBobStore2.instance.getMyDeviceId() } returns FakeCryptoStoreForVerification.bobDeviceId2
+ every { fakeBobStore2.instance.getMyDevice() } returns FakeCryptoStoreForVerification.aBobDevice2
+
+ val bobActor2 = fakeActor(
+ actorCScope,
+ FakeCryptoStoreForVerification.bobMxId,
+ fakeBobStore2.instance,
+ bobTransportLayer
+ )
+
+ aliceTargetChannels.add(bobActor.channel)
+ aliceTargetChannels.add(bobActor2.channel)
+
+ bobTargetChannels.add(aliceActor.channel)
+ bobTargetChannels.add(bobActor2.channel)
+
+ bob2TargetChannels.add(aliceActor.channel)
+ bob2TargetChannels.add(bobActor.channel)
+ }
+
+ private fun mockTransportTo(fromUser: String, otherChannel: (() -> List?>)): VerificationTransportLayer {
return mockk {
coEvery { sendToOther(any(), any(), any()) } answers {
val request = firstArg()
@@ -93,64 +140,76 @@ internal class VerificationActorHelper {
when (type) {
EventType.KEY_VERIFICATION_READY -> {
val readyContent = info.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnReadyReceived(
- transactionId = request.requestId,
- fromUser = fromUser,
- viaRoom = request.roomId,
- readyInfo = readyContent as ValidVerificationInfoReady,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnReadyReceived(
+ transactionId = request.requestId,
+ fromUser = fromUser,
+ viaRoom = request.roomId,
+ readyInfo = readyContent as ValidVerificationInfoReady,
+ )
+ )
+ }
}
EventType.KEY_VERIFICATION_START -> {
val startContent = info.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnStartReceived(
- fromUser = fromUser,
- viaRoom = request.roomId,
- validVerificationInfoStart = startContent as ValidVerificationInfoStart,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnStartReceived(
+ fromUser = fromUser,
+ viaRoom = request.roomId,
+ validVerificationInfoStart = startContent as ValidVerificationInfoStart,
+ )
+ )
+ }
}
EventType.KEY_VERIFICATION_ACCEPT -> {
val content = info.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnAcceptReceived(
- fromUser = fromUser,
- viaRoom = request.roomId,
- validAccept = content as ValidVerificationInfoAccept,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnAcceptReceived(
+ fromUser = fromUser,
+ viaRoom = request.roomId,
+ validAccept = content as ValidVerificationInfoAccept,
+ )
+ )
+ }
}
EventType.KEY_VERIFICATION_KEY -> {
val content = info.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnKeyReceived(
- fromUser = fromUser,
- viaRoom = request.roomId,
- validKey = content as ValidVerificationInfoKey,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnKeyReceived(
+ fromUser = fromUser,
+ viaRoom = request.roomId,
+ validKey = content as ValidVerificationInfoKey,
+ )
+ )
+ }
}
EventType.KEY_VERIFICATION_MAC -> {
val content = info.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnMacReceived(
- fromUser = fromUser,
- viaRoom = request.roomId,
- validMac = content as ValidVerificationInfoMac,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnMacReceived(
+ fromUser = fromUser,
+ viaRoom = request.roomId,
+ validMac = content as ValidVerificationInfoMac,
+ )
+ )
+ }
}
EventType.KEY_VERIFICATION_DONE -> {
val content = info.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnDoneReceived(
- fromUser = fromUser,
- viaRoom = request.roomId,
- transactionId = (content as ValidVerificationDone).transactionId,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnDoneReceived(
+ fromUser = fromUser,
+ viaRoom = request.roomId,
+ transactionId = (content as ValidVerificationDone).transactionId,
+ )
+ )
+ }
}
}
}
@@ -167,26 +226,30 @@ internal class VerificationActorHelper {
val requestContent = content.toModel()?.copy(
transactionId = fakeEventId
)?.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnVerificationRequestReceived(
- requestContent!!,
- senderId = FakeCryptoStoreForVerification.aliceMxId,
- roomId = roomId,
- timeStamp = 0
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnVerificationRequestReceived(
+ requestContent!!,
+ senderId = FakeCryptoStoreForVerification.aliceMxId,
+ roomId = roomId,
+ timeStamp = 0
+ )
+ )
+ }
}
EventType.KEY_VERIFICATION_READY -> {
val readyContent = content.toModel()
?.asValidObject()
- otherChannel()?.send(
- VerificationIntent.OnReadyReceived(
- transactionId = readyContent!!.transactionId,
- fromUser = fromUser,
- viaRoom = roomId,
- readyInfo = readyContent,
- )
- )
+ otherChannel().onEach {
+ it?.send(
+ VerificationIntent.OnReadyReceived(
+ transactionId = readyContent!!.transactionId,
+ fromUser = fromUser,
+ viaRoom = roomId,
+ readyInfo = readyContent,
+ )
+ )
+ }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index 16c423da5b..fb8a05e384 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -31,6 +31,7 @@ import im.vector.app.features.createdirect.CreateDirectRoomViewModel
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel
import im.vector.app.features.crypto.quads.SharedSecureStorageViewModel
import im.vector.app.features.crypto.recover.BootstrapSharedViewModel
+import im.vector.app.features.crypto.verification.self.SelfVerificationViewModel
import im.vector.app.features.crypto.verification.user.UserVerificationViewModel
import im.vector.app.features.devtools.RoomDevToolViewModel
import im.vector.app.features.discovery.DiscoverySettingsViewModel
@@ -594,6 +595,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(UserVerificationViewModel::class)
fun userVerificationBottomSheetViewModelFactory(factory: UserVerificationViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(SelfVerificationViewModel::class)
+ fun selfVerificationBottomSheetViewModelFactory(factory: SelfVerificationViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
@Binds
@IntoMap
@MavericksViewModelKey(CreatePollViewModel::class)
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
index 3a3797af22..4e60a6d831 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
@@ -187,8 +187,9 @@ class IncomingVerificationRequestHandler @Inject constructor(
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let {
val roomId = pr.roomId
if (roomId.isNullOrBlank()) {
- // TODO
- // it.navigator.waitSessionVerification(it)
+ if (pr.otherUserId == session?.myUserId) {
+ it.navigator.showIncomingSelfVerification(it, pr.transactionId)
+ }
} else {
it.navigator.openRoom(
context = it,
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt
index 4201277f4d..55b09d4a4b 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt
@@ -21,6 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction
// TODO Remove otherUserId and transactionId when it's not necessary. Should be known by the ViewModel, no?
sealed class VerificationAction : VectorViewModelAction {
object RequestVerificationByDM : VerificationAction()
+ object RequestSelfVerification : VerificationAction()
object StartSASVerification : VerificationAction()
data class RemoteQrCodeScanned(val otherUserId: String, val transactionId: String, val scannedData: String) : VerificationAction()
object OtherUserScannedSuccessfully : VerificationAction()
@@ -28,6 +29,7 @@ sealed class VerificationAction : VectorViewModelAction {
object SASMatchAction : VerificationAction()
object SASDoNotMatchAction : VerificationAction()
data class GotItConclusion(val verified: Boolean) : VerificationAction()
+ object FailedToGetKeysFrom4S : VerificationAction()
object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction()
object ReadyPendingVerification : VerificationAction()
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationBottomSheet.kt
new file mode 100644
index 0000000000..9fabdc3d12
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationBottomSheet.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.crypto.verification.self
+
+import android.app.Activity
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import im.vector.app.R
+import im.vector.app.core.extensions.commitTransaction
+import im.vector.app.core.extensions.registerStartForActivityResult
+import im.vector.app.core.extensions.toMvRxBundle
+import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
+import im.vector.app.databinding.BottomSheetVerificationBinding
+import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
+import im.vector.app.features.crypto.verification.VerificationAction
+import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents
+import kotlinx.parcelize.Parcelize
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import kotlin.reflect.KClass
+
+class SelfVerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
+
+ override val showExpanded = true
+
+ @Parcelize
+ data class Args(
+ // when verifying a new session from an existing safe one
+ val targetDevice: String? = null,
+ // when we started from an incoming request
+ val transactionId: String? = null,
+ ) : Parcelable
+
+ private val viewModel by fragmentViewModel(SelfVerificationViewModel::class)
+
+ override fun getBinding(
+ inflater: LayoutInflater,
+ container: ViewGroup?
+ ) = BottomSheetVerificationBinding.inflate(inflater, container, false)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ showFragment(SelfVerificationFragment::class)
+ }
+
+ private val secretStartForActivityResult = registerStartForActivityResult { activityResult ->
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ val result = activityResult.data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
+ val reset = activityResult.data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false
+ if (result != null) {
+ viewModel.handle(VerificationAction.GotResultFromSsss(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
+ } else if (reset) {
+ // all have been reset, so we are verified?
+ viewModel.handle(VerificationAction.SecuredStorageHasBeenReset)
+ }
+ } else {
+ viewModel.handle(VerificationAction.CancelledFromSsss)
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.observeViewEvents { event ->
+ when (event) {
+ VerificationBottomSheetViewEvents.AccessSecretStore -> {
+ secretStartForActivityResult.launch(
+ SharedSecureStorageActivity.newReadIntent(
+ requireContext(),
+ null, // use default key
+ listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
+ SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
+ )
+ )
+ }
+ VerificationBottomSheetViewEvents.Dismiss -> {
+ dismiss()
+ }
+ VerificationBottomSheetViewEvents.GoToSettings -> {
+ // nop for user verificaiton
+ }
+ is VerificationBottomSheetViewEvents.ModalError -> {
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(getString(R.string.dialog_title_error))
+ .setMessage(event.errorMessage)
+ .setCancelable(false)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+ }
+ }
+ }
+
+ // viewModel.observeViewEvents {
+// when (it) {
+// is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
+// is VerificationBottomSheetViewEvents.AccessSecretStore -> {
+// secretStartForActivityResult.launch(
+// SharedSecureStorageActivity.newReadIntent(
+// requireContext(),
+// null, // use default key
+// listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
+// SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
+// )
+// )
+// }
+// is VerificationBottomSheetViewEvents.ModalError -> {
+// MaterialAlertDialogBuilder(requireContext())
+// .setTitle(getString(R.string.dialog_title_error))
+// .setMessage(it.errorMessage)
+// .setCancelable(false)
+// .setPositiveButton(R.string.ok, null)
+// .show()
+// Unit
+// }
+// VerificationBottomSheetViewEvents.GoToSettings -> {
+// dismiss()
+// (activity as? VectorBaseActivity<*>)?.let { activity ->
+// activity.navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY)
+// }
+// }
+// }
+// }
+// }
+
+ override fun invalidate() = withState(viewModel) { state ->
+// avatarRenderer.render(state.otherUserMxItem, views.otherUserAvatarImageView)
+ views.otherUserShield.isVisible = false
+ if (state.isThisSessionVerified) {
+ views.otherUserAvatarImageView.setImageResource(
+ R.drawable.ic_shield_trusted
+ )
+ views.otherUserNameText.text = getString(R.string.verification_profile_verify)
+ } else {
+ views.otherUserAvatarImageView.setImageResource(
+ R.drawable.ic_shield_black
+ )
+ views.otherUserNameText.text = getString(R.string.crosssigning_verify_this_session)
+ }
+
+ super.invalidate()
+ }
+
+ private fun showFragment(fragmentClass: KClass, argsParcelable: Parcelable? = null) {
+ if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
+ childFragmentManager.commitTransaction {
+ replace(
+ R.id.bottomSheetFragmentContainer,
+ fragmentClass.java,
+ argsParcelable?.toMvRxBundle(),
+ fragmentClass.simpleName
+ )
+ }
+ }
+ }
+
+ companion object {
+ fun verifyOwnUntrustedDevice(): SelfVerificationBottomSheet {
+ return SelfVerificationBottomSheet().apply {
+ setArguments(
+ Args()
+ )
+ }
+ }
+
+ fun forTransaction(transactionId: String): SelfVerificationBottomSheet {
+ return SelfVerificationBottomSheet().apply {
+ setArguments(
+ Args(transactionId = transactionId)
+ )
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt
new file mode 100644
index 0000000000..aee26922ca
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationController.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.crypto.verification.self
+
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import im.vector.app.R
+import im.vector.app.core.epoxy.bottomSheetDividerItem
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.crypto.verification.epoxy.bottomSheetSelfWaitItem
+import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
+import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
+import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
+import im.vector.app.features.crypto.verification.user.BaseEpoxyVerificationController
+import im.vector.app.features.crypto.verification.user.VerificationTransactionData
+import im.vector.app.features.crypto.verification.user.bottomDone
+import im.vector.app.features.crypto.verification.user.gotIt
+import im.vector.app.features.crypto.verification.user.renderAcceptDeclineRequest
+import im.vector.app.features.crypto.verification.user.renderCancel
+import im.vector.app.features.crypto.verification.user.renderSasTransaction
+import im.vector.app.features.crypto.verification.user.renderStartTransactionOptions
+import im.vector.app.features.crypto.verification.user.verifiedSuccessTile
+import im.vector.app.features.html.EventHtmlRenderer
+import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
+import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
+import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
+import timber.log.Timber
+import javax.inject.Inject
+
+class SelfVerificationController @Inject constructor(
+ stringProvider: StringProvider,
+ colorProvider: ColorProvider,
+ eventHtmlRenderer: EventHtmlRenderer,
+) : BaseEpoxyVerificationController(stringProvider, colorProvider, eventHtmlRenderer) {
+
+ interface InteractionListener : BaseEpoxyVerificationController.InteractionListener {
+ fun onClickRecoverFromPassphrase()
+ fun onClickSkip()
+ fun onClickResetSecurity()
+ fun onDoneFrom4S()
+ fun keysNotIn4S()
+ }
+
+ var state: SelfVerificationViewState? = null
+
+ fun update(state: SelfVerificationViewState) {
+ Timber.w("VALR controller updated $state")
+ this.state = state
+ requestModelBuild()
+ }
+
+ override fun buildModels() {
+ val state = this.state ?: return
+ when (state.pendingRequest) {
+ Uninitialized -> {
+ renderBaseNoActiveRequest(state)
+ }
+ else -> {
+ renderRequest(state)
+ }
+ }
+ }
+
+ private fun renderBaseNoActiveRequest(state: SelfVerificationViewState) {
+ if (state.verifyingFrom4SAction !is Uninitialized) {
+ render4SCheckState(state)
+ } else {
+ renderNoRequestStarted(state)
+ }
+ }
+
+ private fun renderRequest(state: SelfVerificationViewState) {
+ val host = this
+ when (state.pendingRequest) {
+ Uninitialized -> {
+ // let's add option to start one
+ val styledText = stringProvider.getString(R.string.verify_new_session_notice)
+
+ bottomSheetVerificationNoticeItem {
+ id("notice")
+ notice(styledText.toEpoxyCharSequence())
+ }
+
+ bottomSheetDividerItem {
+ id("sep")
+ }
+ bottomSheetVerificationActionItem {
+ id("start")
+ title(host.stringProvider.getString(R.string.start_verification))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
+ // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ listener { host.listener?.onClickOnVerificationStart() }
+ }
+ }
+ is Loading -> {
+ bottomSheetSelfWaitItem {
+ id("waiting")
+ }
+// bottomSheetVerificationWaitingItem {
+// id("waiting_pr_loading")
+// // title(host.stringProvider.getString(R.string.verification_request_waiting_for, state.otherUserMxItem.getBestName()))
+// }
+ }
+ is Success -> {
+ val pendingRequest = state.pendingRequest.invoke()
+ when (pendingRequest.state) {
+ EVerificationState.WaitingForReady -> {
+ bottomSheetSelfWaitItem {
+ id("waiting")
+ }
+ }
+ EVerificationState.Requested -> {
+ // add accept buttons?
+ renderAcceptDeclineRequest()
+ bottomSheetVerificationActionItem {
+ id("not me")
+ title(host.stringProvider.getString(R.string.verify_new_session_was_not_me))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ listener {
+ TODO()
+// host.listener?.wasNotMe()
+ }
+ }
+ }
+ EVerificationState.Ready -> {
+ // add start options
+ renderStartTransactionOptions(pendingRequest, true)
+ }
+ EVerificationState.Started,
+ EVerificationState.WeStarted -> {
+ // nothing to do, in this case the active transaction is shown
+ renderActiveTransaction(state)
+ }
+ EVerificationState.WaitingForDone,
+ EVerificationState.Done -> {
+ verifiedSuccessTile()
+ bottomDone {
+ listener?.onDone(true)
+ }
+ }
+ EVerificationState.Cancelled -> {
+ renderCancel(pendingRequest.cancelConclusion ?: CancelCode.User)
+ }
+ EVerificationState.HandledByOtherSession -> {
+ // we should dismiss
+ }
+ }
+ }
+ is Fail -> {
+ // TODO
+ }
+ }
+ }
+
+ private fun renderNoRequestStarted(state: SelfVerificationViewState) {
+ val host = this
+ bottomSheetVerificationNoticeItem {
+ id("notice")
+ notice(host.stringProvider.getString(R.string.verification_verify_identity).toEpoxyCharSequence())
+ }
+ bottomSheetDividerItem {
+ id("notice_div")
+ }
+ // Option to verify with another device
+ if (state.hasAnyOtherSession) {
+ bottomSheetVerificationActionItem {
+ id("start")
+ title(host.stringProvider.getString(R.string.verification_verify_with_another_device))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ listener { (host.listener as? InteractionListener)?.onClickOnVerificationStart() }
+ }
+
+ bottomSheetDividerItem {
+ id("start_div")
+ }
+ }
+
+ if (state.quadSContainsSecrets) {
+ bottomSheetVerificationActionItem {
+ id("passphrase")
+ title(host.stringProvider.getString(R.string.verification_cannot_access_other_session))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ subTitle(host.stringProvider.getString(R.string.verification_use_passphrase))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ listener { (host.listener as? InteractionListener)?.onClickRecoverFromPassphrase() }
+ }
+
+ bottomSheetDividerItem {
+ id("start_div")
+ }
+ }
+
+ // option to reset all
+ bottomSheetVerificationActionItem {
+ id("reset")
+ title(host.stringProvider.getString(R.string.bad_passphrase_key_reset_all_action))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ subTitle(host.stringProvider.getString(R.string.secure_backup_reset_all_no_other_devices))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
+ listener { (host.listener as? InteractionListener)?.onClickResetSecurity() }
+ }
+
+ if (!state.isVerificationRequired) {
+ bottomSheetDividerItem {
+ id("reset_div")
+ }
+
+ bottomSheetVerificationActionItem {
+ id("skip")
+ title(host.stringProvider.getString(R.string.action_skip))
+ titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
+ iconRes(R.drawable.ic_arrow_right)
+ iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
+ listener { (host.listener as? InteractionListener)?.onClickSkip() }
+ }
+ }
+ }
+
+ private fun render4SCheckState(state: SelfVerificationViewState) {
+ val host = this
+ when (val action = state.verifyingFrom4SAction) {
+ is Fail -> {
+ }
+ is Loading -> {
+ bottomSheetVerificationWaitingItem {
+ id("waiting")
+ title(host.stringProvider.getString(R.string.verification_request_waiting_for_recovery))
+ }
+ }
+ is Success -> {
+ val invoke = action.invoke()
+ if (invoke) {
+ verifiedSuccessTile()
+ bottomDone { (host.listener as? InteractionListener)?.onDoneFrom4S() }
+ } else {
+ bottomSheetVerificationNoticeItem {
+ id("notice_4s_failed'")
+ notice(
+ host.stringProvider.getString(
+ R.string.error_failed_to_import_keys
+ )
+ .toEpoxyCharSequence()
+ )
+ }
+ gotIt { (host.listener as? InteractionListener)?.keysNotIn4S() }
+ }
+ }
+ else -> {
+ // nop
+ }
+ }
+ }
+
+ private fun renderActiveTransaction(state: SelfVerificationViewState) {
+ val transaction = state.startedTransaction
+ val host = this
+ when (transaction) {
+ is Loading -> {
+ // Loading => We are starting a transaction
+ bottomSheetVerificationWaitingItem {
+ id("waiting")
+ title(host.stringProvider.getString(R.string.please_wait))
+ }
+ }
+ is Success -> {
+ // Success => There is an active transaction
+ renderTransaction(state, transaction = transaction.invoke())
+ }
+ is Fail -> {
+ // todo
+ }
+ is Uninitialized -> {
+ }
+ }
+ }
+
+ private fun renderTransaction(state: SelfVerificationViewState, transaction: VerificationTransactionData) {
+ when (transaction) {
+ is VerificationTransactionData.QrTransactionData -> {
+ // TODO
+ // renderQrTransaction(transaction, state.otherUserMxItem)
+ }
+ is VerificationTransactionData.SasTransactionData -> {
+ renderSasTransaction(transaction)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationFragment.kt
new file mode 100644
index 0000000000..67d57a2ae4
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationFragment.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.crypto.verification.self
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.mvrx.parentFragmentViewModel
+import com.airbnb.mvrx.withState
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.extensions.registerStartForActivityResult
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
+import im.vector.app.core.utils.checkPermissions
+import im.vector.app.core.utils.onPermissionDeniedDialog
+import im.vector.app.core.utils.registerForPermissionsResult
+import im.vector.app.databinding.BottomSheetVerificationChildFragmentBinding
+import im.vector.app.features.crypto.verification.VerificationAction
+import im.vector.app.features.qrcode.QrCodeScannerActivity
+import timber.log.Timber
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class SelfVerificationFragment : VectorBaseFragment(),
+ SelfVerificationController.InteractionListener {
+
+ @Inject lateinit var controller: SelfVerificationController
+
+ private val viewModel by parentFragmentViewModel(SelfVerificationViewModel::class)
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetVerificationChildFragmentBinding {
+ return BottomSheetVerificationChildFragmentBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupRecyclerView()
+ }
+
+ override fun onDestroyView() {
+ views.bottomSheetVerificationRecyclerView.cleanup()
+ controller.listener = null
+ super.onDestroyView()
+ }
+
+ private fun setupRecyclerView() {
+ views.bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
+ controller.listener = this
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ Timber.w("VALR: invalidate with State: ${state.pendingRequest}")
+ controller.update(state)
+ }
+
+ override fun onClickRecoverFromPassphrase() {
+ viewModel.handle(VerificationAction.VerifyFromPassphrase)
+ }
+
+ override fun onClickSkip() {
+ TODO("Not yet implemented")
+ }
+
+ override fun onClickResetSecurity() {
+ TODO("Not yet implemented")
+ }
+
+ override fun onDoneFrom4S() {
+ viewModel.handle(VerificationAction.GotItConclusion(true))
+ }
+
+ override fun keysNotIn4S() {
+ viewModel.handle(VerificationAction.FailedToGetKeysFrom4S)
+ }
+
+ override fun onClickOnVerificationStart() {
+ viewModel.handle(VerificationAction.RequestSelfVerification)
+ }
+
+ override fun onDone(b: Boolean) {
+ viewModel.handle(VerificationAction.GotItConclusion(b))
+ }
+
+ override fun onDoNotMatchButtonTapped() {
+ viewModel.handle(VerificationAction.SASDoNotMatchAction)
+ }
+
+ override fun onMatchButtonTapped() {
+ viewModel.handle(VerificationAction.SASMatchAction)
+ }
+
+ private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
+ if (allGranted) {
+ doOpenQRCodeScanner()
+ } else if (deniedPermanently) {
+ activity?.onPermissionDeniedDialog(R.string.denied_permission_camera)
+ }
+ }
+
+ override fun openCamera() {
+ if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
+ doOpenQRCodeScanner()
+ }
+ }
+
+ private fun doOpenQRCodeScanner() {
+ QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher)
+ }
+
+ private val scanActivityResultLauncher = registerStartForActivityResult { activityResult ->
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ val scannedQrCode = QrCodeScannerActivity.getResultText(activityResult.data)
+ val wasQrCode = QrCodeScannerActivity.getResultIsQrCode(activityResult.data)
+
+ if (wasQrCode && !scannedQrCode.isNullOrBlank()) {
+ onRemoteQrCodeScanned(scannedQrCode)
+ } else {
+ Timber.w("It was not a QR code, or empty result")
+ }
+ }
+ }
+
+ private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(viewModel) { state ->
+ viewModel.handle(
+ VerificationAction.RemoteQrCodeScanned(
+ state.pendingRequest.invoke()?.otherUserId.orEmpty(),
+ state.pendingRequest.invoke()?.transactionId.orEmpty(),
+ remoteQrCode
+ )
+ )
+ }
+
+ override fun doVerifyBySas() {
+ viewModel.handle(VerificationAction.StartSASVerification)
+ }
+
+ override fun onUserDeniesQrCodeScanned() {
+ viewModel.handle(VerificationAction.OtherUserDidNotScanned)
+ }
+
+ override fun onUserConfirmsQrCodeScanned() {
+ viewModel.handle(VerificationAction.OtherUserScannedSuccessfully)
+ }
+
+ override fun acceptRequest() {
+ viewModel.handle(VerificationAction.ReadyPendingVerification)
+ }
+
+ override fun declineRequest() {
+ viewModel.handle(VerificationAction.CancelPendingVerification)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt
new file mode 100644
index 0000000000..a46835dfdd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/self/SelfVerificationViewModel.kt
@@ -0,0 +1,428 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.crypto.verification.self
+
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.R
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
+import im.vector.app.features.crypto.verification.VerificationAction
+import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents
+import im.vector.app.features.crypto.verification.user.VerificationTransactionData
+import im.vector.app.features.crypto.verification.user.toDataClass
+import im.vector.app.features.raw.wellknown.getElementWellknown
+import im.vector.app.features.raw.wellknown.isSecureBackupRequired
+import im.vector.app.features.session.coroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
+import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils
+import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
+import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
+import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
+import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
+import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
+import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
+import org.matrix.android.sdk.api.session.crypto.verification.getRequest
+import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
+import org.matrix.android.sdk.api.util.fromBase64
+import timber.log.Timber
+
+data class SelfVerificationViewState(
+ val pendingRequest: Async = Uninitialized,
+ // need something immutable for state to work properly, VerificationTransaction is not
+ val startedTransaction: Async = Uninitialized,
+ val verifyingFrom4SAction: Async = Uninitialized,
+ val otherDeviceId: String? = null,
+ val transactionId: String? = null,
+ val currentDeviceCanCrossSign: Boolean = false,
+ val userWantsToCancel: Boolean = false,
+ val hasAnyOtherSession: Boolean = false,
+ val quadSContainsSecrets: Boolean = false,
+ val isVerificationRequired: Boolean = false,
+ val isThisSessionVerified: Boolean = false,
+) : MavericksState {
+
+ constructor(args: SelfVerificationBottomSheet.Args) : this(
+ transactionId = args.transactionId,
+ otherDeviceId = args.targetDevice,
+ )
+}
+
+class SelfVerificationViewModel @AssistedInject constructor(
+ @Assisted private val initialState: SelfVerificationViewState,
+ private val session: Session,
+ private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
+ private val rawService: RawService,
+ private val stringProvider: StringProvider,
+ private val matrix: Matrix,
+) :
+ VectorViewModel(initialState) {
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: SelfVerificationViewState): SelfVerificationViewModel
+ }
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ init {
+
+ if (initialState.transactionId != null) {
+ setState {
+ copy(pendingRequest = Loading())
+ }
+ viewModelScope.launch {
+ session.cryptoService().verificationService().getExistingVerificationRequest(session.myUserId, initialState.transactionId)?.let {
+ setState {
+ copy(pendingRequest = Success(it))
+ }
+ }
+ }
+ }
+ observeRequestsAndTransactions()
+ // This is async, but at this point should be in cache
+ // so it's ok to not wait until result
+ viewModelScope.launch(Dispatchers.IO) {
+ val wellKnown = rawService.getElementWellknown(session.sessionParams)
+ setState {
+ copy(isVerificationRequired = wellKnown?.isSecureBackupRequired().orFalse())
+ }
+ }
+
+ val hasAnyOtherSession = session.cryptoService()
+ .getCryptoDeviceInfo(session.myUserId)
+ .any {
+ it.deviceId != session.sessionParams.deviceId
+ }
+
+ setState {
+ copy(
+ currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
+ quadSContainsSecrets = session.sharedSecretStorageService().isRecoverySetup(),
+ hasAnyOtherSession = hasAnyOtherSession
+ )
+ }
+
+ viewModelScope.launch {
+ val isThisSessionVerified = session.cryptoService().crossSigningService().isCrossSigningVerified()
+ setState {
+ copy(
+ isThisSessionVerified = isThisSessionVerified,
+ )
+ }
+ }
+//
+ }
+
+ private fun observeRequestsAndTransactions() {
+ session.cryptoService().verificationService()
+ .requestEventFlow()
+ .filter {
+ it.otherUserId == session.myUserId
+ }
+ .onEach {
+ it.getRequest()?.let {
+ setState {
+ copy(
+ pendingRequest = Success(it),
+ )
+ }
+ }
+ it.getTransaction()?.let {
+ val dClass = it.toDataClass()
+ if (dClass != null) {
+ setState {
+ copy(
+ startedTransaction = Success(dClass),
+ )
+ }
+ } else {
+ setState {
+ copy(
+ startedTransaction = Fail(IllegalArgumentException("Unsupported Transaction")),
+ )
+ }
+ }
+ }
+ }
+ .launchIn(viewModelScope)
+ }
+
+ override fun handle(action: VerificationAction) {
+ when (action) {
+ VerificationAction.CancelPendingVerification -> {
+ withState { state ->
+ state.pendingRequest.invoke()?.let {
+ viewModelScope.launch {
+ session.cryptoService().verificationService()
+ .cancelVerificationRequest(it)
+ }
+ }
+ }
+ }
+ VerificationAction.CancelledFromSsss -> {
+ setState {
+ copy(verifyingFrom4SAction = Uninitialized)
+ }
+ }
+ is VerificationAction.GotItConclusion -> {
+ // just dismiss
+ _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
+ }
+ is VerificationAction.GotResultFromSsss -> handleSecretBackFromSSSS(action)
+ VerificationAction.OtherUserDidNotScanned -> {
+ withState { state ->
+ state.startedTransaction.invoke()?.let {
+ viewModelScope.launch {
+ val tx = session.cryptoService().verificationService()
+ .getExistingTransaction(it.otherUserId, it.transactionId)
+ as? QrCodeVerificationTransaction
+ tx?.otherUserDidNotScannedMyQrCode()
+ }
+ }
+ }
+ }
+ VerificationAction.OtherUserScannedSuccessfully -> {
+ withState { state ->
+ state.startedTransaction.invoke()?.let {
+ viewModelScope.launch {
+ val tx = session.cryptoService().verificationService()
+ .getExistingTransaction(it.otherUserId, it.transactionId)
+ as? QrCodeVerificationTransaction
+ tx?.otherUserScannedMyQrCode()
+ }
+ }
+ }
+ }
+ VerificationAction.ReadyPendingVerification -> {
+ withState { state ->
+ state.pendingRequest.invoke()?.let {
+ viewModelScope.launch {
+ session.cryptoService().verificationService()
+ .readyPendingVerification(
+ supportedVerificationMethodsProvider.provide(),
+ it.otherUserId, it.transactionId
+ )
+ }
+ }
+ }
+ }
+ is VerificationAction.RemoteQrCodeScanned -> {
+ setState {
+ copy(startedTransaction = Loading())
+ }
+ withState { state ->
+ val request = state.pendingRequest.invoke() ?: return@withState
+ viewModelScope.launch {
+ try {
+ session.cryptoService().verificationService()
+ .reciprocateQRVerification(
+ request.otherUserId,
+ request.transactionId,
+ action.scannedData
+ )
+ } catch (failure: Throwable) {
+ Timber.w(failure, "Failed to reciprocated")
+ setState {
+ copy(startedTransaction = Fail(failure))
+ }
+ }
+ }
+ }
+ }
+ is VerificationAction.SASDoNotMatchAction -> {
+ withState { state ->
+ viewModelScope.launch {
+ val transaction = session.cryptoService().verificationService()
+ .getExistingTransaction(session.myUserId, state.transactionId.orEmpty())
+ (transaction as? SasVerificationTransaction)?.shortCodeDoesNotMatch()
+ }
+ }
+ }
+ is VerificationAction.SASMatchAction -> {
+ withState { state ->
+ viewModelScope.launch {
+ val transaction = session.cryptoService().verificationService()
+ .getExistingTransaction(session.myUserId, state.transactionId.orEmpty())
+ (transaction as? SasVerificationTransaction)?.userHasVerifiedShortCode()
+ }
+ }
+ }
+ VerificationAction.SecuredStorageHasBeenReset -> TODO()
+ VerificationAction.SkipVerification -> TODO()
+ VerificationAction.StartSASVerification -> {
+ withState { state ->
+ val request = state.pendingRequest.invoke() ?: return@withState
+ viewModelScope.launch {
+ session.cryptoService().verificationService()
+ .startKeyVerification(VerificationMethod.SAS, session.myUserId, request.transactionId)
+ }
+ }
+ }
+ VerificationAction.VerifyFromPassphrase -> {
+ setState { copy(verifyingFrom4SAction = Loading()) }
+ _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore)
+ }
+ VerificationAction.FailedToGetKeysFrom4S -> {
+ setState {
+ copy(
+ verifyingFrom4SAction = Uninitialized,
+ quadSContainsSecrets = false
+ )
+ }
+ }
+ VerificationAction.RequestSelfVerification -> {
+ handleRequestVerification()
+ }
+ VerificationAction.RequestVerificationByDM -> {
+ // not applicable in self verification
+ }
+ }
+ }
+
+ private fun handleRequestVerification() {
+ setState {
+ copy(
+ pendingRequest = Loading()
+ )
+ }
+ var targetDevice: String? = null
+ withState { state ->
+ targetDevice = state.otherDeviceId
+ }
+ viewModelScope.launch {
+ try {
+ val request = session.cryptoService().verificationService().requestDeviceVerification(
+ supportedVerificationMethodsProvider.provide(),
+ session.myUserId,
+ targetDevice
+ )
+ setState {
+ copy(
+ pendingRequest = Success(request),
+ transactionId = request.transactionId
+ )
+ }
+ } catch (failure: Throwable) {
+ setState {
+ copy(
+ pendingRequest = Loading(),
+ )
+ }
+ }
+ }
+ }
+
+ private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ action.cypherData.fromBase64().inputStream().use { ins ->
+ val res = matrix.secureStorageService().loadSecureSecret