From 6bf3a703df3f9bc916db938564092e91688af979 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 30 Dec 2019 17:20:43 +0100 Subject: [PATCH] BottomSheet UX --- .../crypto/sas/SasVerificationService.kt | 4 + .../tasks/RoomVerificationUpdateTask.kt | 164 +++++++++++++++ .../DefaultSasVerificationService.kt | 113 +++++++++- .../PendingVerificationRequest.kt | 39 ++++ .../SASVerificationTransaction.kt | 3 +- .../VerificationMessageLiveObserver.kt | 121 +---------- .../im/vector/riotx/core/di/FragmentModule.kt | 23 +++ .../vector/riotx/core/utils/SpannableUtils.kt | 58 ++++++ .../SASVerificationCodeFragment.kt | 164 +++++++++++++++ .../SASVerificationCodeViewModel.kt | 170 +++++++++++++++ .../SASVerificationStartFragment.kt | 2 +- .../verification/SasVerificationViewModel.kt | 1 + .../verification/VerificationBottomSheet.kt | 195 ++++++++++++++++++ .../VerificationBottomSheetViewModel.kt | 178 ++++++++++++++++ .../VerificationChooseMethodFragment.kt | 69 +++++++ .../VerificationChooseMethodViewModel.kt | 77 +++++++ .../VerificationConclusionFragment.kt | 73 +++++++ .../VerificationConclusionViewModel.kt | 61 ++++++ .../VerificationRequestFragment.kt | 83 ++++++++ .../VerificationRequestViewModel.kt | 109 ++++++++++ .../home/room/detail/RoomDetailFragment.kt | 21 +- .../home/room/detail/RoomDetailViewModel.kt | 29 ++- .../timeline/factory/MessageItemFactory.kt | 24 +-- .../timeline/factory/TimelineItemFactory.kt | 1 + .../timeline/format/NoticeEventFormatter.kt | 1 + .../helper/TimelineDisplayableEvents.kt | 3 +- .../fragment_bottom_sas_verification_code.xml | 195 ++++++++++++++++++ .../fragment_verification_choose_method.xml | 117 +++++++++++ .../fragment_verification_conclusion.xml | 65 ++++++ vector/src/main/res/values/strings.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 22 +- 31 files changed, 2034 insertions(+), 153 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt create mode 100644 vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml create mode 100644 vector/src/main/res/layout/fragment_verification_choose_method.xml create mode 100644 vector/src/main/res/layout/fragment_verification_conclusion.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt index 3c3c43dbd4..cc3b57da20 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -39,6 +39,10 @@ interface SasVerificationService { fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? + fun getExistingVerificationRequest(otherUser: String): List? + + fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? + /** * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS * @see beginKeyVerification diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt new file mode 100644 index 0000000000..0cbefd5f3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.tasks + +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.task.Task +import io.realm.RealmConfiguration +import timber.log.Timber +import java.util.* +import javax.inject.Inject + +internal interface RoomVerificationUpdateTask : Task { + data class Params( + val events: List, + val sasVerificationService: DefaultSasVerificationService, + val cryptoService: CryptoService + ) +} + +internal class DefaultRoomVerificationUpdateTask @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, + private val cryptoService: CryptoService) : RoomVerificationUpdateTask { + + companion object { + // XXX what about multi-account? + private val transactionsHandledByOtherDevice = ArrayList() + } + + override suspend fun execute(params: RoomVerificationUpdateTask.Params): Unit { + + // TODO ignore initial sync or back pagination? + + val now = System.currentTimeMillis() + val tooInThePast = now - (10 * 60 * 1000) + val fiveMinInMs = 5 * 60 * 1000 + val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs + + params.events.forEach { event -> + Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + Timber.v("## SAS Verification live observer: received msgId: $event") + + // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + // the message should be ignored by the receiver. + val ageLocalTs = event.ageLocalTs + if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})") + return@forEach + } else { + val eventOrigin = event.originServerTs ?: -1 + if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin") + return@forEach + } + } + + // decrypt if needed? + if (event.isEncrypted() && event.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + } + } + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + + if (event.senderId == userId) { + // If it's send from me, we need to keep track of Requests or Start + // done from another device of mine + + if (EventType.MESSAGE == event.type) { + val msgType = event.getClearContent().toModel()?.type + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is requested from another device + Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") + event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } + } else if (EventType.KEY_VERIFICATION_START == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } else if (EventType.KEY_VERIFICATION_READY == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { + event.getClearContent().toModel()?.relatesTo?.eventId?.let { + transactionsHandledByOtherDevice.remove(it) + } + } + + return@forEach + } + + val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId + if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { + // Ignore this event, it is directed to another of my devices + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") + return@forEach + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_DONE -> { + params.sasVerificationService.onRoomEvent(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + params.sasVerificationService.onRoomRequestReceived(event) + } + } + } + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt index 1d50fc89fe..45acc80e40 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -189,9 +189,49 @@ internal class DefaultSasVerificationService @Inject constructor( } } - fun onRoomRequestReceived(event: Event) { - // TODO + suspend fun onRoomRequestReceived(event: Event) { Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + val requestInfo = event.getClearContent().toModel() + ?: return + val senderId = event.senderId ?: return + + if (requestInfo.toUserId != credentials.userId) { + //I should ignore this, it's not for me + Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") + return + } + + if(checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) { + //I should ignore this, it's not for me + Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon") + // TODO cancel? + return + } + + // Remember this request + val requestsForUser = pendingRequests[senderId] + ?: ArrayList().also { + pendingRequests[event.senderId] = it + } + + val pendingVerificationRequest = PendingVerificationRequest( + isIncoming = true, + otherUserId = senderId,//requestInfo.toUserId, + transactionId = event.eventId, + requestInfo = requestInfo + ) + requestsForUser.add(pendingVerificationRequest) + + /* + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. + */ } private suspend fun onRoomStartRequestReceived(event: Event) { @@ -263,7 +303,7 @@ internal class DefaultSasVerificationService @Inject constructor( private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") - if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { + if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) { Timber.v("## SAS onStartRequestReceived $startReq") val tid = startReq.transactionID!! val existing = getExistingTransaction(otherUserId, tid) @@ -311,11 +351,11 @@ internal class DefaultSasVerificationService @Inject constructor( } private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: VerificationInfoStart): MXUsersDevicesMap? { + fromDevice: String): MXUsersDevicesMap? { return try { val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null - keys.takeIf { deviceIds.contains(startReq.fromDevice) } + keys.takeIf { deviceIds.contains(fromDevice) } } catch (e: Exception) { null } @@ -333,6 +373,10 @@ internal class DefaultSasVerificationService @Inject constructor( // TODO should we cancel? return } + getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let { + updateOutgoingPendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code))) + // Should we remove it from the list? + } handleOnCancel(event.senderId!!, cancelReq) } @@ -456,6 +500,28 @@ internal class DefaultSasVerificationService @Inject constructor( handleMacReceived(event.senderId, macReq) } + private suspend fun onRoomReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (readyReq == null || readyReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if(checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown") + // TODO cancel? + return + } + + + handleReadyReceived(event.senderId, readyReq) + } + private fun onMacReceived(event: Event) { val macReq = event.getClearContent().toModel()!! @@ -487,6 +553,18 @@ internal class DefaultSasVerificationService @Inject constructor( } } + override fun getExistingVerificationRequest(otherUser: String): List? { + synchronized(lock = pendingRequests) { + return pendingRequests[otherUser] + } + } + + override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } } + } + } + private fun getExistingTransactionsForUser(otherUser: String): Collection? { synchronized(txMap) { return txMap[otherUser]?.values @@ -536,7 +614,30 @@ internal class DefaultSasVerificationService @Inject constructor( } } - override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) { + override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) + : PendingVerificationRequest { + + Timber.i("## SAS Requesting verification to user: $userId in room ${roomId}") + val requestsForUser = pendingRequests[userId] + ?: ArrayList().also { + pendingRequests[userId] = it + } + + val params = requestVerificationDMTask.createParamsAndLocalEcho( + roomId = roomId, + from = credentials.deviceId ?: "", + methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), + to = userId, + cryptoService = cryptoService + ) + val verificationRequest = PendingVerificationRequest( + isIncoming = false, + localID = params.event.eventId ?: "", + otherUserId = userId + ) + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + requestVerificationDMTask.configureWith( requestVerificationDMTask.createParamsAndLocalEcho( roomId = roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt new file mode 100644 index 0000000000..6447b8668b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.verification + +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import java.util.* + +/** + * Stores current pending verification requests + */ +data class PendingVerificationRequest( + val isIncoming: Boolean = false, + val localID: String = UUID.randomUUID().toString(), + val otherUserId: String, + val transactionId: String? = null, + val requestInfo: MessageVerificationRequestContent? = null, + val readyInfo: VerificationInfoReady? = null, + val cancelConclusion: CancelCode? = null, + val isSuccessful : Boolean = false + +) { + + val isReady: Boolean = readyInfo != null + val isSent: Boolean = transactionId != null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index 31d6fd4b5c..c0e3d292c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt @@ -222,13 +222,14 @@ internal abstract class SASVerificationTransaction( val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() if (otherDeviceKey == null) { - Timber.e("Verification: Could not find device $keyIDNoPrefix to verify") + Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify") // just ignore and continue return@forEach } val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) if (mac != theirMac?.mac?.get(it)) { // WRONG! + Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") cancel(CancelCode.MismatchedKeys) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt index 2fee568895..1b25e32d7c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.* -import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask +import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.types -import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import io.realm.OrderedCollectionChangeSet import io.realm.RealmConfiguration import io.realm.RealmResults -import timber.log.Timber -import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList internal class VerificationMessageLiveObserver @Inject constructor( @SessionDatabase realmConfiguration: RealmConfiguration, - @UserId private val userId: String, - @DeviceId private val deviceId: String?, + private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask, private val cryptoService: CryptoService, private val sasVerificationService: DefaultSasVerificationService, private val taskExecutor: TaskExecutor ) : RealmLiveEntityObserver(realmConfiguration) { - override val query = Monarchy.Query { + override val query = Monarchy.Query { EventEntity.types(it, listOf( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_ACCEPT, @@ -61,11 +54,8 @@ internal class VerificationMessageLiveObserver @Inject constructor( ) } - val transactionsHandledByOtherDevice = ArrayList() - override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - // TODO do that in a task - // TODO how to ignore when it's an initial sync? + // Should we ignore when it's an initial sync? val events = changeSet.insertions .asSequence() .mapNotNull { results[it]?.asDomain() } @@ -75,102 +65,9 @@ internal class VerificationMessageLiveObserver @Inject constructor( } .toList() - // TODO ignore initial sync or back pagination? + roomVerificationUpdateTask.configureWith( + RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService) + ).executeBy(taskExecutor) - val now = System.currentTimeMillis() - val tooInThePast = now - (10 * 60 * 1000) - val fiveMinInMs = 5 * 60 * 1000 - val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs - - events.forEach { event -> - Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") - Timber.v("## SAS Verification live observer: received msgId: $event") - - // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, - // the message should be ignored by the receiver. - val ageLocalTs = event.ageLocalTs - if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})") - return@forEach - } else { - val eventOrigin = event.originServerTs ?: -1 - if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin") - return@forEach - } - } - - // decrypt if needed? - if (event.isEncrypted() && event.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.e("## SAS Failed to decrypt event: ${event.eventId}") - } - } - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") - - if (event.senderId == userId) { - // If it's send from me, we need to keep track of Requests or Start - // done from another device of mine - - if (EventType.MESSAGE == event.type) { - val msgType = event.getClearContent().toModel()?.type - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is requested from another device - Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") - event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } - } else if (EventType.KEY_VERIFICATION_START == event.type) { - event.getClearContent().toModel()?.let { - if (it.fromDevice != deviceId) { - // The verification is started from another device - Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") - it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } - } - } - } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { - event.getClearContent().toModel()?.relatesTo?.eventId?.let { - transactionsHandledByOtherDevice.remove(it) - } - } - - return@forEach - } - - val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId - if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { - // Ignore this event, it is directed to another of my devices - Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") - return@forEach - } - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_ACCEPT, - EventType.KEY_VERIFICATION_KEY, - EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_CANCEL, - EventType.KEY_VERIFICATION_DONE -> { - sasVerificationService.onRoomEvent(event) - } - EventType.MESSAGE -> { - if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { - sasVerificationService.onRoomRequestReceived(event) - } - } - } - } } } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index d457581c8e..5fde9c1444 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -272,4 +272,27 @@ interface FragmentModule { @IntoMap @FragmentKey(SoftLogoutFragment::class) fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(VerificationRequestFragment::class) + fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VerificationChooseMethodFragment::class) + fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(SASVerificationCodeFragment::class) + fun bindVerificationSasCodeFragment(fragment: SASVerificationCodeFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(VerificationConclusionFragment::class) + fun bindVerificationSasConclusionFragment(fragment: VerificationConclusionFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt new file mode 100644 index 0000000000..1b56fc2a57 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2018 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.riotx.core.utils + +import android.text.Spannable +import android.text.style.BulletSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import androidx.annotation.ColorInt +import me.gujun.android.span.Span + +fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} + +fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} + +fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable { + if (match.isEmpty()) return this + indexOf(match).takeIf { it != -1 }?.let { start -> + this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return this +} + +fun Span.bullet(text: CharSequence = "", + init: Span.() -> Unit = {}): Span = apply { + append(Span(parent = this).apply { + this.text = text + this.spans.add(BulletSpan()) + init() + build() + }) +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt new file mode 100644 index 0000000000..ebd7f351a4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.OnClick +import com.airbnb.mvrx.* +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import kotlinx.android.synthetic.main.fragment_bottom_sas_verification_code.* +import javax.inject.Inject + +class SASVerificationCodeFragment @Inject constructor( + val viewModelFactory: SASVerificationCodeViewModel.Factory +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bottom_sas_verification_code + + @BindView(R.id.sas_emoji_grid) + lateinit var emojiGrid: ViewGroup + + + @BindView(R.id.sas_decimal_code) + lateinit var decimalTextView: TextView + + @BindView(R.id.emoji0) + lateinit var emoji0View: ViewGroup + @BindView(R.id.emoji1) + lateinit var emoji1View: ViewGroup + @BindView(R.id.emoji2) + lateinit var emoji2View: ViewGroup + @BindView(R.id.emoji3) + lateinit var emoji3View: ViewGroup + @BindView(R.id.emoji4) + lateinit var emoji4View: ViewGroup + @BindView(R.id.emoji5) + lateinit var emoji5View: ViewGroup + @BindView(R.id.emoji6) + lateinit var emoji6View: ViewGroup + + + private val viewModel by fragmentViewModel(SASVerificationCodeViewModel::class) + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun invalidate() = withState(viewModel) { state -> + + if (state.supportsEmoji) { + decimalTextView.isVisible = false + when(val emojiDescription = state.emojiDescription) { + is Success -> { + sasLoadingProgress.isVisible = false + emojiGrid.isVisible = true + ButtonsVisibilityGroup.isVisible = true + emojiDescription.invoke().forEachIndexed { index, emojiRepresentation -> + when (index) { + 0 -> { + emoji0View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 1 -> { + emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 2 -> { + emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 3 -> { + emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId) + } + 4 -> { + emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 5 -> { + emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 6 -> { + emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji6View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + } + } + + if (state.isWaitingFromOther) { + //hide buttons + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + } else { + ButtonsVisibilityGroup.isVisible = true + sasCodeWaitingPartnerText.isVisible = false + } + + } + is Fail -> { + sasLoadingProgress.isVisible = false + emojiGrid.isInvisible = true + ButtonsVisibilityGroup.isInvisible = true + //TODO? + } + else -> { + sasLoadingProgress.isVisible = true + emojiGrid.isInvisible = true + ButtonsVisibilityGroup.isInvisible = true + } + } + } else { + //Decimal + emojiGrid.isInvisible = true + decimalTextView.isVisible = true + val decimalCode = state.decimalDescription.invoke() + decimalTextView.text = decimalCode + + //TODO + if (state.isWaitingFromOther) { + //hide buttons + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + } else { + ButtonsVisibilityGroup.isVisible = decimalCode != null + sasCodeWaitingPartnerText.isVisible = false + } + } + } + + + @OnClick(R.id.sas_request_continue_button) + fun onMatchButtonTapped() = withState(viewModel) { state -> + //UX echo + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + sharedViewModel.handle(VerificationAction.SASMatchAction(state.otherUserId, state.transactionId)) + } + + @OnClick(R.id.sas_request_cancel_button) + fun onDoNotMatchButtonTapped() = withState(viewModel) { state -> + //UX echo + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(state.otherUserId, state.transactionId)) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt new file mode 100644 index 0000000000..5fa9cc6cff --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class SASVerificationCodeViewState( + val transactionId: String, + val otherUserId: String, + val otherUser: MatrixItem? = null, + val supportsEmoji: Boolean = true, + val emojiDescription: Async> = Uninitialized, + val decimalDescription: Async = Uninitialized, + val isWaitingFromOther: Boolean = false +) : MvRxState + +class SASVerificationCodeViewModel @AssistedInject constructor( + @Assisted initialState: SASVerificationCodeViewState, + private val session: Session +) : VectorViewModel(initialState) + , SasVerificationService.SasVerificationListener { + + init { + withState { state -> + val matrixItem = session.getUser(state.otherUserId)?.toMatrixItem() + setState { + copy(otherUser = matrixItem) + } + val sasTx = session.getSasVerificationService() + .getExistingTransaction(state.otherUserId, state.transactionId) + if (sasTx == null) { + setState { + copy( + isWaitingFromOther = false, + emojiDescription = Fail(Throwable("Unknown Transaction")), + decimalDescription = Fail(Throwable("Unknown Transaction")) + ) + } + } else { + refreshStateFromTx(sasTx) + } + } + + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + + private fun refreshStateFromTx(sasTx: SasVerificationTransaction) { + when (sasTx.state) { + SasVerificationTxState.None, + SasVerificationTxState.SendingStart, + SasVerificationTxState.Started, + SasVerificationTxState.OnStarted, + SasVerificationTxState.SendingAccept, + SasVerificationTxState.Accepted, + SasVerificationTxState.OnAccepted, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent, + SasVerificationTxState.OnKeyReceived -> { + setState { + copy( + isWaitingFromOther = false, + supportsEmoji = sasTx.supportsEmoji(), + emojiDescription = Loading>() + .takeIf { sasTx.supportsEmoji() } + ?: Uninitialized, + decimalDescription = Loading() + .takeIf { sasTx.supportsEmoji().not() } + ?: Uninitialized + ) + } + } + SasVerificationTxState.ShortCodeReady -> { + setState { + copy( + isWaitingFromOther = false, + supportsEmoji = sasTx.supportsEmoji(), + emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation()) + else Uninitialized, + decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation()) + else Uninitialized + ) + } + } + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying, + SasVerificationTxState.Verified -> { + setState { + copy(isWaitingFromOther = true) + } + } + SasVerificationTxState.Cancelled, + SasVerificationTxState.OnCancelled -> { + // The fragment should not be rendered in this state, + // it should have been replaced by a conclusion fragment + setState { + copy( + isWaitingFromOther = false, + supportsEmoji = sasTx.supportsEmoji(), + emojiDescription = Fail(Throwable("Transaction Cancelled")), + decimalDescription = Fail(Throwable("Transaction Cancelled")) + ) + } + } + } + } + + override fun transactionCreated(tx: SasVerificationTransaction) { + transactionUpdated(tx) + } + + override fun transactionUpdated(tx: SasVerificationTransaction) { + refreshStateFromTx(tx) + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SASVerificationCodeViewState): SASVerificationCodeViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? { + val factory = (viewModelContext as FragmentViewModelContext).fragment().viewModelFactory + return factory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? { + val args = viewModelContext.args() + return SASVerificationCodeViewState( + transactionId = args.verificationId ?: "", + otherUserId = args.otherUserId + ) + } + } + + override fun handle(action: EmptyAction) { + + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt index d9c3b1d155..d33167518f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt @@ -91,7 +91,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() { (requireActivity() as VectorBaseActivity).notImplemented() /* - viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId + viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId ?: "", object : SimpleApiCallback() { override fun onSuccess(info: MXDeviceInfo?) { info?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt index f14a85c516..637df8818e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest import im.vector.riotx.core.utils.LiveEvent import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt new file mode 100644 index 0000000000..a92910cc92 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.text.toSpannable +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.transition.AutoTransition +import androidx.transition.TransitionManager +import butterknife.BindView +import butterknife.ButterKnife +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.commitTransactionNow +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.bottom_sheet_verification.* +import javax.inject.Inject +import kotlin.reflect.KClass + +class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class VerificationArgs( + val otherUserId: String, + val verificationId: String? = null, + val roomId: String? = null + ) : Parcelable + + + @Inject + lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory + @Inject + lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory + @Inject + lateinit var avatarRenderer: AvatarRenderer + + private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + @BindView(R.id.verificationRequestName) + lateinit var otherUserNameText: TextView + + @BindView(R.id.verificationRequestAvatar) + lateinit var otherUserAvatarImageView: ImageView + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false) + ButterKnife.bind(this, view) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.requestLiveData.observe(this, Observer { + it.peekContent().let { va -> + when (va) { + is Success -> { + if (va.invoke() is VerificationAction.GotItConclusion) { + dismiss() + } + } + } + } + }) + } + + override fun invalidate() = withState(viewModel) { + + it.otherUserMxItem?.let { matrixItem -> + val displayName = matrixItem.displayName ?: "" + otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName) + .toSpannable() + .colorizeMatchingText(displayName, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + + avatarRenderer.render(matrixItem, otherUserAvatarImageView) + } + + // Did the request result in a SAS transaction? + if (it.sasTransactionState != null) { + + when (it.sasTransactionState) { + SasVerificationTxState.None, + SasVerificationTxState.SendingStart, + SasVerificationTxState.Started, + SasVerificationTxState.OnStarted, + SasVerificationTxState.SendingAccept, + SasVerificationTxState.Accepted, + SasVerificationTxState.OnAccepted, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent, + SasVerificationTxState.OnKeyReceived, + SasVerificationTxState.ShortCodeReady, + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying -> { + showFragment(SASVerificationCodeFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs( + it.otherUserMxItem?.id ?: "", + it.pendingRequest?.transactionId)) + }) + } + SasVerificationTxState.Verified, + SasVerificationTxState.Cancelled, + SasVerificationTxState.OnCancelled -> { + showFragment(VerificationConclusionFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args( + it.sasTransactionState == SasVerificationTxState.Verified, + it.cancelCode?.value)) + }) + } + } + + return@withState + } + + + // Transaction has not yet started + if (it.pendingRequest == null || !it.pendingRequest.isReady) { + showFragment(VerificationRequestFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id ?: "")) + }) + } else if (it.pendingRequest.isReady) { + showFragment(VerificationChooseMethodFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id + ?: "", it.pendingRequest.transactionId)) + }) + + } + + super.invalidate() + } + + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + // We want to animate the bottomsheet bound changes + bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout -> + TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 }) + } + // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate) + childFragmentManager.commitTransactionNow { + + replace(R.id.bottomSheetFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } + } +} + + +fun View.getParentCoordinatorLayout(): CoordinatorLayout? { + var current = this as? View + while (current != null) { + if (current is CoordinatorLayout) return current + current = current.parent as? View + } + return null +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt new file mode 100644 index 0000000000..a9265293fa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.core.utils.LiveEvent + + +data class VerificationBottomSheetViewState( + val otherUserMxItem: MatrixItem? = null, + val roomId: String? = null, + val pendingRequest: PendingVerificationRequest? = null, + val sasTransactionState: SasVerificationTxState? = null, + val cancelCode: CancelCode? = null +) : MvRxState + + +sealed class VerificationAction : VectorViewModelAction { + data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction() + data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction() + data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction() + data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction() + object GotItConclusion : VerificationAction() +} + +class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState, + private val session: Session) + : VectorViewModel(initialState), + SasVerificationService.SasVerificationListener { + + + // Can be used for several actions, for a one shot result + private val _requestLiveData = MutableLiveData>>() + val requestLiveData: LiveData>> + get() = _requestLiveData + + init { + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? { + val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args() + + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + val userItem = session.getUser(args.otherUserId) + + val sasTx = state.pendingRequest?.transactionId?.let { + session.getSasVerificationService().getExistingTransaction(args.otherUserId, it) + } + + val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId) + ?.firstOrNull { it.transactionId == args.verificationId } + + return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState( + otherUserMxItem = userItem?.toMatrixItem(), + sasTransactionState = sasTx?.state, + pendingRequest = pr, + roomId = args.roomId) + ) + } + } + + override fun handle(action: VerificationAction) = withState { state -> + val otherUserId = state.otherUserMxItem?.id ?: return@withState + val roomId = state.roomId + ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId + ?: return@withState + when (action) { + is VerificationAction.RequestVerificationByDM -> { +// session + setState { + copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(otherUserId, roomId, null)) + } + } + is VerificationAction.StartSASVerification -> { + val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId) + ?.firstOrNull { it.transactionId == action.pendingRequestTransactionId } + ?: return@withState + + val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice + session.getSasVerificationService().beginKeyVerificationInDMs( + KeyVerificationStart.VERIF_METHOD_SAS, + transactionId = action.pendingRequestTransactionId, + roomId = roomId, + otherUserId = request.otherUserId, + otherDeviceId = otherDevice ?: "", + callback = null + ) + } + is VerificationAction.SASMatchAction -> { + session.getSasVerificationService() + .getExistingTransaction(action.userID, action.sasTransactionId) + ?.userHasVerifiedShortCode() + } + is VerificationAction.SASDoNotMatchAction -> { + session.getSasVerificationService() + .getExistingTransaction(action.userID, action.sasTransactionId) + ?.shortCodeDoNotMatch() + } + is VerificationAction.GotItConclusion -> { + _requestLiveData.postValue(LiveEvent(Success(action))) + } + } + } + + + override fun transactionCreated(tx: SasVerificationTransaction) { + transactionUpdated(tx) + } + + override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state -> + if (tx.transactionId == state.pendingRequest?.transactionId) { + // A SAS tx has been started following this request + setState { + copy( + sasTransactionState = tx.state, + cancelCode = tx.cancelledReason + ) + } + } + } + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + verificationRequestUpdated(pr) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> + + if (pr.localID == state.pendingRequest?.localID || state.pendingRequest?.transactionId == pr.transactionId) { + setState { + copy(pendingRequest = pr) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt new file mode 100644 index 0000000000..69c599d335 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import android.text.style.ClickableSpan +import android.view.View +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import im.vector.riotx.core.utils.tappableMatchingText +import kotlinx.android.synthetic.main.fragment_verification_choose_method.* +import javax.inject.Inject + +class VerificationChooseMethodFragment @Inject constructor( + val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_verification_choose_method + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class) + + override fun invalidate() = withState(viewModel) { state -> + if (state.QRModeAvailable) { + val cSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + + } + } + val openLink = getString(R.string.verify_open_camera_link) + val descCharSequence = + getString(R.string.verify_by_scanning_description, openLink) + .toSpannable() + .tappableMatchingText(openLink, cSpan) + verifyQRDescription.text = descCharSequence + verifyQRGroup.isVisible = true + } else { + verifyQRGroup.isVisible = false + } + + verifyEmojiGroup.isVisible = state.SASMOdeAvailable + } + + @OnClick(R.id.verificationByEmojiButton) + fun doVerifyBySas() = withState(sharedViewModel) { + sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId + ?: "")) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt new file mode 100644 index 0000000000..38482e9b09 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class VerificationChooseMethodViewState( + val otherUserId: String = "", + val transactionId: String = "", + val QRModeAvailable: Boolean = false, + val SASMOdeAvailable: Boolean = false +) : MvRxState + + +class VerificationChooseMethodViewModel @AssistedInject constructor( + @Assisted initialState: VerificationChooseMethodViewState, + private val session: Session +) : VectorViewModel(initialState) { + + + init { + withState { state -> + val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId)?.first { + it.transactionId == state.transactionId + } + val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN) ?: false + val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS) ?: false + setState { + copy(QRModeAvailable = qrAvailable, SASMOdeAvailable = emojiAvailable) + } + } + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? { + val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.verificationChooseMethodViewModelFactory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? { + val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args() + return VerificationChooseMethodViewState(otherUserId = args.otherUserId, transactionId = args.verificationId ?: "") + } + } + + + override fun handle(action: EmptyAction) {} + + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt new file mode 100644 index 0000000000..da3b0dd187 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import android.os.Parcelable +import androidx.core.content.ContextCompat +import butterknife.OnClick +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import io.noties.markwon.Markwon +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_verification_conclusion.* +import javax.inject.Inject + +class VerificationConclusionFragment @Inject constructor() : VectorBaseFragment() { + + @Parcelize + data class Args( + val isSuccessFull: Boolean, + val cancelReason: String? + ) : Parcelable + + override fun getLayoutResId() = R.layout.fragment_verification_conclusion + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class) + + override fun invalidate() = withState(viewModel) { + when (it.conclusionState) { + ConclusionState.SUCCESS -> { + verificationConclusionTitle.text = getString(R.string.sas_verified) + verifyConclusionDescription.setTextOrHide(getString(R.string.sas_verified_successful_description)) + verifyConclusionBottomDescription.text = getString(R.string.verification_green_shield) + verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_trusted)) + + } + ConclusionState.WARNING -> { + verificationConclusionTitle.text = getString(R.string.verification_conclusion_not_secure) + verifyConclusionDescription.setTextOrHide(null) + verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_warning)) + + verifyConclusionBottomDescription.text = Markwon.builder(requireContext()).build().toMarkdown(getString(R.string.verification_conclusion_compromised)) + } + ConclusionState.CANCELLED -> { + // Just dismiss in this case + sharedViewModel.handle(VerificationAction.GotItConclusion) + } + } + } + + @OnClick(R.id.verificationConclusionButton) + fun onButtonTapped() { + sharedViewModel.handle(VerificationAction.GotItConclusion) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt new file mode 100644 index 0000000000..ca069bf853 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.safeValueOf +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class SASVerificationConclusionViewState( + val conclusionState: ConclusionState = ConclusionState.CANCELLED +) : MvRxState + +enum class ConclusionState { + SUCCESS, + WARNING, + CANCELLED +} + +class VerificationConclusionViewModel(initialState: SASVerificationConclusionViewState) + : VectorViewModel(initialState) { + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? { + val args = viewModelContext.args() + + return when (safeValueOf(args.cancelReason)) { + CancelCode.MismatchedSas, + CancelCode.MismatchedCommitment, + CancelCode.MismatchedKeys -> { + SASVerificationConclusionViewState(ConclusionState.WARNING) + } + else -> { + SASVerificationConclusionViewState( + if (args.isSuccessFull) ConclusionState.SUCCESS + else ConclusionState.CANCELLED + ) + } + } + } + } + + override fun handle(action: EmptyAction) {} +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt new file mode 100644 index 0000000000..60b89c19d6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import android.graphics.Typeface +import androidx.core.text.toSpannable +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.platform.parentFragmentViewModel +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.core.utils.styleMatchingText +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.fragment_verification_request.* +import javax.inject.Inject + +class VerificationRequestFragment @Inject constructor( + val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory, + val avatarRenderer: AvatarRenderer +) : VectorBaseFragment() { + + private val viewModel by fragmentViewModel(VerificationRequestViewModel::class) + + private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_verification_request + + override fun invalidate() = withState(viewModel) { state -> + state.matrixItem.let { + val styledText = getString(R.string.verification_request_alert_description, it.id) + .toSpannable() + .styleMatchingText(it.id, Typeface.BOLD) + .colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + verificationRequestText.text = styledText + } + + when (state.started) { + is Loading -> { + //Hide the start button, show waiting + verificationStartButton.isInvisible = true + verificationWaitingText.isVisible = true + val otherUser = state.matrixItem.displayName ?: state.matrixItem.id + verificationWaitingText.text = getString(R.string.verification_request_waiting_for, otherUser) + .toSpannable() + .styleMatchingText(otherUser, Typeface.BOLD) + .colorizeMatchingText(otherUser, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color)) + } + else -> { + verificationStartButton.isEnabled = true + verificationStartButton.isVisible = true + verificationWaitingText.isInvisible = true + } + } + + Unit + } + + @OnClick(R.id.verificationStartButton) + fun onClickOnVerificationStart() = withState(viewModel) { state -> + verificationStartButton.isEnabled = false + sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId)) + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt new file mode 100644 index 0000000000..752d6a6a8a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 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.riotx.features.crypto.verification + +import com.airbnb.mvrx.* +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.platform.VectorViewModel + + +data class VerificationRequestViewState( + val roomId: String? = null, + val matrixItem: MatrixItem, + val started: Async = Success(false) +) : MvRxState + + +class VerificationRequestViewModel @AssistedInject constructor( + @Assisted initialState: VerificationRequestViewState, + private val session: Session +) : VectorViewModel(initialState), SasVerificationService.SasVerificationListener { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel + } + + init { + withState { + val pr = session.getSasVerificationService() + .getExistingVerificationRequest(it.matrixItem.id) + ?.firstOrNull() + setState { + copy( + started = Success(false).takeIf { pr == null } + ?: Success(true).takeIf { pr?.isReady == true } + ?: Loading() + ) + } + } + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + session.getSasVerificationService().removeListener(this) + super.onCleared() + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? { + val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.verificationRequestViewModelFactory.create(state) + } + + override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? { + val otherUserId = viewModelContext.args().otherUserId + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + return session.getUser(otherUserId)?.let { + VerificationRequestViewState(matrixItem = it.toMatrixItem()) + } + } + } + + override fun handle(action: VerificationAction) { + } + + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) {} + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + verificationRequestUpdated(pr) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> + if (pr.otherUserId == state.matrixItem.id) { + if (pr.isReady) { + setState { + copy(started = Success(true)) + } + } else { + setState { + copy(started = Loading()) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index e983542ad2..3768820206 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -923,7 +923,7 @@ class RoomDetailFragment @Inject constructor( } is Success -> { when (val data = result.invoke()) { - is RoomDetailAction.ReportContent -> { + is RoomDetailAction.ReportContent -> { when { data.spam -> { AlertDialog.Builder(requireActivity()) @@ -960,6 +960,22 @@ class RoomDetailFragment @Inject constructor( } } } + is RoomDetailAction.RequestVerification -> { + VerificationBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(data.userId, roomId = roomDetailArgs.roomId)) + } +// setArguments() + }.show(parentFragmentManager, "REQ") + } + is RoomDetailAction.AcceptVerificationRequest -> { + VerificationBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs( + data.otherUserId, data.transactionId, roomId = roomDetailArgs.roomId)) + } + }.show(parentFragmentManager, "REQ") + } } } } @@ -1114,7 +1130,8 @@ class RoomDetailFragment @Inject constructor( } override fun onAvatarClicked(informationData: MessageInformationData) { - vectorBaseActivity.notImplemented("Click on user avatar") + //vectorBaseActivity.notImplemented("Click on user avatar") + roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId)) } override fun onMemberNameClicked(informationData: MessageInformationData) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index b0c0144d66..0212ee75d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -49,7 +49,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.R @@ -184,8 +183,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) } } @@ -796,20 +796,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) { - session.getSasVerificationService().beginKeyVerificationInDMs( - KeyVerificationStart.VERIF_METHOD_SAS, - action.transactionId, - room.roomId, - action.otherUserId, - action.otherdDeviceId, - null - ) + session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId,room.roomId, + action.transactionId) + _requestLiveData.postValue(LiveEvent(Success(action))) +// session.getSasVerificationService().beginKeyVerificationInDMs( +// KeyVerificationStart.VERIF_METHOD_SAS, +// action.transactionId, +// room.roomId, +// action.otherUserMxItem, +// action.otherdDeviceId, +// null +// ) } private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { Timber.e("TODO implement $action") } + private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) { + _requestLiveData.postValue(LiveEvent(Success(action))) + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index dadf267dd7..36856eebe4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor( return VerificationRequestItem_() .attributes( VerificationRequestItem.Attributes( - otherUserId, - otherUserName.toString(), - messageContent.fromDevice, - informationData.eventId, - informationData, - attributes.avatarRenderer, - attributes.colorProvider, - attributes.itemLongClickListener, - attributes.itemClickListener, - attributes.reactionPillCallback, - attributes.readReceiptsCallback, - attributes.emojiTypeFace + otherUserId = otherUserId, + otherUserName = otherUserName.toString(), + fromDevide = messageContent.fromDevice, + referenceId = informationData.eventId, + informationData = informationData, + avatarRenderer = attributes.avatarRenderer, + colorProvider = attributes.colorProvider, + itemLongClickListener = attributes.itemLongClickListener, + itemClickListener = attributes.itemClickListener, + reactionPillCallback = attributes.reactionPillCallback, + readReceiptsCallback = attributes.readReceiptsCallback, + emojiTypeFace = attributes.emojiTypeFace ) ) .callback(callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 13679cecaf..3ff4af27c2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_MAC -> { // These events are filtered from timeline in normal case // Only visible in developer mode diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 39afebf5af..c8058d0fa8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY, EventType.REDACTION -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 71272fe815..2864fe6802 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -50,7 +50,8 @@ object TimelineDisplayableEvents { EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, - EventType.KEY_VERIFICATION_KEY + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_READY ) } diff --git a/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml new file mode 100644 index 0000000000..bf51aab3df --- /dev/null +++ b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_choose_method.xml b/vector/src/main/res/layout/fragment_verification_choose_method.xml new file mode 100644 index 0000000000..37b3c6e53a --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_choose_method.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_conclusion.xml b/vector/src/main/res/layout/fragment_verification_conclusion.xml new file mode 100644 index 0000000000..099297b936 --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_conclusion.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 07a2f40bbd..213600b2b4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1500,7 +1500,7 @@ Why choose Riot.im? Verified! You\'ve successfully verified this device. - Secure messages with this user are end-to-end encrypted and not able to be read by third parties. + Messages with this user in this room are end-to-end encrypted and can‘t be read by third parties. Got it Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification. diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1c7b9756c0..e9db6a4f39 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,15 +5,25 @@ Request to verify the given userID Prepends ¯\\_(ツ)_/¯ to a plain-text message + Initial Sync… - File - Audio - Image. - Video. - - Untrusted sign in + They match + They don‘t match + Verify this user by confirming the following unique emoji appear on their screen, in the same order." + For ultimate security, use another trusted means of communication or do this in person. + Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure. + + Not secure + One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device + + + Video. + Image. + Audio + File + Waiting… %s cancelled You cancelled